Split reset_agent_config into two independent methods for getting and resetting model parameters Add functionality to read quota configuration from environment variables to the default free tier
2700 lines
98 KiB
Python
2700 lines
98 KiB
Python
"""
|
||
应用服务层
|
||
|
||
提供应用管理的业务逻辑,包括:
|
||
- 应用的创建、更新、查询
|
||
- Agent 配置管理
|
||
- 应用发布和版本管理
|
||
- 应用回滚
|
||
"""
|
||
import copy
|
||
import datetime
|
||
import uuid
|
||
from typing import Annotated, Any, Dict, List, Optional, Tuple
|
||
|
||
from fastapi import Depends
|
||
from sqlalchemy import and_, delete, func, or_, select, update as sa_update
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.error_codes import BizCode
|
||
from app.core.exceptions import (
|
||
BusinessException,
|
||
ResourceNotFoundException,
|
||
)
|
||
from app.core.logging_config import get_business_logger
|
||
from app.core.workflow.validator import WorkflowValidator
|
||
from app.db import get_db
|
||
from app.models import (
|
||
AgentConfig,
|
||
App,
|
||
AppRelease,
|
||
AppShare,
|
||
MultiAgentConfig,
|
||
WorkflowConfig,
|
||
Workspace,
|
||
)
|
||
from app.models.app_model import AppStatus, AppType
|
||
from app.repositories.app_repository import get_apps_by_id, AppRepository
|
||
from app.repositories.workflow_repository import WorkflowConfigRepository
|
||
from app.schemas import app_schema
|
||
from app.schemas.workflow_schema import WorkflowConfigUpdate
|
||
from app.services.agent_config_converter import AgentConfigConverter
|
||
from app.services.model_service import ModelApiKeyService
|
||
from app.services.workflow_service import WorkflowService
|
||
from app.utils.app_config_utils import model_parameters_to_dict
|
||
|
||
# 获取业务日志器
|
||
logger = get_business_logger()
|
||
|
||
|
||
class AppService:
|
||
"""应用服务类
|
||
|
||
负责应用相关的所有业务逻辑处理,遵循单一职责原则。
|
||
"""
|
||
|
||
def __init__(self, db: Session):
|
||
"""初始化应用服务
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
"""
|
||
self.db = db
|
||
self.app_repo = AppRepository(self.db)
|
||
|
||
# ==================== 私有辅助方法 ====================
|
||
|
||
def _validate_workspace_access(self, app: App, workspace_id: Optional[uuid.UUID]) -> None:
|
||
"""验证工作空间访问权限(严格模式,用于修改操作)
|
||
|
||
Args:
|
||
app: 应用对象
|
||
workspace_id: 工作空间ID
|
||
|
||
Raises:
|
||
BusinessException: 当应用不在指定工作空间时
|
||
"""
|
||
if workspace_id is not None and app.workspace_id != workspace_id:
|
||
logger.warning(
|
||
"工作空间访问被拒",
|
||
extra={"app_id": str(app.id), "workspace_id": str(workspace_id)}
|
||
)
|
||
raise BusinessException("应用不在指定工作空间中", BizCode.WORKSPACE_NO_ACCESS)
|
||
|
||
|
||
|
||
def _check_app_accessible(self, app: App, workspace_id: Optional[uuid.UUID]) -> bool:
|
||
"""检查应用是否可访问(包括共享应用)
|
||
|
||
Args:
|
||
app: 应用对象
|
||
workspace_id: 工作空间ID
|
||
|
||
Returns:
|
||
bool: 是否可访问
|
||
"""
|
||
from app.models import AppShare
|
||
|
||
if workspace_id is None:
|
||
return True
|
||
|
||
# 1. 检查是否是本工作空间的应用
|
||
if app.workspace_id == workspace_id:
|
||
return True
|
||
|
||
# 2. 检查是否是共享给本工作空间的应用
|
||
stmt = select(AppShare).where(
|
||
AppShare.source_app_id == app.id,
|
||
AppShare.target_workspace_id == workspace_id,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
share = self.db.scalars(stmt).first()
|
||
|
||
return share is not None
|
||
|
||
def _validate_app_accessible(self, app: App, workspace_id: Optional[uuid.UUID]) -> None:
|
||
"""验证应用是否可访问(包括共享应用,用于只读操作)
|
||
|
||
Args:
|
||
app: 应用对象
|
||
workspace_id: 工作空间ID
|
||
|
||
Raises:
|
||
BusinessException: 当应用不可访问时
|
||
"""
|
||
if not self._check_app_accessible(app, workspace_id):
|
||
logger.warning(
|
||
"应用访问被拒",
|
||
extra={"app_id": str(app.id), "workspace_id": str(workspace_id)}
|
||
)
|
||
raise BusinessException("应用不可访问", BizCode.WORKSPACE_NO_ACCESS)
|
||
|
||
def _unique_app_name(self, name: str, workspace_id: uuid.UUID, app_type: AppType) -> str:
|
||
"""生成唯一应用名称,同时检查本空间自有应用和共享到本空间的应用"""
|
||
existing = {r[0] for r in self.db.query(App.name).filter(
|
||
App.workspace_id == workspace_id,
|
||
App.type == app_type,
|
||
App.is_active.is_(True)
|
||
).all()}
|
||
shared_names = {r[0] for r in self.db.query(App.name).join(
|
||
AppShare, AppShare.source_app_id == App.id
|
||
).filter(
|
||
AppShare.target_workspace_id == workspace_id,
|
||
App.type == app_type,
|
||
App.is_active.is_(True)
|
||
).all()}
|
||
existing |= shared_names
|
||
if name not in existing:
|
||
return name
|
||
counter = 1
|
||
while f"{name}({counter})" in existing:
|
||
counter += 1
|
||
return f"{name}({counter})"
|
||
|
||
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,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
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.
|
||
|
||
- Own workspace app: allowed
|
||
- Shared app with editable permission: allowed
|
||
- Shared app with readonly permission: 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
|
||
|
||
# Check share permission
|
||
permission = self._get_share_permission(app, workspace_id)
|
||
if permission == "editable":
|
||
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:
|
||
"""获取应用或抛出404异常
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
|
||
Returns:
|
||
App: 应用对象
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
"""
|
||
app = get_apps_by_id(self.db, app_id)
|
||
if not app:
|
||
logger.warning("应用不存在", extra={"app_id": str(app_id)})
|
||
raise ResourceNotFoundException("应用", str(app_id))
|
||
return app
|
||
|
||
def _check_workflow_config(self, app_id: uuid.UUID):
|
||
from sqlalchemy import select
|
||
|
||
from app.core.exceptions import BusinessException
|
||
from app.models import ModelConfig, WorkflowConfig
|
||
# 2. 获取 Agent 配置
|
||
stmt = select(WorkflowConfig).where(AgentConfig.app_id == app_id)
|
||
agent_cfg = self.db.scalars(stmt).first()
|
||
if not agent_cfg:
|
||
raise BusinessException("Agent 配置不存在,无法试运行", BizCode.AGENT_CONFIG_MISSING)
|
||
|
||
# 3. 获取模型配置
|
||
model_config = None
|
||
if agent_cfg.default_model_config_id:
|
||
model_config = self.db.get(ModelConfig, agent_cfg.default_model_config_id)
|
||
|
||
if not model_config:
|
||
raise BusinessException("模型配置不存在,无法试运行", BizCode.AGENT_CONFIG_MISSING)
|
||
|
||
def _check_agent_config(self, app_id: uuid.UUID):
|
||
from sqlalchemy import select
|
||
|
||
from app.core.exceptions import BusinessException
|
||
from app.models import AgentConfig, ModelConfig
|
||
# 2. 获取 Agent 配置
|
||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id)
|
||
agent_cfg = self.db.scalars(stmt).first()
|
||
if not agent_cfg:
|
||
raise BusinessException("Agent 配置不存在,无法试运行", BizCode.AGENT_CONFIG_MISSING)
|
||
|
||
# 3. 获取模型配置
|
||
model_config = None
|
||
if agent_cfg.default_model_config_id:
|
||
model_config = self.db.get(ModelConfig, agent_cfg.default_model_config_id)
|
||
|
||
if not model_config:
|
||
raise BusinessException("模型配置不存在,无法试运行", BizCode.AGENT_CONFIG_MISSING)
|
||
|
||
def _check_multi_agent_config(self, app_id: uuid.UUID):
|
||
"""检查多智能体配置的完整性
|
||
|
||
验证内容:
|
||
1. 多智能体配置是否存在
|
||
2. 主 Agent 配置是否存在
|
||
3. 子 Agent 配置是否存在
|
||
4. 所有 Agent 的模型配置是否存在
|
||
|
||
Args:
|
||
app_id: 应用 ID
|
||
|
||
Raises:
|
||
BusinessException: 配置不完整或不存在时抛出
|
||
"""
|
||
from app.models import ModelConfig
|
||
from app.services.multi_agent_service import MultiAgentService
|
||
|
||
# 1. 检查多智能体配置是否存在
|
||
service = MultiAgentService(self.db)
|
||
multi_agent_config = service.get_config(app_id)
|
||
|
||
if not multi_agent_config:
|
||
raise BusinessException(
|
||
"多智能体配置不存在,无法运行",
|
||
BizCode.AGENT_CONFIG_MISSING
|
||
)
|
||
|
||
if not multi_agent_config.is_active:
|
||
raise BusinessException(
|
||
"多智能体配置未激活,无法运行",
|
||
BizCode.AGENT_CONFIG_MISSING
|
||
)
|
||
if multi_agent_config.orchestration_mode == "supervisor":
|
||
if not multi_agent_config.default_model_config_id:
|
||
# # 2. 检查主 Agent 配置
|
||
if not multi_agent_config.master_agent_id:
|
||
raise BusinessException(
|
||
"未配置主 Agent,无法运行",
|
||
BizCode.AGENT_CONFIG_MISSING
|
||
)
|
||
|
||
master_agent_release = self.db.get(AppRelease, multi_agent_config.master_agent_id)
|
||
if not master_agent_release:
|
||
raise BusinessException(
|
||
f"主 Agent 配置不存在: {multi_agent_config.master_agent_id}",
|
||
BizCode.AGENT_CONFIG_MISSING
|
||
)
|
||
|
||
# 检查主 Agent 的模型配置
|
||
multi_agent_config.default_model_config_id = master_agent_release.default_model_config_id
|
||
|
||
model_api_key = ModelApiKeyService.get_available_api_key(self.db, multi_agent_config.default_model_config_id)
|
||
if not model_api_key:
|
||
raise ResourceNotFoundException("模型配置", str(multi_agent_config.default_model_config_id))
|
||
|
||
# 3. 检查子 Agent 配置
|
||
if not multi_agent_config.sub_agents or len(multi_agent_config.sub_agents) == 0:
|
||
raise BusinessException(
|
||
"未配置子 Agent,无法运行",
|
||
BizCode.AGENT_CONFIG_MISSING
|
||
)
|
||
|
||
# 4. 验证每个子 Agent 及其模型配置
|
||
for idx, sub_agent_data in enumerate(multi_agent_config.sub_agents):
|
||
agent_id = sub_agent_data.get('agent_id')
|
||
if not agent_id:
|
||
raise BusinessException(
|
||
f"子 Agent #{idx + 1} 缺少 agent_id",
|
||
BizCode.AGENT_CONFIG_MISSING
|
||
)
|
||
|
||
# 转换为 UUID
|
||
try:
|
||
from uuid import UUID
|
||
agent_uuid = UUID(agent_id) if isinstance(agent_id, str) else agent_id
|
||
except (ValueError, TypeError):
|
||
raise BusinessException(
|
||
f"子 Agent #{idx + 1} 的 agent_id 格式无效: {agent_id}",
|
||
BizCode.INVALID_PARAMETER
|
||
)
|
||
|
||
# 检查子 Agent 是否存在
|
||
sub_agent_release = self.db.get(AppRelease, agent_uuid)
|
||
if not sub_agent_release:
|
||
raise BusinessException(
|
||
f"子 Agent 配置不存在: {agent_id} ({sub_agent_data.get('name', '未命名')})",
|
||
BizCode.AGENT_CONFIG_MISSING
|
||
)
|
||
|
||
# 检查子 Agent 的模型配置
|
||
if sub_agent_release.default_model_config_id:
|
||
sub_model = self.db.get(ModelConfig, sub_agent_release.default_model_config_id)
|
||
if not sub_model:
|
||
raise BusinessException(
|
||
f"子 Agent '{sub_agent_data.get('name', '未命名')}' 的模型配置不存在: {sub_agent_release.default_model_config_id}",
|
||
BizCode.MODEL_NOT_FOUND
|
||
)
|
||
else:
|
||
raise BusinessException(
|
||
f"子 Agent '{sub_agent_data.get('name', '未命名')}' 未配置模型,无法运行",
|
||
BizCode.MODEL_NOT_FOUND
|
||
)
|
||
|
||
logger.info(
|
||
"多智能体配置检查通过"
|
||
)
|
||
|
||
def _create_agent_config(
|
||
self,
|
||
app_id: uuid.UUID,
|
||
config_data: app_schema.AgentConfigCreate,
|
||
now: datetime.datetime
|
||
) -> None:
|
||
"""创建 Agent 配置(内部方法)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
config_data: Agent 配置数据
|
||
now: 当前时间
|
||
"""
|
||
storage_data = AgentConfigConverter.to_storage_format(config_data)
|
||
|
||
agent_cfg = AgentConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=app_id,
|
||
system_prompt=config_data.system_prompt,
|
||
default_model_config_id=config_data.default_model_config_id,
|
||
model_parameters=storage_data.get("model_parameters"),
|
||
knowledge_retrieval=storage_data.get("knowledge_retrieval"),
|
||
memory=storage_data.get("memory"),
|
||
variables=storage_data.get("variables", []),
|
||
tools=storage_data.get("tools", []),
|
||
skills=storage_data.get("skills", {}),
|
||
features=storage_data.get("features", {}),
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(agent_cfg)
|
||
logger.debug("Agent 配置已创建", extra={"app_id": str(app_id)})
|
||
|
||
def _create_workflow_config(
|
||
self,
|
||
app_id: uuid.UUID,
|
||
data,
|
||
now: datetime.datetime
|
||
):
|
||
workflow_cfg = WorkflowConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=app_id,
|
||
nodes=[node.model_dump() for node in data.nodes] if data.nodes else [],
|
||
edges=[edge.model_dump() for edge in data.edges] if data.edges else [],
|
||
variables=[var.model_dump() for var in data.variables] if data.variables else [],
|
||
execution_config=data.execution_config.model_dump() if data.execution_config else {},
|
||
features=data.features if data.features else {},
|
||
triggers=[trigger.model_dump() for trigger in data.triggers] if data.triggers else [],
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
self.db.add(workflow_cfg)
|
||
|
||
def _create_multi_agent_config(
|
||
self,
|
||
app_id: uuid.UUID,
|
||
config_data: Dict[str, Any],
|
||
now: datetime.datetime
|
||
) -> None:
|
||
"""创建多 Agent 配置(内部方法)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
config_data: 多 Agent 配置数据(Dict)
|
||
now: 当前时间
|
||
"""
|
||
# 将 Dict 转换为 MultiAgentConfigCreate
|
||
from app.schemas.multi_agent_schema import (
|
||
ExecutionConfig,
|
||
MultiAgentConfigCreate,
|
||
RoutingRule,
|
||
SubAgentConfig,
|
||
)
|
||
|
||
# 转换 sub_agents
|
||
sub_agents = [SubAgentConfig(**sa) for sa in config_data.get('sub_agents', [])]
|
||
|
||
# 转换 routing_rules(如果有)
|
||
routing_rules = None
|
||
if config_data.get('routing_rules'):
|
||
routing_rules = [RoutingRule(**rr) for rr in config_data['routing_rules']]
|
||
|
||
# 转换 execution_config
|
||
execution_config = ExecutionConfig(**config_data.get('execution_config', {}))
|
||
|
||
# 创建 MultiAgentConfigCreate 对象
|
||
config = MultiAgentConfigCreate(
|
||
master_agent_id=config_data['master_agent_id'],
|
||
orchestration_mode=config_data['orchestration_mode'],
|
||
sub_agents=sub_agents,
|
||
routing_rules=routing_rules,
|
||
execution_config=execution_config,
|
||
aggregation_strategy=config_data.get('aggregation_strategy', 'merge')
|
||
)
|
||
|
||
# 验证主 Agent 存在
|
||
master_agent = self.db.get(AgentConfig, config.master_agent_id)
|
||
if not master_agent:
|
||
raise ResourceNotFoundException("主 Agent", str(config.master_agent_id))
|
||
|
||
# 验证子 Agent 存在
|
||
for sub_agent in config.sub_agents:
|
||
agent = self.db.get(AgentConfig, sub_agent.agent_id)
|
||
if not agent:
|
||
raise ResourceNotFoundException("子 Agent", str(sub_agent.agent_id))
|
||
|
||
# 创建多 Agent 配置
|
||
# 将 UUID 转换为字符串以便 JSON 序列化
|
||
sub_agents_data = []
|
||
for sub_agent in config.sub_agents:
|
||
sa_dict = sub_agent.model_dump()
|
||
sa_dict['agent_id'] = str(sa_dict['agent_id']) # UUID -> str
|
||
sub_agents_data.append(sa_dict)
|
||
|
||
routing_rules_data = None
|
||
if config.routing_rules:
|
||
routing_rules_data = []
|
||
for rule in config.routing_rules:
|
||
rule_dict = rule.model_dump()
|
||
rule_dict['target_agent_id'] = str(rule_dict['target_agent_id']) # UUID -> str
|
||
routing_rules_data.append(rule_dict)
|
||
|
||
multi_agent_cfg = MultiAgentConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=app_id,
|
||
master_agent_id=config.master_agent_id,
|
||
orchestration_mode=config.orchestration_mode,
|
||
sub_agents=sub_agents_data,
|
||
routing_rules=routing_rules_data,
|
||
execution_config=config.execution_config.model_dump(),
|
||
aggregation_strategy=config.aggregation_strategy,
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(multi_agent_cfg)
|
||
logger.debug("多 Agent 配置已创建", extra={"app_id": str(app_id), "mode": config.orchestration_mode})
|
||
|
||
def _get_next_version(self, app_id: uuid.UUID) -> int:
|
||
"""获取下一个版本号
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
|
||
Returns:
|
||
int: 下一个版本号
|
||
"""
|
||
stmt = select(func.max(AppRelease.version)).where(AppRelease.app_id == app_id)
|
||
max_ver = self.db.execute(stmt).scalar()
|
||
return 1 if max_ver is None else int(max_ver) + 1
|
||
|
||
def _convert_to_schema(
|
||
self,
|
||
app: App,
|
||
current_workspace_id: uuid.UUID
|
||
) -> app_schema.App:
|
||
"""将 App 模型转换为 Schema,并设置 is_shared 字段
|
||
|
||
Args:
|
||
app: App 模型实例
|
||
current_workspace_id: 当前工作空间ID
|
||
|
||
Returns:
|
||
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
|
||
share_id = None
|
||
shared_by = None
|
||
shared_by_name = None
|
||
shared_at = 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,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
share = self.db.scalars(stmt).first()
|
||
if share:
|
||
share_id = share.id
|
||
share_permission = share.permission
|
||
shared_by = share.shared_by
|
||
shared_at = share.created_at
|
||
if share.shared_user:
|
||
shared_by_name = share.shared_user.username
|
||
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 = {
|
||
"id": app.id,
|
||
"workspace_id": app.workspace_id,
|
||
"created_by": app.created_by,
|
||
"name": app.name,
|
||
"description": app.description,
|
||
"icon": app.icon,
|
||
"icon_type": app.icon_type,
|
||
"type": app.type,
|
||
"visibility": app.visibility,
|
||
"status": app.status,
|
||
"tags": app.tags or [],
|
||
"current_release_id": app.current_release_id,
|
||
"is_active": app.is_active,
|
||
"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,
|
||
"share_id": share_id,
|
||
"shared_by": shared_by,
|
||
"shared_by_name": shared_by_name,
|
||
"shared_at": shared_at,
|
||
"created_at": app.created_at,
|
||
"updated_at": app.updated_at
|
||
}
|
||
return app_schema.App(**app_dict)
|
||
|
||
# ==================== 应用管理 ====================
|
||
|
||
def get_app(
|
||
self,
|
||
app_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> App:
|
||
"""获取应用详情
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
workspace_id: 工作空间ID(用于权限验证,支持共享应用)
|
||
|
||
Returns:
|
||
App: 应用对象
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用不可访问时
|
||
"""
|
||
app = self._get_app_or_404(app_id)
|
||
self._validate_app_accessible(app, workspace_id)
|
||
return app
|
||
|
||
def get_release_by_id(self, app_id: uuid.UUID, release_id: uuid.UUID) -> AppRelease:
|
||
"""按发布版本ID获取发布快照
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
release_id: 发布版本ID
|
||
|
||
Returns:
|
||
AppRelease: 发布快照
|
||
|
||
Raises:
|
||
BusinessException: 版本不存在或已下线
|
||
"""
|
||
from app.repositories.app_repository import get_release_by_id
|
||
release = get_release_by_id(self.db, app_id, release_id)
|
||
if not release:
|
||
raise BusinessException(
|
||
f"版本 {release_id} 不存在或已下线",
|
||
BizCode.RELEASE_NOT_FOUND,
|
||
)
|
||
return release
|
||
|
||
def create_app(
|
||
self,
|
||
*,
|
||
user_id: uuid.UUID,
|
||
workspace_id: uuid.UUID,
|
||
data: app_schema.AppCreate
|
||
) -> App:
|
||
"""创建应用
|
||
|
||
Args:
|
||
user_id: 创建者用户ID
|
||
workspace_id: 工作空间ID
|
||
data: 应用创建数据
|
||
|
||
Returns:
|
||
App: 创建的应用对象
|
||
|
||
Raises:
|
||
BusinessException: 当创建失败时
|
||
"""
|
||
logger.info(
|
||
"创建应用",
|
||
extra={"app_name": data.name, "type": data.type, "workspace_id": str(workspace_id)}
|
||
)
|
||
apps = self.app_repo.get_apps_by_name(data.name, data.type, workspace_id)
|
||
if apps:
|
||
raise BusinessException(message="已存在同名应用", code=BizCode.RESOURCE_ALREADY_EXISTS)
|
||
|
||
try:
|
||
now = datetime.datetime.now()
|
||
|
||
app = App(
|
||
id=uuid.uuid4(),
|
||
workspace_id=workspace_id,
|
||
created_by=user_id,
|
||
name=data.name,
|
||
description=data.description,
|
||
icon=data.icon,
|
||
icon_type=data.icon_type,
|
||
type=data.type,
|
||
visibility=data.visibility,
|
||
status=data.status,
|
||
tags=data.tags or [],
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(app)
|
||
self.db.flush() # 获取 app.id
|
||
|
||
# 如果是 agent 类型且提供了配置,创建 AgentConfig
|
||
if app.type == "agent" and data.agent_config:
|
||
self._create_agent_config(app.id, data.agent_config, now)
|
||
|
||
# 如果是 multi_agent 类型且提供了配置,创建 MultiAgentConfig
|
||
if app.type == "multi_agent" and data.multi_agent_config:
|
||
self._create_multi_agent_config(app.id, data.multi_agent_config, now)
|
||
|
||
if app.type == "workflow" and data.workflow_config:
|
||
from app.schemas.workflow_schema import WorkflowConfigCreate
|
||
wf_data = WorkflowConfigCreate(**data.workflow_config) if isinstance(data.workflow_config, dict) else data.workflow_config
|
||
self._create_workflow_config(app.id, wf_data, now)
|
||
|
||
self.db.commit()
|
||
self.db.refresh(app)
|
||
|
||
logger.info("应用创建成功", extra={"app_id": str(app.id), "app_name": app.name})
|
||
return app
|
||
|
||
except Exception as e:
|
||
self.db.rollback()
|
||
logger.error("应用创建失败", extra={"app_name": data.name, "error": str(e)})
|
||
raise BusinessException(f"应用创建失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
|
||
|
||
def update_app(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
data: app_schema.AppUpdate,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> App:
|
||
"""更新应用基本信息
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
data: 更新数据
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
App: 更新后的应用对象
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用不在指定工作空间时
|
||
"""
|
||
logger.info("更新应用", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
self._validate_app_writable(app, workspace_id)
|
||
|
||
changed = False
|
||
for field in ["name", "description", "icon", "icon_type", "visibility", "status", "tags"]:
|
||
val = getattr(data, field, None)
|
||
if val is not None:
|
||
setattr(app, field, val)
|
||
changed = True
|
||
|
||
if changed:
|
||
app.updated_at = datetime.datetime.now()
|
||
self.db.commit()
|
||
self.db.refresh(app)
|
||
logger.info("应用更新成功", extra={"app_id": str(app_id)})
|
||
else:
|
||
logger.debug("应用无变更", extra={"app_id": str(app_id)})
|
||
|
||
return app
|
||
|
||
def delete_app(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> None:
|
||
"""删除应用
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用不在指定工作空间时
|
||
"""
|
||
logger.info("删除应用", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
self._validate_workspace_access(app, workspace_id)
|
||
|
||
# 逻辑删除应用
|
||
app.is_active = False
|
||
|
||
# 更新 app_shares 表中该应用的所有共享记录为失效状态,并更新 updated_at 时间
|
||
stmt = sa_update(AppShare).where(
|
||
AppShare.source_app_id == app_id,
|
||
AppShare.is_active.is_(True)
|
||
).values(
|
||
is_active=False,
|
||
updated_at=datetime.datetime.now()
|
||
)
|
||
self.db.execute(stmt)
|
||
|
||
self.db.commit()
|
||
|
||
logger.info(
|
||
"应用删除成功",
|
||
extra={
|
||
"app_id": str(app_id),
|
||
"app_name": app.name,
|
||
"app_type": app.type
|
||
}
|
||
)
|
||
|
||
def copy_app(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
user_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None,
|
||
new_name: Optional[str] = None
|
||
) -> App:
|
||
"""复制应用(包括基础信息和配置)
|
||
|
||
Args:
|
||
app_id: 源应用ID
|
||
user_id: 创建者用户ID
|
||
workspace_id: 目标工作空间ID(如果为None,则复制到源应用所在工作空间)
|
||
new_name: 新应用名称(如果为None,则使用"源应用名称 - 副本")
|
||
|
||
Returns:
|
||
App: 复制后的新应用对象
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当源应用不存在时
|
||
BusinessException: 当复制失败时
|
||
"""
|
||
logger.info("复制应用", extra={"source_app_id": str(app_id)})
|
||
|
||
try:
|
||
# 获取源应用
|
||
source_app = self._get_app_or_404(app_id)
|
||
self._validate_app_accessible(source_app, workspace_id)
|
||
|
||
# 确定目标工作空间
|
||
target_workspace_id = workspace_id or source_app.workspace_id
|
||
|
||
# 确定新应用名称
|
||
if not new_name:
|
||
new_name = f"{source_app.name} - 副本"
|
||
new_name = self._unique_app_name(new_name, target_workspace_id, source_app.type)
|
||
|
||
now = datetime.datetime.now()
|
||
|
||
# 创建新应用(复制基础信息)
|
||
new_app = App(
|
||
id=uuid.uuid4(),
|
||
workspace_id=target_workspace_id,
|
||
created_by=user_id,
|
||
name=new_name,
|
||
description=source_app.description,
|
||
icon=source_app.icon,
|
||
icon_type=source_app.icon_type,
|
||
type=source_app.type,
|
||
visibility=source_app.visibility,
|
||
status="draft", # 复制的应用默认为草稿状态
|
||
tags=source_app.tags.copy() if source_app.tags else [],
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(new_app)
|
||
self.db.flush()
|
||
|
||
# 判断是否跨工作空间复制(共享应用复制到自己的工作空间)
|
||
is_cross_workspace = target_workspace_id != source_app.workspace_id
|
||
|
||
# 跨工作空间时,获取目标工作空间的 tenant_id 用于判断模型配置是否可用
|
||
target_tenant_id = None
|
||
if is_cross_workspace:
|
||
target_ws = self.db.get(Workspace, target_workspace_id)
|
||
if not target_ws:
|
||
raise ResourceNotFoundException("工作空间", str(target_workspace_id))
|
||
target_tenant_id = target_ws.tenant_id
|
||
|
||
# 如果是 agent 类型,复制 AgentConfig
|
||
if source_app.type == AppType.AGENT:
|
||
source_config = self.db.query(AgentConfig).filter(
|
||
AgentConfig.app_id == source_app.id
|
||
).first()
|
||
|
||
if source_config:
|
||
if is_cross_workspace:
|
||
# 跨工作空间:model/tools/skills 属于 tenant 级别直接保留,
|
||
# knowledge_bases 属于 workspace 级别需过滤,memory_config 需清空
|
||
_, kb_ids = self._collect_resource_ids_from_config(
|
||
None, source_config.knowledge_retrieval
|
||
)
|
||
_, available_kb_ids = self._preload_cross_workspace_resources(
|
||
target_tenant_id, target_workspace_id, set(), kb_ids
|
||
)
|
||
new_model_config_id = source_config.default_model_config_id
|
||
new_knowledge_retrieval = self._clean_knowledge_retrieval(
|
||
source_config.knowledge_retrieval, available_kb_ids
|
||
)
|
||
new_tools = copy.deepcopy(source_config.tools) if source_config.tools else []
|
||
new_memory = self._clean_memory_cross_workspace(
|
||
source_config.memory, target_workspace_id
|
||
)
|
||
new_skills = copy.deepcopy(source_config.skills) if source_config.skills else {}
|
||
else:
|
||
new_model_config_id = source_config.default_model_config_id
|
||
new_knowledge_retrieval = copy.deepcopy(source_config.knowledge_retrieval) if source_config.knowledge_retrieval else None
|
||
new_tools = copy.deepcopy(source_config.tools) if source_config.tools else []
|
||
new_memory = copy.deepcopy(source_config.memory) if source_config.memory else None
|
||
new_skills = copy.deepcopy(source_config.skills) if source_config.skills else {}
|
||
|
||
new_config = AgentConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=new_app.id,
|
||
system_prompt=source_config.system_prompt,
|
||
default_model_config_id=new_model_config_id,
|
||
model_parameters=copy.deepcopy(source_config.model_parameters) if source_config.model_parameters else None,
|
||
knowledge_retrieval=new_knowledge_retrieval,
|
||
memory=new_memory,
|
||
variables=copy.deepcopy(source_config.variables) if source_config.variables else [],
|
||
tools=new_tools,
|
||
skills=new_skills,
|
||
features=copy.deepcopy(source_config.features) if source_config.features else {},
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(new_config)
|
||
|
||
elif source_app.type == AppType.WORKFLOW:
|
||
source_config = self.db.query(WorkflowConfig).filter(
|
||
WorkflowConfig.app_id == source_app.id
|
||
).first()
|
||
|
||
if source_config:
|
||
new_config = WorkflowConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=new_app.id,
|
||
nodes=copy.deepcopy(source_config.nodes) if source_config.nodes else [],
|
||
edges=copy.deepcopy(source_config.edges) if source_config.edges else [],
|
||
variables=copy.deepcopy(source_config.variables) if source_config.variables else [],
|
||
execution_config=copy.deepcopy(source_config.execution_config) if source_config.execution_config else {},
|
||
features=copy.deepcopy(source_config.features) if source_config.features else {},
|
||
triggers=copy.deepcopy(source_config.triggers) if source_config.triggers else [],
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(new_config)
|
||
|
||
elif source_app.type == AppType.MULTI_AGENT:
|
||
source_config = self.db.query(MultiAgentConfig).filter(
|
||
MultiAgentConfig.app_id == source_app.id
|
||
).first()
|
||
|
||
if source_config:
|
||
# multi_agent 的 model_config_id/sub_agents/routing_rules 均属于 tenant 级别直接保留
|
||
# 跨空间时 master_agent_id(AppRelease)属于源空间,需清空
|
||
new_config = MultiAgentConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=new_app.id,
|
||
master_agent_id=source_config.master_agent_id if not is_cross_workspace else None,
|
||
master_agent_name=source_config.master_agent_name,
|
||
default_model_config_id=source_config.default_model_config_id,
|
||
model_parameters=copy.deepcopy(source_config.model_parameters) if source_config.model_parameters else None,
|
||
orchestration_mode=source_config.orchestration_mode,
|
||
sub_agents=copy.deepcopy(source_config.sub_agents) if source_config.sub_agents else [],
|
||
routing_rules=copy.deepcopy(source_config.routing_rules) if source_config.routing_rules else None,
|
||
execution_config=copy.deepcopy(source_config.execution_config) if source_config.execution_config else {},
|
||
aggregation_strategy=source_config.aggregation_strategy,
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(new_config)
|
||
|
||
self.db.commit()
|
||
self.db.refresh(new_app)
|
||
|
||
logger.info(
|
||
"应用复制成功",
|
||
extra={
|
||
"source_app_id": str(app_id),
|
||
"new_app_id": str(new_app.id),
|
||
"new_app_name": new_app.name
|
||
}
|
||
)
|
||
|
||
return new_app
|
||
|
||
except Exception as e:
|
||
self.db.rollback()
|
||
logger.error(
|
||
"应用复制失败",
|
||
extra={"source_app_id": str(app_id), "error": str(e)}
|
||
)
|
||
raise BusinessException(f"应用复制失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
|
||
|
||
def _preload_cross_workspace_resources(
|
||
self,
|
||
target_tenant_id: Optional[uuid.UUID],
|
||
target_workspace_id: uuid.UUID,
|
||
model_config_ids: set,
|
||
kb_ids: set
|
||
) -> tuple:
|
||
"""Batch-load model configs and knowledge bases to avoid N+1 queries.
|
||
|
||
Returns:
|
||
(available_model_ids, available_kb_ids): sets of IDs available in target workspace
|
||
"""
|
||
from app.models.models_model import ModelConfig as MC
|
||
from app.models.knowledge_model import Knowledge
|
||
from app.models.knowledgeshare_model import KnowledgeShare
|
||
|
||
# Batch check model configs by tenant
|
||
available_model_ids: set = set()
|
||
if model_config_ids and target_tenant_id:
|
||
stmt = select(MC.id).where(
|
||
MC.id.in_(model_config_ids),
|
||
MC.tenant_id == target_tenant_id
|
||
)
|
||
available_model_ids = set(self.db.scalars(stmt).all())
|
||
|
||
# Batch check knowledge bases
|
||
available_kb_ids: set = set()
|
||
if kb_ids:
|
||
kb_uuids = set()
|
||
for kid in kb_ids:
|
||
try:
|
||
kb_uuids.add(uuid.UUID(str(kid)))
|
||
except (ValueError, AttributeError):
|
||
pass
|
||
|
||
if kb_uuids:
|
||
# KBs in target workspace
|
||
stmt = select(Knowledge.id).where(
|
||
Knowledge.id.in_(kb_uuids),
|
||
Knowledge.workspace_id == target_workspace_id
|
||
)
|
||
available_kb_ids.update(self.db.scalars(stmt).all())
|
||
|
||
# KBs shared to target workspace
|
||
remaining = kb_uuids - available_kb_ids
|
||
if remaining:
|
||
stmt = select(KnowledgeShare.source_kb_id).where(
|
||
KnowledgeShare.source_kb_id.in_(remaining),
|
||
KnowledgeShare.target_workspace_id == target_workspace_id
|
||
)
|
||
available_kb_ids.update(self.db.scalars(stmt).all())
|
||
|
||
return available_model_ids, available_kb_ids
|
||
|
||
@staticmethod
|
||
def _collect_resource_ids_from_config(
|
||
model_config_id: Optional[uuid.UUID],
|
||
knowledge_retrieval: Optional[dict]
|
||
) -> tuple:
|
||
"""Extract all model config IDs and knowledge base IDs from an app config."""
|
||
model_ids: set = set()
|
||
kb_ids: set = set()
|
||
|
||
if model_config_id:
|
||
model_ids.add(model_config_id)
|
||
|
||
if knowledge_retrieval and isinstance(knowledge_retrieval, dict):
|
||
if "knowledge_bases" in knowledge_retrieval:
|
||
for kid in knowledge_retrieval.get("knowledge_bases", []):
|
||
kb_ids.add(str(kid.get("kb_id")))
|
||
|
||
return model_ids, kb_ids
|
||
|
||
@staticmethod
|
||
def _is_kb_available(kb_id: Optional[str], available_kb_ids: set) -> Optional[str]:
|
||
if not kb_id:
|
||
return None
|
||
try:
|
||
return kb_id if uuid.UUID(str(kb_id)) in available_kb_ids else None
|
||
except (ValueError, AttributeError):
|
||
return None
|
||
|
||
def _clean_knowledge_retrieval(
|
||
self,
|
||
knowledge_retrieval: Optional[dict],
|
||
available_kb_ids: set
|
||
) -> Optional[dict]:
|
||
"""Clean knowledge retrieval config, keeping only available KBs."""
|
||
if not knowledge_retrieval:
|
||
return None
|
||
|
||
cleaned = copy.deepcopy(knowledge_retrieval)
|
||
|
||
if "knowledge_bases" in cleaned and isinstance(cleaned["knowledge_bases"], list):
|
||
cleaned["knowledge_bases"] = [
|
||
kb for kb in cleaned["knowledge_bases"]
|
||
if self._is_kb_available(kb.get("kb_id"), available_kb_ids)
|
||
]
|
||
|
||
return cleaned
|
||
|
||
def _clean_memory_cross_workspace(
|
||
self,
|
||
memory: Optional[dict],
|
||
target_workspace_id: uuid.UUID
|
||
) -> Optional[dict]:
|
||
"""Clear memory_config_id/memory_content if it doesn't belong to target workspace."""
|
||
if not memory:
|
||
return None
|
||
|
||
from app.models.memory_config_model import MemoryConfig
|
||
|
||
cleaned = copy.deepcopy(memory)
|
||
# 兼容旧字段 memory_content 和新字段 memory_config_id
|
||
mid = cleaned.get("memory_config_id") or cleaned.get("memory_content")
|
||
if mid:
|
||
try:
|
||
mid_uuid = uuid.UUID(str(mid))
|
||
except (ValueError, AttributeError):
|
||
exists = self.db.query(MemoryConfig).filter(
|
||
MemoryConfig.config_id_old == int(mid),
|
||
MemoryConfig.workspace_id == target_workspace_id
|
||
).first()
|
||
if not exists:
|
||
cleaned["memory_config_id"] = None
|
||
cleaned.pop("memory_content", None)
|
||
return cleaned
|
||
|
||
exists = self.db.query(
|
||
self.db.query(MemoryConfig).filter(
|
||
MemoryConfig.config_id == mid_uuid,
|
||
MemoryConfig.workspace_id == target_workspace_id
|
||
).exists()
|
||
).scalar()
|
||
if not exists:
|
||
cleaned["memory_config_id"] = None
|
||
cleaned.pop("memory_content", None)
|
||
|
||
return cleaned
|
||
|
||
def list_apps(
|
||
self,
|
||
*,
|
||
workspace_id: uuid.UUID,
|
||
type: Optional[str] = None,
|
||
visibility: Optional[str] = None,
|
||
status: Optional[str] = None,
|
||
search: Optional[str] = None,
|
||
include_shared: bool = True,
|
||
shared_only: bool = False,
|
||
page: int = 1,
|
||
pagesize: int = 10,
|
||
) -> Tuple[List[App], int]:
|
||
"""列出工作空间中的应用(分页)
|
||
|
||
包括:
|
||
1. 本工作空间创建的应用
|
||
2. 其他工作空间分享给本工作空间的应用(如果 include_shared=True)
|
||
|
||
Args:
|
||
workspace_id: 工作空间ID
|
||
type: 应用类型过滤
|
||
visibility: 可见性过滤
|
||
status: 状态过滤
|
||
search: 搜索关键词
|
||
include_shared: 是否包含分享的应用
|
||
page: 页码(从1开始)
|
||
pagesize: 每页数量
|
||
|
||
Returns:
|
||
Tuple[List[App], int]: (应用列表, 总数)
|
||
"""
|
||
from app.models import AppShare
|
||
|
||
logger.debug(
|
||
"查询应用列表",
|
||
extra={
|
||
"workspace_id": str(workspace_id),
|
||
"include_shared": include_shared,
|
||
"page": page,
|
||
"pagesize": pagesize
|
||
}
|
||
)
|
||
|
||
# 构建查询条件
|
||
filters = [App.is_active.is_(True)]
|
||
if type:
|
||
filters.append(App.type == type)
|
||
if visibility:
|
||
filters.append(App.visibility == visibility)
|
||
if status:
|
||
filters.append(App.status == status)
|
||
if search:
|
||
filters.append(func.lower(App.name).like(f"%{search.lower()}%"))
|
||
|
||
# shared_only implies include_shared; enforce to avoid confusing API usage
|
||
if shared_only:
|
||
include_shared = True
|
||
|
||
# 基础查询:本工作空间的应用
|
||
if shared_only:
|
||
# 只返回共享给本工作空间的应用,不含自有应用
|
||
shared_app_ids_stmt = (
|
||
select(AppShare.source_app_id)
|
||
.where(AppShare.target_workspace_id == workspace_id, AppShare.is_active.is_(True))
|
||
)
|
||
stmt = select(App).where(App.id.in_(shared_app_ids_stmt))
|
||
elif include_shared:
|
||
# 查询本工作空间的应用 + 分享给本工作空间的应用
|
||
shared_app_ids_stmt = (
|
||
select(AppShare.source_app_id)
|
||
.where(AppShare.target_workspace_id == workspace_id, AppShare.is_active.is_(True))
|
||
)
|
||
stmt = select(App).where(
|
||
or_(
|
||
App.workspace_id == workspace_id,
|
||
App.id.in_(shared_app_ids_stmt)
|
||
)
|
||
)
|
||
else:
|
||
# 只查询本工作空间的应用
|
||
stmt = select(App).where(App.workspace_id == workspace_id)
|
||
|
||
# 应用过滤条件
|
||
if filters:
|
||
stmt = stmt.where(and_(*filters))
|
||
|
||
# 计算总数
|
||
total_stmt = select(func.count()).select_from(stmt.subquery())
|
||
total = self.db.execute(total_stmt).scalar() or 0
|
||
|
||
# 分页
|
||
offset = (page - 1) * pagesize
|
||
stmt = stmt.order_by(App.created_at.desc()).offset(offset).limit(pagesize)
|
||
|
||
items = list(self.db.scalars(stmt).all())
|
||
|
||
logger.debug(
|
||
"应用列表查询完成",
|
||
extra={"total": total, "returned": len(items), "include_shared": include_shared}
|
||
)
|
||
return items, int(total)
|
||
|
||
def get_apps_by_ids(
|
||
self,
|
||
app_ids: List[str],
|
||
workspace_id: uuid.UUID
|
||
) -> List[App]:
|
||
"""根据ID列表获取应用
|
||
|
||
Args:
|
||
app_ids: 应用ID列表
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
List[App]: 应用列表
|
||
"""
|
||
if not app_ids:
|
||
return []
|
||
|
||
# 转换字符串ID为UUID
|
||
try:
|
||
uuid_ids = [uuid.UUID(app_id) for app_id in app_ids]
|
||
except ValueError:
|
||
return []
|
||
|
||
# 查询本工作空间的应用 + 分享给本工作空间的应用
|
||
stmt = select(App).where(
|
||
App.id.in_(uuid_ids),
|
||
App.workspace_id == workspace_id
|
||
)
|
||
|
||
return list(self.db.scalars(stmt).all())
|
||
|
||
# ==================== Agent 配置管理 ====================
|
||
|
||
def update_agent_config(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
data: app_schema.AgentConfigUpdate,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> AgentConfig:
|
||
"""更新 Agent 配置
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
data: 配置更新数据
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
AgentConfig: 更新后的配置对象
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用类型不支持或不在指定工作空间时
|
||
"""
|
||
logger.info("更新 Agent 配置", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
|
||
if app.type != "agent":
|
||
raise BusinessException("只有 Agent 类型应用支持 Agent 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||
|
||
self._validate_app_writable(app, workspace_id)
|
||
|
||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active.is_(True)).order_by(
|
||
AgentConfig.updated_at.desc())
|
||
agent_cfg: Optional[AgentConfig] = self.db.scalars(stmt).first()
|
||
now = datetime.datetime.now()
|
||
|
||
if not agent_cfg:
|
||
agent_cfg = AgentConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=app_id,
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(agent_cfg)
|
||
logger.debug("创建新的 Agent 配置", extra={"app_id": str(app_id)})
|
||
|
||
# 转换为存储格式
|
||
storage_data = AgentConfigConverter.to_storage_format(data)
|
||
|
||
# 更新字段
|
||
# if data.system_prompt is not None:
|
||
agent_cfg.system_prompt = data.system_prompt
|
||
# if data.default_model_config_id is not None:
|
||
agent_cfg.default_model_config_id = data.default_model_config_id
|
||
# if data.model_parameters is not None:
|
||
agent_cfg.model_parameters = storage_data.get("model_parameters")
|
||
# if data.knowledge_retrieval is not None:
|
||
agent_cfg.knowledge_retrieval = storage_data.get("knowledge_retrieval")
|
||
# if data.memory is not None:
|
||
agent_cfg.memory = storage_data.get("memory")
|
||
# if data.variables is not None:
|
||
agent_cfg.variables = storage_data.get("variables", [])
|
||
# if data.tools is not None:
|
||
agent_cfg.tools = storage_data.get("tools", [])
|
||
agent_cfg.skills = storage_data.get("skills", {})
|
||
agent_cfg.features = storage_data.get("features", {})
|
||
|
||
agent_cfg.updated_at = now
|
||
|
||
self.db.commit()
|
||
self.db.refresh(agent_cfg)
|
||
|
||
logger.info("Agent 配置更新成功", extra={"app_id": str(app_id)})
|
||
return agent_cfg
|
||
|
||
def _agent_config_from_release(self, release: "AppRelease") -> "AgentConfig":
|
||
"""从发布版本快照重建 AgentConfig 对象(不入库,仅用于运行)"""
|
||
cfg = release.config or {}
|
||
now = release.created_at or datetime.datetime.now()
|
||
agent_cfg = AgentConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=release.app_id,
|
||
system_prompt=cfg.get("system_prompt", ""),
|
||
default_model_config_id=release.default_model_config_id,
|
||
model_parameters=cfg.get("model_parameters"),
|
||
knowledge_retrieval=cfg.get("knowledge_retrieval"),
|
||
memory=cfg.get("memory", {}),
|
||
variables=cfg.get("variables", []),
|
||
tools=cfg.get("tools", []),
|
||
skills=cfg.get("skills", {}),
|
||
features=cfg.get("features", {}),
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
return agent_cfg
|
||
|
||
def _workflow_config_from_release(self, release: "AppRelease") -> "WorkflowConfig":
|
||
"""从发布版本快照重建 WorkflowConfig 对象(不入库,仅用于运行)"""
|
||
cfg = release.config or {}
|
||
now = release.created_at or datetime.datetime.now()
|
||
from app.models.workflow_model import WorkflowConfig as WorkflowConfigModel
|
||
# 查出源应用真实的 WorkflowConfig id,供 workflow_executions 外键使用
|
||
real_config = WorkflowConfigRepository(self.db).get_by_app_id(release.app_id)
|
||
real_id = real_config.id if real_config else uuid.uuid4()
|
||
wf_cfg = WorkflowConfigModel(
|
||
id=real_id,
|
||
app_id=release.app_id,
|
||
nodes=cfg.get("nodes", []),
|
||
edges=cfg.get("edges", []),
|
||
variables=cfg.get("variables", []),
|
||
execution_config=cfg.get("execution_config", {}),
|
||
triggers=cfg.get("triggers", []),
|
||
features=cfg.get("features", {}),
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
return wf_cfg
|
||
|
||
def get_agent_config(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> AgentConfig:
|
||
"""获取 Agent 配置
|
||
|
||
如果配置不存在,返回默认配置模板(不保存到数据库)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
AgentConfig: Agent 配置对象(存在的配置或默认模板)
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用类型不支持或不可访问时
|
||
"""
|
||
logger.debug("获取 Agent 配置", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
|
||
if app.type != "agent":
|
||
raise BusinessException("只有 Agent 类型应用支持 Agent 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||
|
||
# 只读操作,允许访问共享应用
|
||
self._validate_app_accessible(app, workspace_id)
|
||
|
||
# 共享应用:返回最新发布版本的配置快照,而非草稿
|
||
if workspace_id and app.workspace_id != workspace_id:
|
||
if not app.current_release_id:
|
||
raise BusinessException("该应用尚未发布,无法使用", BizCode.AGENT_CONFIG_MISSING)
|
||
release = self.db.get(AppRelease, app.current_release_id)
|
||
if not release:
|
||
raise BusinessException("发布版本不存在", BizCode.AGENT_CONFIG_MISSING)
|
||
return self._agent_config_from_release(release)
|
||
|
||
stmt = select(AgentConfig).where(
|
||
AgentConfig.app_id == app_id,
|
||
AgentConfig.is_active.is_(True)
|
||
).order_by(
|
||
AgentConfig.updated_at.desc()
|
||
)
|
||
|
||
config = self.db.scalars(stmt).first()
|
||
|
||
try:
|
||
config_memory = config.memory
|
||
if 'memory_content' in config_memory:
|
||
config.memory['memory_config_id'] = config.memory.pop('memory_content')
|
||
except:
|
||
logger.debug("记忆配置不存在")
|
||
if config:
|
||
return config
|
||
|
||
# 返回默认配置模板(不保存到数据库)
|
||
logger.debug("配置不存在,返回默认模板", extra={"app_id": str(app_id)})
|
||
return self._create_default_agent_config(app_id)
|
||
|
||
def get_default_model_parameters(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
) -> "ModelParameters":
|
||
"""获取 Agent 默认模型参数(不修改数据库)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
|
||
Returns:
|
||
ModelParameters: 默认模型参数
|
||
"""
|
||
logger.info("获取 Agent 默认模型参数", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
|
||
if app.type != "agent":
|
||
raise BusinessException("只有 Agent 类型应用支持 Agent 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||
|
||
from app.schemas.app_schema import ModelParameters
|
||
default_model_parameters = ModelParameters()
|
||
|
||
logger.info("获取 Agent 默认模型参数成功", extra={"app_id": str(app_id)})
|
||
return default_model_parameters
|
||
|
||
def reset_agent_config(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> "ModelParameters":
|
||
"""将 Agent 模型参数重置为默认值(不影响其他配置)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
ModelParameters: 重置后的模型参数
|
||
"""
|
||
logger.info("重置 Agent 模型参数为默认值", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
|
||
if app.type != "agent":
|
||
raise BusinessException("只有 Agent 类型应用支持 Agent 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||
|
||
self._validate_app_writable(app, workspace_id)
|
||
|
||
from app.schemas.app_schema import ModelParameters
|
||
default_model_parameters = ModelParameters()
|
||
|
||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active.is_(True)).order_by(
|
||
AgentConfig.updated_at.desc())
|
||
agent_cfg: Optional[AgentConfig] = self.db.scalars(stmt).first()
|
||
now = datetime.datetime.now()
|
||
|
||
if agent_cfg:
|
||
agent_cfg.default_model_config_id = None
|
||
agent_cfg.model_parameters = default_model_parameters
|
||
agent_cfg.updated_at = now
|
||
else:
|
||
agent_cfg = AgentConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=app_id,
|
||
default_model_config_id=None,
|
||
model_parameters=default_model_parameters,
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(agent_cfg)
|
||
|
||
self.db.commit()
|
||
|
||
logger.info("Agent 模型参数重置成功", extra={"app_id": str(app_id)})
|
||
return default_model_parameters
|
||
|
||
def _create_default_agent_config(self, app_id: uuid.UUID) -> AgentConfig:
|
||
"""创建默认的 Agent 配置模板(不保存到数据库)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
|
||
Returns:
|
||
AgentConfig: 默认配置对象
|
||
"""
|
||
now = datetime.datetime.now()
|
||
|
||
# 创建一个临时的配置对象,不添加到数据库
|
||
default_config = AgentConfig(
|
||
id=uuid.uuid4(), # 临时ID
|
||
app_id=app_id,
|
||
system_prompt="你是一个专业的AI助手,你的职责是帮助用户解决问题。",
|
||
default_model_config_id=None,
|
||
model_parameters={
|
||
"temperature": 0.7,
|
||
"max_tokens": 2000,
|
||
"top_p": 1.0,
|
||
"frequency_penalty": 0.0,
|
||
"presence_penalty": 0.0,
|
||
"n": 1,
|
||
"stop": None
|
||
},
|
||
knowledge_retrieval={
|
||
"knowledge_bases": [],
|
||
"merge_strategy": "weighted"
|
||
},
|
||
memory={
|
||
"enabled": True,
|
||
"memory_config_id": None,
|
||
"max_history": 10
|
||
},
|
||
variables=[],
|
||
tools=[],
|
||
skills=[],
|
||
features={},
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
|
||
return default_config
|
||
|
||
def get_workflow_config(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> WorkflowConfig:
|
||
"""获取 workflow 配置
|
||
|
||
如果配置不存在,返回默认配置模板(不保存到数据库)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
WorkflowConfig: Workflow 配置对象(存在的配置或默认模板)
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用类型不支持或不可访问时
|
||
"""
|
||
logger.debug("获取 Workflow 配置", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
|
||
if app.type != AppType.WORKFLOW:
|
||
raise BusinessException("只有 Workflow 类型应用支持 Workflow 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||
|
||
# 只读操作,允许访问共享应用
|
||
self._validate_app_accessible(app, workspace_id)
|
||
|
||
# 共享应用:返回最新发布版本的配置快照,而非草稿
|
||
if workspace_id and app.workspace_id != workspace_id:
|
||
if not app.current_release_id:
|
||
raise BusinessException("该应用尚未发布,无法使用", BizCode.CONFIG_MISSING)
|
||
release = self.db.get(AppRelease, app.current_release_id)
|
||
if not release:
|
||
raise BusinessException("发布版本不存在", BizCode.CONFIG_MISSING)
|
||
return self._workflow_config_from_release(release)
|
||
|
||
repo = WorkflowConfigRepository(self.db)
|
||
config = repo.get_by_app_id(app_id)
|
||
if config:
|
||
return config
|
||
|
||
# 返回默认配置模板(不保存到数据库)
|
||
logger.debug("配置不存在,返回默认模板", extra={"app_id": str(app_id)})
|
||
return self._create_default_workflow_config(app_id)
|
||
|
||
def update_workflow_config(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
data: WorkflowConfigUpdate,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> WorkflowConfig:
|
||
"""更新 Workflow 配置(全量更新)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
data: 配置更新数据(全量数据)
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
WorkflowConfig: 更新后的配置对象
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用类型不支持或不在指定工作空间时
|
||
"""
|
||
logger.info("更新 Workflow 配置", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
|
||
if app.type != AppType.WORKFLOW:
|
||
raise BusinessException("只有 Workflow 类型应用支持 Workflow 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||
|
||
self._validate_app_writable(app, workspace_id)
|
||
|
||
# 获取现有配置
|
||
repo = WorkflowConfigRepository(self.db)
|
||
workflow_cfg = repo.get_by_app_id(app_id)
|
||
now = datetime.datetime.now()
|
||
|
||
if not workflow_cfg:
|
||
# 如果配置不存在,创建新配置
|
||
workflow_cfg = WorkflowConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=app_id,
|
||
nodes=[node.model_dump() for node in data.nodes] if data.nodes else [],
|
||
edges=[edge.model_dump() for edge in data.edges] if data.edges else [],
|
||
variables=[var.model_dump() for var in data.variables] if data.variables else [],
|
||
execution_config=data.execution_config.model_dump() if data.execution_config else {},
|
||
triggers=[trigger.model_dump() for trigger in data.triggers] if data.triggers else [],
|
||
features=data.features or {},
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
self.db.add(workflow_cfg)
|
||
logger.debug("创建新的 Workflow 配置", extra={"app_id": str(app_id)})
|
||
else:
|
||
# 全量更新现有配置
|
||
workflow_cfg.nodes = [node.model_dump() for node in data.nodes] if data.nodes else []
|
||
workflow_cfg.edges = [edge.model_dump() for edge in data.edges] if data.edges else []
|
||
workflow_cfg.variables = [var.model_dump() for var in data.variables] if data.variables else []
|
||
workflow_cfg.execution_config = data.execution_config.model_dump() if data.execution_config else {}
|
||
workflow_cfg.triggers = [trigger.model_dump() for trigger in data.triggers] if data.triggers else []
|
||
workflow_cfg.features = data.features or {}
|
||
workflow_cfg.updated_at = now
|
||
|
||
self.db.commit()
|
||
self.db.refresh(workflow_cfg)
|
||
|
||
logger.info("Workflow 配置更新成功", extra={"app_id": str(app_id)})
|
||
return workflow_cfg
|
||
|
||
def _create_default_workflow_config(self, app_id: uuid.UUID) -> WorkflowConfig:
|
||
"""创建默认的 workflow 配置模板(不保存到数据库)
|
||
|
||
使用 template_loader 加载 simple_qa 模板作为默认配置
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
|
||
Returns:
|
||
WorkflowConfig: 默认配置对象
|
||
"""
|
||
from app.core.workflow.template_loader import load_workflow_template
|
||
|
||
now = datetime.datetime.now()
|
||
|
||
# 使用 template_loader 加载 simple_qa 模板
|
||
template_data = load_workflow_template('simple_qa')
|
||
|
||
if not template_data:
|
||
# 如果模板加载失败,返回最小化配置
|
||
logger.warning(
|
||
"无法加载默认工作流模板,使用最小化配置",
|
||
extra={"app_id": str(app_id)}
|
||
)
|
||
template_data = {
|
||
'nodes': [
|
||
{'id': 'start', 'type': 'start', 'name': '开始'},
|
||
{'id': 'end', 'type': 'end', 'name': '结束'}
|
||
],
|
||
'edges': [
|
||
{'source': 'start', 'target': 'end'}
|
||
],
|
||
'variables': [],
|
||
'execution_config': {
|
||
'max_execution_time': 300,
|
||
'max_iterations': 10
|
||
},
|
||
'triggers': []
|
||
}
|
||
|
||
# 转换为 WorkflowConfig 格式
|
||
default_config = WorkflowConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=app_id,
|
||
nodes=template_data.get('nodes', []),
|
||
edges=template_data.get('edges', []),
|
||
variables=template_data.get('variables', []),
|
||
execution_config=template_data.get('execution_config', {}),
|
||
triggers=template_data.get('triggers', []),
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
|
||
return default_config
|
||
|
||
# ==================== 记忆配置提取方法 ====================
|
||
|
||
def _get_memory_config_id_from_release(
|
||
self,
|
||
app_type: str,
|
||
config: Dict[str, Any]
|
||
) -> Tuple[Optional[uuid.UUID], bool]:
|
||
"""从发布配置中提取 memory_config_id(委托给 MemoryConfigService)
|
||
|
||
Args:
|
||
app_type: 应用类型 (agent, workflow, multi_agent)
|
||
config: 发布配置字典
|
||
|
||
Returns:
|
||
Tuple[Optional[uuid.UUID], bool]: (memory_config_id, is_legacy_int)
|
||
- memory_config_id: 提取的配置ID,如果不存在或为旧格式则返回 None
|
||
- is_legacy_int: 是否检测到旧格式 int 数据,需要回退到工作空间默认配置
|
||
"""
|
||
from app.services.memory_config_service import MemoryConfigService
|
||
|
||
service = MemoryConfigService(self.db)
|
||
return service.extract_memory_config_id(app_type, config)
|
||
|
||
def _get_workspace_default_memory_config_id(
|
||
self,
|
||
workspace_id: uuid.UUID
|
||
) -> Optional[uuid.UUID]:
|
||
"""获取工作空间的默认记忆配置ID
|
||
|
||
Args:
|
||
workspace_id: 工作空间ID
|
||
|
||
Returns:
|
||
Optional[uuid.UUID]: 默认记忆配置ID,如果不存在则返回 None
|
||
"""
|
||
from app.services.memory_config_service import MemoryConfigService
|
||
|
||
service = MemoryConfigService(self.db)
|
||
config = service.get_workspace_default_config(workspace_id)
|
||
|
||
if not config:
|
||
logger.warning(
|
||
f"工作空间没有可用的记忆配置: workspace_id={workspace_id}"
|
||
)
|
||
return None
|
||
|
||
return config.config_id
|
||
|
||
def _update_endusers_memory_config_by_app(
|
||
self,
|
||
app_id: uuid.UUID,
|
||
memory_config_id: uuid.UUID
|
||
) -> int:
|
||
"""批量更新应用下所有终端用户的 memory_config_id
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
memory_config_id: 新的记忆配置ID
|
||
|
||
Returns:
|
||
int: 更新的终端用户数量
|
||
"""
|
||
from app.repositories.end_user_repository import EndUserRepository
|
||
|
||
repo = EndUserRepository(self.db)
|
||
updated_count = repo.batch_update_memory_config_id_by_app(
|
||
app_id=app_id,
|
||
memory_config_id=memory_config_id
|
||
)
|
||
|
||
return updated_count
|
||
|
||
# ==================== 应用发布管理 ====================
|
||
|
||
def publish(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
publisher_id: uuid.UUID,
|
||
version_name: str,
|
||
workspace_id: Optional[uuid.UUID] = None,
|
||
release_notes: Optional[str] = None
|
||
) -> AppRelease:
|
||
"""发布应用(创建不可变快照)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
publisher_id: 发布者用户ID
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
release_notes: 版本说明
|
||
|
||
Returns:
|
||
AppRelease: 发布版本对象
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用缺少配置或不在指定工作空间时
|
||
"""
|
||
logger.info("发布应用", extra={"app_id": str(app_id), "publisher_id": str(publisher_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
# 检查应用归属
|
||
self._validate_workspace_access(app, workspace_id)
|
||
|
||
# 构建快照配置
|
||
config: Dict[str, Any] = {}
|
||
default_model_config_id = None
|
||
|
||
if app.type == AppType.AGENT:
|
||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active.is_(True)).order_by(
|
||
AgentConfig.updated_at.desc())
|
||
agent_cfg = self.db.scalars(stmt).first()
|
||
if not agent_cfg:
|
||
raise BusinessException("Agent 应用缺少配置,无法发布", BizCode.AGENT_CONFIG_MISSING)
|
||
|
||
miss_params = []
|
||
if agent_cfg.default_model_config_id is None:
|
||
miss_params.append("模型配置")
|
||
|
||
if agent_cfg.memory.get("enabled") and not agent_cfg.memory.get("memory_config_id"):
|
||
miss_params.append("记忆配置")
|
||
if miss_params:
|
||
raise BusinessException(
|
||
f"应用发布失败:检测到以下必要配置尚未完成:{', '.join(miss_params)}。请返回应用编辑页面完成相关配置后再尝试发布。",
|
||
BizCode.CONFIG_MISSING,
|
||
context={"missing_params": miss_params},
|
||
)
|
||
|
||
config = {
|
||
"system_prompt": agent_cfg.system_prompt,
|
||
"model_parameters": model_parameters_to_dict(agent_cfg.model_parameters),
|
||
"knowledge_retrieval": agent_cfg.knowledge_retrieval,
|
||
"memory": agent_cfg.memory,
|
||
"variables": agent_cfg.variables or [],
|
||
"tools": agent_cfg.tools or [],
|
||
"skills": agent_cfg.skills or {},
|
||
"features": agent_cfg.features or {}
|
||
}
|
||
# config = AgentConfigConverter.from_storage_format(agent_cfg)
|
||
default_model_config_id = agent_cfg.default_model_config_id
|
||
elif app.type == AppType.MULTI_AGENT:
|
||
# 1. 获取多智能体配置
|
||
stmt = (
|
||
select(MultiAgentConfig)
|
||
.where(
|
||
MultiAgentConfig.app_id == app_id,
|
||
MultiAgentConfig.is_active.is_(True)
|
||
)
|
||
.order_by(MultiAgentConfig.updated_at.desc())
|
||
)
|
||
multi_agent_cfg = self.db.scalars(stmt).first()
|
||
if not multi_agent_cfg:
|
||
raise BusinessException("多 Agent 应用缺少有效配置,无法发布", BizCode.AGENT_CONFIG_MISSING)
|
||
|
||
# 2. 检查配置完整性
|
||
self._check_multi_agent_config(app_id)
|
||
|
||
# 3. 获取主 Agent 的模型配置 ID
|
||
default_model_config_id = multi_agent_cfg.default_model_config_id
|
||
|
||
# 4. 构建配置快照
|
||
|
||
config = {
|
||
"model_parameters": model_parameters_to_dict(multi_agent_cfg.model_parameters),
|
||
"master_agent_id": str(multi_agent_cfg.master_agent_id),
|
||
"orchestration_mode": multi_agent_cfg.orchestration_mode,
|
||
"sub_agents": multi_agent_cfg.sub_agents,
|
||
"routing_rules": multi_agent_cfg.routing_rules,
|
||
"execution_config": multi_agent_cfg.execution_config,
|
||
"aggregation_strategy": multi_agent_cfg.aggregation_strategy,
|
||
}
|
||
|
||
logger.info(
|
||
"多智能体应用发布配置准备完成",
|
||
extra={
|
||
"app_id": str(app_id),
|
||
"default_model_config_id": str(default_model_config_id),
|
||
"sub_agent_count": len(multi_agent_cfg.sub_agents) if multi_agent_cfg.sub_agents else 0,
|
||
"orchestration_mode": multi_agent_cfg.orchestration_mode
|
||
}
|
||
)
|
||
elif app.type == AppType.WORKFLOW:
|
||
service = WorkflowService(self.db)
|
||
workflow_cfg = service.get_workflow_config(app_id)
|
||
if not workflow_cfg:
|
||
raise BusinessException("应用缺少有效配置,无法发布", BizCode.CONFIG_MISSING)
|
||
|
||
config = {
|
||
"id": str(workflow_cfg.id),
|
||
"nodes": workflow_cfg.nodes,
|
||
"edges": workflow_cfg.edges,
|
||
"variables": workflow_cfg.variables,
|
||
"execution_config": workflow_cfg.execution_config,
|
||
"triggers": workflow_cfg.triggers,
|
||
"features": workflow_cfg.features or {}
|
||
}
|
||
|
||
is_valid, errors = WorkflowValidator.validate_for_publish(config)
|
||
if not is_valid:
|
||
raise BusinessException(f"应用缺少有效配置,无法发布, errors:{','.join(errors)}", BizCode.CONFIG_MISSING)
|
||
logger.info(
|
||
"应用发布配置准备完成"
|
||
)
|
||
|
||
now = datetime.datetime.now()
|
||
version = self._get_next_version(app_id)
|
||
|
||
release = AppRelease(
|
||
id=uuid.uuid4(),
|
||
app_id=app_id,
|
||
version=version,
|
||
version_name=version_name,
|
||
release_notes=release_notes,
|
||
name=app.name,
|
||
description=app.description,
|
||
icon=app.icon,
|
||
icon_type=app.icon_type,
|
||
type=app.type,
|
||
visibility=app.visibility,
|
||
config=config,
|
||
default_model_config_id=default_model_config_id,
|
||
published_by=publisher_id,
|
||
published_at=now,
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(release)
|
||
self.db.flush() # 先 flush,确保 release 已插入数据库
|
||
|
||
# 提取记忆配置ID并更新终端用户
|
||
memory_config_id, is_legacy_int = self._get_memory_config_id_from_release(app.type, config)
|
||
|
||
# 如果检测到旧格式 int 数据,回退到工作空间默认配置
|
||
if is_legacy_int and not memory_config_id:
|
||
memory_config_id = self._get_workspace_default_memory_config_id(app.workspace_id)
|
||
if memory_config_id:
|
||
logger.info(
|
||
f"发布时使用工作空间默认记忆配置(旧数据兼容): app_id={app_id}, "
|
||
f"workspace_id={app.workspace_id}, memory_config_id={memory_config_id}"
|
||
)
|
||
|
||
if memory_config_id:
|
||
app = self.db.query(App).filter(App.id == app_id).first()
|
||
if app:
|
||
updated_count = self._update_endusers_memory_config_by_app(
|
||
app_id, memory_config_id
|
||
)
|
||
logger.info(
|
||
f"发布时更新终端用户记忆配置: app_id={app_id}, workspace_id={app.workspace_id}, "
|
||
f"memory_config_id={memory_config_id}, updated_count={updated_count}"
|
||
)
|
||
|
||
# 更新当前发布版本指针
|
||
app.current_release_id = release.id
|
||
app.status = AppStatus.ACTIVE
|
||
app.updated_at = now
|
||
|
||
self.db.commit()
|
||
self.db.refresh(release)
|
||
|
||
logger.info(
|
||
"应用发布成功",
|
||
extra={"app_id": str(app_id), "version": version, "release_id": str(release.id)}
|
||
)
|
||
return release
|
||
|
||
def get_current_release(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> Optional[AppRelease]:
|
||
"""获取当前发布版本
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
Optional[AppRelease]: 当前发布版本,如果未发布则返回 None
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用不可访问时
|
||
"""
|
||
logger.debug("获取当前发布版本", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
# 只读操作,允许访问共享应用
|
||
self._validate_app_accessible(app, workspace_id)
|
||
|
||
if not app.current_release_id:
|
||
return None
|
||
|
||
return self.db.get(AppRelease, app.current_release_id)
|
||
|
||
def list_releases(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> List[AppRelease]:
|
||
"""列出应用的所有发布版本(倒序)
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
List[AppRelease]: 发布版本列表
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用不可访问时
|
||
"""
|
||
logger.debug("列出发布版本", extra={"app_id": str(app_id)})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
# 只读操作,允许访问共享应用
|
||
self._validate_app_accessible(app, workspace_id)
|
||
|
||
stmt = (
|
||
select(AppRelease)
|
||
.where(AppRelease.app_id == app_id, AppRelease.is_active.is_(True))
|
||
.order_by(AppRelease.version.desc())
|
||
)
|
||
return list(self.db.scalars(stmt).all())
|
||
|
||
def rollback(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
version: int,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> AppRelease:
|
||
"""回滚到指定版本
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
version: 目标版本号
|
||
workspace_id: 工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
AppRelease: 回滚到的版本对象
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用或版本不存在时
|
||
BusinessException: 当应用不在指定工作空间时
|
||
"""
|
||
logger.info("回滚应用", extra={"app_id": str(app_id), "version": version})
|
||
|
||
app = self._get_app_or_404(app_id)
|
||
self._validate_app_accessible(app, workspace_id)
|
||
|
||
stmt = select(AppRelease).where(
|
||
AppRelease.app_id == app_id,
|
||
AppRelease.version == version
|
||
)
|
||
release = self.db.scalars(stmt).first()
|
||
|
||
if not release:
|
||
logger.warning(
|
||
"发布版本不存在",
|
||
extra={"app_id": str(app_id), "version": version}
|
||
)
|
||
raise ResourceNotFoundException("发布版本", f"app_id={app_id}, version={version}")
|
||
|
||
# 提取记忆配置ID并更新终端用户
|
||
memory_config_id, is_legacy_int = self._get_memory_config_id_from_release(release.type, release.config)
|
||
|
||
# 如果检测到旧格式 int 数据,回退到工作空间默认配置
|
||
if is_legacy_int and not memory_config_id:
|
||
memory_config_id = self._get_workspace_default_memory_config_id(app.workspace_id)
|
||
if memory_config_id:
|
||
logger.info(
|
||
f"回滚时使用工作空间默认记忆配置(旧数据兼容): app_id={app_id}, "
|
||
f"workspace_id={app.workspace_id}, memory_config_id={memory_config_id}"
|
||
)
|
||
|
||
if memory_config_id:
|
||
|
||
updated_count = self._update_endusers_memory_config_by_app(app_id, memory_config_id)
|
||
logger.info(
|
||
f"回滚时更新终端用户记忆配置: app_id={app_id}, version={version}, "
|
||
f"memory_config_id={memory_config_id}, updated_count={updated_count}"
|
||
)
|
||
|
||
app.current_release_id = release.id
|
||
app.updated_at = datetime.datetime.now()
|
||
|
||
self.db.commit()
|
||
self.db.refresh(release)
|
||
|
||
logger.info(
|
||
"应用回滚成功",
|
||
extra={"app_id": str(app_id), "version": version, "release_id": str(release.id)}
|
||
)
|
||
return release
|
||
|
||
# ==================== 应用分享功能 ====================
|
||
|
||
def share_app(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
target_workspace_ids: List[uuid.UUID],
|
||
user_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None,
|
||
permission: str = "readonly"
|
||
) -> list[AppShare]:
|
||
"""分享应用到其他工作空间
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
target_workspace_ids: 目标工作空间ID列表
|
||
user_id: 分享者用户ID
|
||
workspace_id: 当前工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
List[AppShare]: 创建的分享记录列表
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用不在指定工作空间或目标工作空间无效时
|
||
"""
|
||
|
||
logger.info(
|
||
"分享应用",
|
||
extra={
|
||
"app_id": str(app_id),
|
||
"target_workspaces": [str(wid) for wid in target_workspace_ids],
|
||
"user_id": str(user_id)
|
||
}
|
||
)
|
||
|
||
# 1. 验证应用
|
||
app = self._get_app_or_404(app_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. 验证目标工作空间
|
||
for target_ws_id in target_workspace_ids:
|
||
target_ws = self.db.get(Workspace, target_ws_id)
|
||
if not target_ws:
|
||
raise ResourceNotFoundException("工作空间", str(target_ws_id))
|
||
|
||
# 不能分享给自己的工作空间
|
||
if target_ws_id == app.workspace_id:
|
||
raise BusinessException(
|
||
"不能分享应用到自己的工作空间",
|
||
BizCode.INVALID_PARAMETER
|
||
)
|
||
|
||
# 3. 创建分享记录
|
||
now = datetime.datetime.now()
|
||
shares = []
|
||
|
||
for target_ws_id in target_workspace_ids:
|
||
# 检查是否已经分享过
|
||
stmt = select(AppShare).where(
|
||
AppShare.source_app_id == app_id,
|
||
AppShare.target_workspace_id == target_ws_id,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
existing_share = self.db.scalars(stmt).first()
|
||
|
||
if existing_share:
|
||
logger.debug(
|
||
"应用已分享到该工作空间,跳过",
|
||
extra={"app_id": str(app_id), "target_workspace_id": str(target_ws_id)}
|
||
)
|
||
shares.append(existing_share)
|
||
continue
|
||
|
||
# 创建新的分享记录
|
||
share = AppShare(
|
||
id=uuid.uuid4(),
|
||
source_app_id=app_id,
|
||
source_workspace_id=app.workspace_id,
|
||
target_workspace_id=target_ws_id,
|
||
shared_by=user_id,
|
||
permission=permission,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
self.db.add(share)
|
||
shares.append(share)
|
||
|
||
logger.debug(
|
||
"创建分享记录",
|
||
extra={"app_id": str(app_id), "target_workspace_id": str(target_ws_id)}
|
||
)
|
||
|
||
self.db.commit()
|
||
|
||
logger.info(
|
||
"应用分享成功",
|
||
extra={
|
||
"app_id": str(app_id),
|
||
"shared_count": len(shares),
|
||
"app_name": app.name
|
||
}
|
||
)
|
||
|
||
return shares
|
||
|
||
def unshare_app(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
target_workspace_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> None:
|
||
"""取消应用分享
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
target_workspace_id: 目标工作空间ID
|
||
workspace_id: 当前工作空间ID(用于权限验证)
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用或分享记录不存在时
|
||
BusinessException: 当应用不在指定工作空间时
|
||
"""
|
||
from app.models import AppShare
|
||
|
||
logger.info(
|
||
"取消应用分享",
|
||
extra={
|
||
"app_id": str(app_id),
|
||
"target_workspace_id": str(target_workspace_id)
|
||
}
|
||
)
|
||
|
||
# 1. 验证应用
|
||
app = self._get_app_or_404(app_id)
|
||
self._validate_workspace_access(app, workspace_id)
|
||
|
||
# 2. 查找分享记录
|
||
stmt = select(AppShare).where(
|
||
AppShare.source_app_id == app_id,
|
||
AppShare.target_workspace_id == target_workspace_id,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
share = self.db.scalars(stmt).first()
|
||
|
||
if not share:
|
||
logger.warning(
|
||
"分享记录不存在",
|
||
extra={"app_id": str(app_id), "target_workspace_id": str(target_workspace_id)}
|
||
)
|
||
raise ResourceNotFoundException(
|
||
"分享记录",
|
||
f"app_id={app_id}, target_workspace_id={target_workspace_id}"
|
||
)
|
||
|
||
# 3. 逻辑删除分享记录
|
||
share.is_active = False
|
||
self.db.commit()
|
||
|
||
logger.info(
|
||
"应用分享已取消",
|
||
extra={"app_id": str(app_id), "target_workspace_id": str(target_workspace_id)}
|
||
)
|
||
|
||
def unshare_all_apps_to_workspace(
|
||
self,
|
||
*,
|
||
target_workspace_id: uuid.UUID,
|
||
workspace_id: uuid.UUID
|
||
) -> int:
|
||
"""Cancel all app shares from current workspace to a target workspace.
|
||
|
||
Args:
|
||
target_workspace_id: Target workspace ID to cancel all shares to
|
||
workspace_id: Current workspace ID (source)
|
||
|
||
Returns:
|
||
Number of share records deleted
|
||
"""
|
||
from app.models import AppShare
|
||
|
||
logger.info(
|
||
"取消对目标工作空间的所有应用分享",
|
||
extra={"target_workspace_id": str(target_workspace_id), "workspace_id": str(workspace_id)}
|
||
)
|
||
|
||
# Query active records first for reliable count
|
||
id_stmt = select(AppShare.id).where(
|
||
AppShare.source_workspace_id == workspace_id,
|
||
AppShare.target_workspace_id == target_workspace_id,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
ids = list(self.db.scalars(id_stmt).all())
|
||
count = len(ids)
|
||
|
||
if ids:
|
||
# Soft delete: mark as inactive
|
||
from sqlalchemy import update as sa_update
|
||
self.db.execute(
|
||
sa_update(AppShare).where(AppShare.id.in_(ids)).values(is_active=False)
|
||
)
|
||
self.db.commit()
|
||
|
||
logger.info("已取消分享记录数", extra={"count": count})
|
||
return count
|
||
|
||
def list_app_shares(
|
||
self,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
workspace_id: Optional[uuid.UUID] = None
|
||
) -> List[AppShare]:
|
||
"""列出应用的所有分享记录
|
||
|
||
Args:
|
||
app_id: 应用ID
|
||
workspace_id: 当前工作空间ID(用于权限验证)
|
||
|
||
Returns:
|
||
List[AppShare]: 分享记录列表
|
||
|
||
Raises:
|
||
ResourceNotFoundException: 当应用不存在时
|
||
BusinessException: 当应用不在指定工作空间时
|
||
"""
|
||
from app.models import AppShare
|
||
|
||
logger.debug("列出应用分享记录", extra={"app_id": str(app_id)})
|
||
|
||
# 验证应用
|
||
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.is_active.is_(True)
|
||
).order_by(AppShare.created_at.desc())
|
||
|
||
shares = list(self.db.scalars(stmt).all())
|
||
|
||
logger.debug(
|
||
"应用分享记录查询完成",
|
||
extra={"app_id": str(app_id), "count": len(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,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
share = self.db.scalars(stmt).first()
|
||
|
||
if not share:
|
||
raise ResourceNotFoundException(
|
||
"共享记录",
|
||
f"app_id={app_id}, workspace_id={workspace_id}"
|
||
)
|
||
|
||
# Soft delete
|
||
share.is_active = False
|
||
self.db.commit()
|
||
|
||
logger.info(
|
||
"共享应用已移除",
|
||
extra={"app_id": str(app_id), "workspace_id": str(workspace_id)}
|
||
)
|
||
|
||
def remove_all_shared_apps_from_workspace(
|
||
self,
|
||
*,
|
||
source_workspace_id: uuid.UUID,
|
||
workspace_id: uuid.UUID
|
||
) -> int:
|
||
"""Remove all shared apps from a specific source workspace.
|
||
|
||
Args:
|
||
source_workspace_id: The workspace that shared the apps
|
||
workspace_id: Current workspace ID (recipient)
|
||
|
||
Returns:
|
||
Number of share records deleted
|
||
"""
|
||
from app.models import AppShare
|
||
|
||
logger.info(
|
||
"批量移除来源工作空间的共享应用",
|
||
extra={"source_workspace_id": str(source_workspace_id), "workspace_id": str(workspace_id)}
|
||
)
|
||
|
||
# Query active records for reliable count, then soft delete
|
||
id_stmt = select(AppShare.id).where(
|
||
AppShare.source_workspace_id == source_workspace_id,
|
||
AppShare.target_workspace_id == workspace_id,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
ids = list(self.db.scalars(id_stmt).all())
|
||
count = len(ids)
|
||
|
||
if ids:
|
||
from sqlalchemy import update as sa_update
|
||
self.db.execute(
|
||
sa_update(AppShare).where(AppShare.id.in_(ids)).values(is_active=False)
|
||
)
|
||
self.db.commit()
|
||
|
||
logger.info("已移除共享记录数", extra={"count": count})
|
||
return count
|
||
|
||
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,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
.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,
|
||
AppShare.is_active.is_(True)
|
||
)
|
||
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
|
||
|
||
|
||
# ==================== 向后兼容的函数接口 ====================
|
||
# 保留函数接口以兼容现有代码,但内部使用服务类
|
||
|
||
def create_app(db: Session, *, user_id: uuid.UUID, workspace_id: uuid.UUID, data: app_schema.AppCreate) -> App:
|
||
"""创建应用(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.create_app(user_id=user_id, workspace_id=workspace_id, data=data)
|
||
|
||
|
||
def update_app(db: Session, *, app_id: uuid.UUID, data: app_schema.AppUpdate,
|
||
workspace_id: uuid.UUID | None = None) -> App:
|
||
"""更新应用(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.update_app(app_id=app_id, data=data, workspace_id=workspace_id)
|
||
|
||
|
||
def delete_app(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> None:
|
||
"""删除应用(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.delete_app(app_id=app_id, workspace_id=workspace_id)
|
||
|
||
|
||
def update_agent_config(db: Session, *, app_id: uuid.UUID, data: app_schema.AgentConfigUpdate,
|
||
workspace_id: uuid.UUID | None = None) -> AgentConfig:
|
||
"""更新 Agent 配置(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.update_agent_config(app_id=app_id, data=data, workspace_id=workspace_id)
|
||
|
||
|
||
def update_workflow_config(db: Session, *, app_id: uuid.UUID, data: WorkflowConfigUpdate,
|
||
workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
|
||
"""更新 Agent 配置(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.update_workflow_config(app_id=app_id, data=data, workspace_id=workspace_id)
|
||
|
||
|
||
def get_agent_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> AgentConfig:
|
||
"""获取 Agent 配置(向后兼容接口)
|
||
|
||
如果配置不存在,返回默认配置模板
|
||
"""
|
||
service = AppService(db)
|
||
return service.get_agent_config(app_id=app_id, workspace_id=workspace_id)
|
||
|
||
|
||
def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
|
||
"""获取 Agent 配置(向后兼容接口)
|
||
|
||
如果配置不存在,返回默认配置模板
|
||
"""
|
||
service = AppService(db)
|
||
return service.get_workflow_config(app_id=app_id, workspace_id=workspace_id)
|
||
|
||
|
||
def publish(db: Session, *, app_id: uuid.UUID, publisher_id: uuid.UUID, workspace_id: uuid.UUID | None = None,
|
||
version_name: str, release_notes: Optional[str] = None) -> AppRelease:
|
||
"""发布应用(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.publish(app_id=app_id, publisher_id=publisher_id, version_name=version_name,
|
||
workspace_id=workspace_id, release_notes=release_notes)
|
||
|
||
|
||
def get_current_release(
|
||
db: Session,
|
||
*,
|
||
app_id: uuid.UUID,
|
||
workspace_id: uuid.UUID | None = None
|
||
) -> Optional[AppRelease]:
|
||
"""获取当前发布版本(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.get_current_release(app_id=app_id, workspace_id=workspace_id)
|
||
|
||
|
||
def list_releases(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> List[AppRelease]:
|
||
"""列出发布版本(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.list_releases(app_id=app_id, workspace_id=workspace_id)
|
||
|
||
|
||
def rollback(db: Session, *, app_id: uuid.UUID, version: int, workspace_id: uuid.UUID | None = None) -> AppRelease:
|
||
"""回滚应用(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.rollback(app_id=app_id, version=version, workspace_id=workspace_id)
|
||
|
||
|
||
def list_apps(
|
||
db: Session,
|
||
*,
|
||
workspace_id: uuid.UUID,
|
||
type: Optional[str] = None,
|
||
visibility: Optional[str] = None,
|
||
status: Optional[str] = None,
|
||
search: Optional[str] = None,
|
||
include_shared: bool = True,
|
||
shared_only: bool = False,
|
||
page: int = 1,
|
||
pagesize: int = 10,
|
||
) -> Tuple[List[App], int]:
|
||
"""列出应用(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.list_apps(
|
||
workspace_id=workspace_id,
|
||
type=type,
|
||
visibility=visibility,
|
||
status=status,
|
||
search=search,
|
||
include_shared=include_shared,
|
||
shared_only=shared_only,
|
||
page=page,
|
||
pagesize=pagesize,
|
||
)
|
||
|
||
|
||
def get_apps_by_ids(
|
||
db: Session,
|
||
app_ids: List[str],
|
||
workspace_id: uuid.UUID
|
||
) -> List[App]:
|
||
"""根据ID列表获取应用(向后兼容接口)"""
|
||
service = AppService(db)
|
||
return service.get_apps_by_ids(app_ids, workspace_id)
|
||
|
||
|
||
# ==================== 依赖注入函数 ====================
|
||
|
||
def get_app_service(
|
||
db: Annotated[Session, Depends(get_db)]
|
||
) -> AppService:
|
||
"""获取工作流服务(依赖注入)"""
|
||
return AppService(db)
|