Files
MemoryBear/api/app/services/app_service.py
Ke Sun f7e89af9d2 fix(app): memory config initialization for end users
- Add memory_config_id extraction and assignment when creating new end users in public share chat
- Introduce get_or_create_end_user_with_config method to handle memory config setup in single transaction
- Add batch_update_memory_config_id_by_app method for bulk updating end user memory configs
- Rename _update_endusers_memory_config_by_workspace to _update_endusers_memory_config_by_app for correct scope
- Update app publish flow to use app_id instead of workspace_id for memory config updates
- Remove unused actual_end_user_id variable in langchain_agent
- Ensures end users are properly associated with memory configs on creation and during app updates
2026-03-30 16:44:43 +08:00

2584 lines
94 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
应用服务层
提供应用管理的业务逻辑,包括:
- 应用的创建、更新、查询
- 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
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: app_schema.WorkflowConfigCreate,
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 {},
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 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:
self._create_workflow_config(app.id, data.workflow_config, 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
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_idAppRelease属于源空间需清空
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", []),
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 _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)