From 034559aac77f69a0f83312ed59ed2a4fa0b68f5f Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Thu, 15 Jan 2026 11:21:50 +0800 Subject: [PATCH 01/14] fix(workflow): Fix workflow release process and API call issues --- .../controllers/public_share_controller.py | 142 ++++--- .../controllers/service/app_api_controller.py | 10 +- api/app/core/workflow/nodes/knowledge/node.py | 7 +- api/app/models/workflow_model.py | 8 + api/app/services/app_chat_service.py | 6 +- api/app/services/app_service.py | 401 +++++++++--------- api/app/services/workflow_service.py | 19 +- api/app/utils/app_config_utils.py | 5 +- 8 files changed, 314 insertions(+), 284 deletions(-) diff --git a/api/app/controllers/public_share_controller.py b/api/app/controllers/public_share_controller.py index 04da05df..464e602b 100644 --- a/api/app/controllers/public_share_controller.py +++ b/api/app/controllers/public_share_controller.py @@ -8,9 +8,10 @@ from sqlalchemy.orm import Session from app.core.logging_config import get_business_logger from app.core.response_utils import success -from app.db import get_db +from app.db import get_db, get_db_read from app.dependencies import get_share_user_id, ShareTokenData from app.repositories import knowledge_repository +from app.repositories.workflow_repository import WorkflowConfigRepository from app.schemas import release_share_schema, conversation_schema from app.schemas.response_schema import PageData, PageMeta from app.services import workspace_service @@ -19,7 +20,8 @@ from app.services.conversation_service import ConversationService from app.services.release_share_service import ReleaseShareService from app.services.shared_chat_service import SharedChatService from app.services.app_chat_service import AppChatService, get_app_chat_service -from app.utils.app_config_utils import dict_to_multi_agent_config, workflow_config_4_app_release, agent_config_4_app_release, multi_agent_config_4_app_release +from app.utils.app_config_utils import dict_to_multi_agent_config, workflow_config_4_app_release, \ + agent_config_4_app_release, multi_agent_config_4_app_release router = APIRouter(prefix="/public/share", tags=["Public Share"]) logger = get_business_logger() @@ -65,10 +67,10 @@ def get_or_generate_user_id(payload_user_id: str, request: Request) -> str: summary="获取访问 token" ) def get_access_token( - share_token: str, - payload: release_share_schema.TokenRequest, - request: Request, - db: Session = Depends(get_db), + share_token: str, + payload: release_share_schema.TokenRequest, + request: Request, + db: Session = Depends(get_db), ): """获取访问 token @@ -113,9 +115,9 @@ def get_access_token( response_model=None ) def get_shared_release( - password: str = Query(None, description="访问密码(如果需要)"), - share_data: ShareTokenData = Depends(get_share_user_id), - db: Session = Depends(get_db), + password: str = Query(None, description="访问密码(如果需要)"), + share_data: ShareTokenData = Depends(get_share_user_id), + db: Session = Depends(get_db), ): """获取公开分享的发布版本信息 @@ -137,9 +139,9 @@ def get_shared_release( summary="验证访问密码" ) def verify_password( - payload: release_share_schema.PasswordVerifyRequest, - share_data: ShareTokenData = Depends(get_share_user_id), - db: Session = Depends(get_db), + payload: release_share_schema.PasswordVerifyRequest, + share_data: ShareTokenData = Depends(get_share_user_id), + db: Session = Depends(get_db), ): """验证分享的访问密码 @@ -159,11 +161,11 @@ def verify_password( summary="获取嵌入代码" ) def get_embed_code( - width: str = Query("100%", description="iframe 宽度"), - height: str = Query("600px", description="iframe 高度"), - request: Request = None, - share_data: ShareTokenData = Depends(get_share_user_id), - db: Session = Depends(get_db), + width: str = Query("100%", description="iframe 宽度"), + height: str = Query("600px", description="iframe 高度"), + request: Request = None, + share_data: ShareTokenData = Depends(get_share_user_id), + db: Session = Depends(get_db), ): """获取嵌入代码 @@ -183,7 +185,6 @@ def get_embed_code( return success(data=embed_code) - # ---------- 会话管理接口 ---------- @router.get( @@ -191,11 +192,11 @@ def get_embed_code( summary="获取会话列表" ) def list_conversations( - password: str = Query(None, description="访问密码"), - page: int = Query(1, ge=1), - pagesize: int = Query(20, ge=1, le=100), - share_data: ShareTokenData = Depends(get_share_user_id), - db: Session = Depends(get_db), + password: str = Query(None, description="访问密码"), + page: int = Query(1, ge=1), + pagesize: int = Query(20, ge=1, le=100), + share_data: ShareTokenData = Depends(get_share_user_id), + db: Session = Depends(get_db), ): """获取分享应用的会话列表 @@ -209,9 +210,9 @@ def list_conversations( from app.repositories.end_user_repository import EndUserRepository end_user_repo = EndUserRepository(db) new_end_user = end_user_repo.get_or_create_end_user( - app_id=share.app_id, - other_id=other_id - ) + app_id=share.app_id, + other_id=other_id + ) logger.debug(new_end_user.id) service = SharedChatService(db) conversations, total = service.list_conversations( @@ -233,10 +234,10 @@ def list_conversations( summary="获取会话详情(含消息)" ) def get_conversation( - conversation_id: uuid.UUID, - password: str = Query(None, description="访问密码"), - share_data: ShareTokenData = Depends(get_share_user_id), - db: Session = Depends(get_db), + conversation_id: uuid.UUID, + password: str = Query(None, description="访问密码"), + share_data: ShareTokenData = Depends(get_share_user_id), + db: Session = Depends(get_db), ): """获取会话详情和消息历史""" chat_service = SharedChatService(db) @@ -266,10 +267,10 @@ def get_conversation( summary="发送消息(支持流式和非流式)" ) async def chat( - payload: conversation_schema.ChatRequest, - share_data: ShareTokenData = Depends(get_share_user_id), - db: Session = Depends(get_db), - app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None, + payload: conversation_schema.ChatRequest, + share_data: ShareTokenData = Depends(get_share_user_id), + db: Session = Depends(get_db), + app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None, ): """发送消息并获取回复 @@ -313,7 +314,7 @@ async def chat( ) end_user_id = str(new_end_user.id) - appid=share.app_id + appid = share.app_id """获取存储类型和工作空间的ID""" # 直接通过 SQLAlchemy 查询 app @@ -425,16 +426,16 @@ async def chat( # ) async def event_generator(): async for event in app_chat_service.agnet_chat_stream( - message=payload.message, - conversation_id=conversation.id, # 使用已创建的会话 ID - user_id= str(new_end_user.id), # 转换为字符串 - variables=payload.variables, - web_search=payload.web_search, - config=agent_config, - memory=payload.memory, - storage_type=storage_type, - user_rag_memory_id=user_rag_memory_id, - workspace_id=workspace_id + message=payload.message, + conversation_id=conversation.id, # 使用已创建的会话 ID + user_id=str(new_end_user.id), # 转换为字符串 + variables=payload.variables, + web_search=payload.web_search, + config=agent_config, + memory=payload.memory, + storage_type=storage_type, + user_rag_memory_id=user_rag_memory_id, + workspace_id=workspace_id ): yield event @@ -481,15 +482,15 @@ async def chat( async def event_generator(): async for event in app_chat_service.multi_agent_chat_stream( - message=payload.message, - conversation_id=conversation.id, # 使用已创建的会话 ID - user_id=str(new_end_user.id), # 转换为字符串 - variables=payload.variables, - config=config, - web_search=payload.web_search, - memory=payload.memory, - storage_type=storage_type, - user_rag_memory_id=user_rag_memory_id + message=payload.message, + conversation_id=conversation.id, # 使用已创建的会话 ID + user_id=str(new_end_user.id), # 转换为字符串 + variables=payload.variables, + config=config, + web_search=payload.web_search, + memory=payload.memory, + storage_type=storage_type, + user_rag_memory_id=user_rag_memory_id ): yield event @@ -561,24 +562,26 @@ async def chat( # return success(data=conversation_schema.ChatResponse(**result)) elif app_type == AppType.WORKFLOW: - config = workflow_config_4_app_release(release) + if not config.id: + with get_db_read() as db: + source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id) + config.id = source_config.id if payload.stream: async def event_generator(): - async for event in app_chat_service.workflow_chat_stream( - - message=payload.message, - conversation_id=conversation.id, # 使用已创建的会话 ID - user_id=end_user_id, # 转换为字符串 - variables=payload.variables, - config=config, - web_search=payload.web_search, - memory=payload.memory, - storage_type=storage_type, - user_rag_memory_id=user_rag_memory_id, - app_id=release.app_id, - workspace_id=workspace_id + message=payload.message, + conversation_id=conversation.id, # 使用已创建的会话 ID + user_id=end_user_id, # 转换为字符串 + variables=payload.variables, + config=config, + web_search=payload.web_search, + memory=payload.memory, + storage_type=storage_type, + user_rag_memory_id=user_rag_memory_id, + app_id=release.app_id, + workspace_id=workspace_id, + release_id=release.id ): event_type = event.get("event", "message") event_data = event.get("data", {}) @@ -610,7 +613,8 @@ async def chat( storage_type=storage_type, user_rag_memory_id=user_rag_memory_id, app_id=release.app_id, - workspace_id=workspace_id + workspace_id=workspace_id, + release_id=release.id ) logger.debug( "工作流试运行返回结果", diff --git a/api/app/controllers/service/app_api_controller.py b/api/app/controllers/service/app_api_controller.py index 583b4700..677e1623 100644 --- a/api/app/controllers/service/app_api_controller.py +++ b/api/app/controllers/service/app_api_controller.py @@ -242,8 +242,9 @@ async def chat( memory=payload.memory, storage_type=storage_type, user_rag_memory_id=user_rag_memory_id, - app_id=app.app_id, - workspace_id=workspace_id + app_id=app.id, + workspace_id=workspace_id, + release_id=app.current_release.id, ): event_type = event.get("event", "message") event_data = event.get("data", {}) @@ -274,8 +275,9 @@ async def chat( memory=payload.memory, storage_type=storage_type, user_rag_memory_id=user_rag_memory_id, - app_id=app.app_id, - workspace_id=workspace_id + app_id=app.id, + workspace_id=workspace_id, + release_id=app.current_release.id ) logger.debug( "工作流试运行返回结果", diff --git a/api/app/core/workflow/nodes/knowledge/node.py b/api/app/core/workflow/nodes/knowledge/node.py index 221ca079..997135f3 100644 --- a/api/app/core/workflow/nodes/knowledge/node.py +++ b/api/app/core/workflow/nodes/knowledge/node.py @@ -10,9 +10,8 @@ from app.core.workflow.nodes.base_node import BaseNode, WorkflowState from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNodeConfig from app.db import get_db_read from app.models import knowledge_model, knowledgeshare_model, ModelType -from app.repositories import knowledge_repository +from app.repositories import knowledge_repository, knowledgeshare_repository from app.schemas.chunk_schema import RetrieveType -from app.services import knowledge_service, knowledgeshare_service from app.services.model_service import ModelConfigService logger = logging.getLogger(__name__) @@ -96,7 +95,7 @@ class KnowledgeRetrievalNode(BaseNode): filters = self._build_kb_filter(kb_ids, knowledge_model.PermissionType.Share) - share_ids = knowledge_service.knowledge_repository.get_chunked_knowledgeids( + share_ids = knowledge_repository.get_chunked_knowledgeids( db=db, filters=filters ) @@ -105,7 +104,7 @@ class KnowledgeRetrievalNode(BaseNode): filters = [ knowledgeshare_model.KnowledgeShare.target_kb_id.in_(kb_ids) ] - items = knowledgeshare_service.knowledgeshare_repository.get_source_kb_ids_by_target_kb_id( + items = knowledgeshare_repository.get_source_kb_ids_by_target_kb_id( db=db, filters=filters ) diff --git a/api/app/models/workflow_model.py b/api/app/models/workflow_model.py index d599f717..4f9ffe68 100644 --- a/api/app/models/workflow_model.py +++ b/api/app/models/workflow_model.py @@ -75,6 +75,14 @@ class WorkflowExecution(Base): nullable=False, index=True ) + + release_id = Column( + UUID(as_uuid=True), + ForeignKey("app_releases.id", ondelete="CASCADE"), + nullable=True, + index=True + ) + app_id = Column( UUID(as_uuid=True), ForeignKey("apps.id", ondelete="CASCADE"), diff --git a/api/app/services/app_chat_service.py b/api/app/services/app_chat_service.py index bc2d6ca3..c0a66e03 100644 --- a/api/app/services/app_chat_service.py +++ b/api/app/services/app_chat_service.py @@ -527,6 +527,7 @@ class AppChatService: conversation_id: uuid.UUID, config: WorkflowConfig, app_id: uuid.UUID, + release_id: uuid.UUID, workspace_id: uuid.UUID, user_id: Optional[str] = None, variables: Optional[Dict[str, Any]] = None, @@ -549,6 +550,7 @@ class AppChatService: payload=payload, config=config, workspace_id=workspace_id, + release_id=release_id, ) async def workflow_chat_stream( @@ -557,6 +559,7 @@ class AppChatService: conversation_id: uuid.UUID, config: WorkflowConfig, app_id: uuid.UUID, + release_id: uuid.UUID, workspace_id: uuid.UUID, user_id: str = None, variables: Optional[Dict[str, Any]] = None, @@ -565,7 +568,7 @@ class AppChatService: storage_type: Optional[str] = None, user_rag_memory_id: Optional[str] = None, - ) -> AsyncGenerator[str, None]: + ) -> AsyncGenerator[dict, None]: """聊天(流式)""" workflow_service = WorkflowService(self.db) payload = DraftRunRequest( @@ -580,6 +583,7 @@ class AppChatService: payload=payload, config=config, workspace_id=workspace_id, + release_id=release_id ): yield event diff --git a/api/app/services/app_service.py b/api/app/services/app_service.py index 6d5204f8..c91e9153 100644 --- a/api/app/services/app_service.py +++ b/api/app/services/app_service.py @@ -129,7 +129,7 @@ class AppService: Raises: ResourceNotFoundException: 当应用不存在时 """ - app = get_apps_by_id(self.db,app_id) + 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)) @@ -227,7 +227,6 @@ class AppService: 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( @@ -281,10 +280,10 @@ class AppService: ) def _create_agent_config( - self, - app_id: uuid.UUID, - config_data: app_schema.AgentConfigCreate, - now: datetime.datetime + self, + app_id: uuid.UUID, + config_data: app_schema.AgentConfigCreate, + now: datetime.datetime ) -> None: """创建 Agent 配置(内部方法) @@ -313,10 +312,10 @@ class AppService: logger.debug("Agent 配置已创建", extra={"app_id": str(app_id)}) def _create_multi_agent_config( - self, - app_id: uuid.UUID, - config_data: Dict[str, Any], - now: datetime.datetime + self, + app_id: uuid.UUID, + config_data: Dict[str, Any], + now: datetime.datetime ) -> None: """创建多 Agent 配置(内部方法) @@ -411,9 +410,9 @@ class AppService: return 1 if max_ver is None else int(max_ver) + 1 def _convert_to_schema( - self, - app: App, - current_workspace_id: uuid.UUID + self, + app: App, + current_workspace_id: uuid.UUID ) -> app_schema.App: """将 App 模型转换为 Schema,并设置 is_shared 字段 @@ -447,9 +446,9 @@ class AppService: # ==================== 应用管理 ==================== def get_app( - self, - app_id: uuid.UUID, - workspace_id: Optional[uuid.UUID] = None + self, + app_id: uuid.UUID, + workspace_id: Optional[uuid.UUID] = None ) -> App: """获取应用详情 @@ -469,11 +468,11 @@ class AppService: return app def create_app( - self, - *, - user_id: uuid.UUID, - workspace_id: uuid.UUID, - data: app_schema.AppCreate + self, + *, + user_id: uuid.UUID, + workspace_id: uuid.UUID, + data: app_schema.AppCreate ) -> App: """创建应用 @@ -535,11 +534,11 @@ class AppService: 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 + self, + *, + app_id: uuid.UUID, + data: app_schema.AppUpdate, + workspace_id: Optional[uuid.UUID] = None ) -> App: """更新应用基本信息 @@ -578,10 +577,10 @@ class AppService: return app def delete_app( - self, - *, - app_id: uuid.UUID, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + workspace_id: Optional[uuid.UUID] = None ) -> None: """删除应用 @@ -612,12 +611,12 @@ class AppService: ) def copy_app( - self, - *, - app_id: uuid.UUID, - user_id: uuid.UUID, - workspace_id: Optional[uuid.UUID] = None, - new_name: Optional[str] = None + self, + *, + app_id: uuid.UUID, + user_id: uuid.UUID, + workspace_id: Optional[uuid.UUID] = None, + new_name: Optional[str] = None ) -> App: """复制应用(包括基础信息和配置) @@ -716,16 +715,16 @@ class AppService: raise BusinessException(f"应用复制失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e) 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, - page: int = 1, - pagesize: int = 10, + 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, + page: int = 1, + pagesize: int = 10, ) -> Tuple[List[App], int]: """列出工作空间中的应用(分页) @@ -813,9 +812,9 @@ class AppService: return items, int(total) def get_apps_by_ids( - self, - app_ids: List[str], - workspace_id: uuid.UUID + self, + app_ids: List[str], + workspace_id: uuid.UUID ) -> List[App]: """根据ID列表获取应用 @@ -846,11 +845,11 @@ class AppService: # ==================== Agent 配置管理 ==================== def update_agent_config( - self, - *, - app_id: uuid.UUID, - data: app_schema.AgentConfigUpdate, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + data: app_schema.AgentConfigUpdate, + workspace_id: Optional[uuid.UUID] = None ) -> AgentConfig: """更新 Agent 配置 @@ -875,7 +874,8 @@ class AppService: self._validate_workspace_access(app, workspace_id) - stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active==True).order_by(AgentConfig.updated_at.desc()) + stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by( + AgentConfig.updated_at.desc()) agent_cfg: Optional[AgentConfig] = self.db.scalars(stmt).first() now = datetime.datetime.now() @@ -918,10 +918,10 @@ class AppService: return agent_cfg def get_agent_config( - self, - *, - app_id: uuid.UUID, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + workspace_id: Optional[uuid.UUID] = None ) -> AgentConfig: """获取 Agent 配置 @@ -948,7 +948,8 @@ class AppService: # 只读操作,允许访问共享应用 self._validate_app_accessible(app, workspace_id) - stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(AgentConfig.updated_at.desc()) + stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by( + AgentConfig.updated_at.desc()) config = self.db.scalars(stmt).first() if config: @@ -1166,13 +1167,13 @@ class AppService: # ==================== 应用发布管理 ==================== 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 + self, + *, + app_id: uuid.UUID, + publisher_id: uuid.UUID, + version_name: str, + workspace_id: Optional[uuid.UUID] = None, + release_notes: Optional[str] = None ) -> AppRelease: """发布应用(创建不可变快照) @@ -1200,7 +1201,8 @@ class AppService: default_model_config_id = None if app.type == AppType.AGENT: - stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(AgentConfig.updated_at.desc()) + stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == 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) @@ -1236,8 +1238,7 @@ class AppService: 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), @@ -1264,6 +1265,7 @@ class AppService: raise BusinessException("应用缺少有效配置,无法发布", BizCode.CONFIG_MISSING) config = { + "id": workflow_cfg.id, "nodes": workflow_cfg.nodes, "edges": workflow_cfg.edges, "variables": workflow_cfg.variables, @@ -1285,7 +1287,7 @@ class AppService: id=uuid.uuid4(), app_id=app_id, version=version, - version_name = version_name, + version_name=version_name, release_notes=release_notes, name=app.name, description=app.description, @@ -1319,10 +1321,10 @@ class AppService: return release def get_current_release( - self, - *, - app_id: uuid.UUID, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + workspace_id: Optional[uuid.UUID] = None ) -> Optional[AppRelease]: """获取当前发布版本 @@ -1349,10 +1351,10 @@ class AppService: return self.db.get(AppRelease, app.current_release_id) def list_releases( - self, - *, - app_id: uuid.UUID, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + workspace_id: Optional[uuid.UUID] = None ) -> List[AppRelease]: """列出应用的所有发布版本(倒序) @@ -1381,11 +1383,11 @@ class AppService: return list(self.db.scalars(stmt).all()) def rollback( - self, - *, - app_id: uuid.UUID, - version: int, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + version: int, + workspace_id: Optional[uuid.UUID] = None ) -> AppRelease: """回滚到指定版本 @@ -1434,12 +1436,12 @@ class AppService: # ==================== 应用分享功能 ==================== def share_app( - self, - *, - app_id: uuid.UUID, - target_workspace_ids: List[uuid.UUID], - user_id: uuid.UUID, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + target_workspace_ids: List[uuid.UUID], + user_id: uuid.UUID, + workspace_id: Optional[uuid.UUID] = None ) -> AppShare: """分享应用到其他工作空间 @@ -1457,7 +1459,6 @@ class AppService: BusinessException: 当应用不在指定工作空间或目标工作空间无效时 """ - logger.info( "分享应用", extra={ @@ -1536,11 +1537,11 @@ class AppService: return shares def unshare_app( - self, - *, - app_id: uuid.UUID, - target_workspace_id: uuid.UUID, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + target_workspace_id: uuid.UUID, + workspace_id: Optional[uuid.UUID] = None ) -> None: """取消应用分享 @@ -1594,10 +1595,10 @@ class AppService: ) def list_app_shares( - self, - *, - app_id: uuid.UUID, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + workspace_id: Optional[uuid.UUID] = None ) -> List[AppShare]: """列出应用的所有分享记录 @@ -1637,14 +1638,14 @@ class AppService: # ==================== 试运行功能 ==================== async def draft_run( - self, - *, - app_id: uuid.UUID, - message: str, - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + message: str, + conversation_id: Optional[str] = None, + user_id: Optional[str] = None, + variables: Optional[Dict[str, Any]] = None, + workspace_id: Optional[uuid.UUID] = None ) -> Dict[str, Any]: """试运行 Agent(使用当前草稿配置) @@ -1736,14 +1737,14 @@ class AppService: return result async def draft_run_stream( - self, - *, - app_id: uuid.UUID, - message: str, - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None + self, + *, + app_id: uuid.UUID, + message: str, + conversation_id: Optional[str] = None, + user_id: Optional[str] = None, + variables: Optional[Dict[str, Any]] = None, + workspace_id: Optional[uuid.UUID] = None ): """试运行 Agent(流式返回) @@ -1794,30 +1795,30 @@ class AppService: # 4. 调用流式试运行服务 draft_service = DraftRunService(self.db) async for event in draft_service.run_stream( - agent_config=agent_cfg, - model_config=model_config, - message=message, - workspace_id=workspace_id, - conversation_id=conversation_id, - user_id=user_id, - variables=variables + agent_config=agent_cfg, + model_config=model_config, + message=message, + workspace_id=workspace_id, + conversation_id=conversation_id, + user_id=user_id, + variables=variables ): yield event # ==================== 多模型对比试运行 ==================== async def draft_run_compare( - self, - *, - app_id: uuid.UUID, - message: str, - models: List[app_schema.ModelCompareItem], - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None, - parallel: bool = True, - timeout: int = 60 + self, + *, + app_id: uuid.UUID, + message: str, + models: List[app_schema.ModelCompareItem], + conversation_id: Optional[str] = None, + user_id: Optional[str] = None, + variables: Optional[Dict[str, Any]] = None, + workspace_id: Optional[uuid.UUID] = None, + parallel: bool = True, + timeout: int = 60 ) -> Dict[str, Any]: """多模型对比试运行 @@ -1907,17 +1908,17 @@ class AppService: return result async def draft_run_compare_stream( - self, - *, - app_id: uuid.UUID, - message: str, - models: List[app_schema.ModelCompareItem], - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None, - parallel: bool = True, - timeout: int = 60 + self, + *, + app_id: uuid.UUID, + message: str, + models: List[app_schema.ModelCompareItem], + conversation_id: Optional[str] = None, + user_id: Optional[str] = None, + variables: Optional[Dict[str, Any]] = None, + workspace_id: Optional[uuid.UUID] = None, + parallel: bool = True, + timeout: int = 60 ): """多模型对比试运行(流式返回) @@ -1982,15 +1983,15 @@ class AppService: # 4. 调用 DraftRunService 的流式对比方法 draft_service = DraftRunService(self.db) async for event in draft_service.run_compare_stream( - agent_config=agent_cfg, - models=model_configs, - message=message, - workspace_id=workspace_id, - conversation_id=conversation_id, - user_id=user_id, - variables=variables, - parallel=parallel, - timeout=timeout + agent_config=agent_cfg, + models=model_configs, + message=message, + workspace_id=workspace_id, + conversation_id=conversation_id, + user_id=user_id, + variables=variables, + parallel=parallel, + timeout=timeout ): yield event @@ -2009,7 +2010,8 @@ def create_app(db: Session, *, user_id: uuid.UUID, workspace_id: uuid.UUID, data 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: +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) @@ -2021,12 +2023,15 @@ def delete_app(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None 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: +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: + +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) @@ -2040,6 +2045,7 @@ def get_agent_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID 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 配置(向后兼容接口) @@ -2049,13 +2055,16 @@ def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UU 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: +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) + 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]: +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) @@ -2074,16 +2083,16 @@ def rollback(db: Session, *, app_id: uuid.UUID, version: int, workspace_id: uuid 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, - page: int = 1, - pagesize: int = 10, + 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, + page: int = 1, + pagesize: int = 10, ) -> Tuple[List[App], int]: """列出应用(向后兼容接口)""" service = AppService(db) @@ -2100,9 +2109,9 @@ def list_apps( def get_apps_by_ids( - db: Session, - app_ids: List[str], - workspace_id: uuid.UUID + db: Session, + app_ids: List[str], + workspace_id: uuid.UUID ) -> List[App]: """根据ID列表获取应用(向后兼容接口)""" service = AppService(db) @@ -2112,14 +2121,14 @@ def get_apps_by_ids( # ==================== 向后兼容的函数接口 ==================== async def draft_run( - db: Session, - *, - app_id: uuid.UUID, - message: str, - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None + db: Session, + *, + app_id: uuid.UUID, + message: str, + conversation_id: Optional[str] = None, + user_id: Optional[str] = None, + variables: Optional[Dict[str, Any]] = None, + workspace_id: Optional[uuid.UUID] = None ) -> Dict[str, Any]: """试运行 Agent(向后兼容接口)""" service = AppService(db) @@ -2134,30 +2143,28 @@ async def draft_run( async def draft_run_stream( - db: Session, - *, - app_id: uuid.UUID, - message: str, - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None + db: Session, + *, + app_id: uuid.UUID, + message: str, + conversation_id: Optional[str] = None, + user_id: Optional[str] = None, + variables: Optional[Dict[str, Any]] = None, + workspace_id: Optional[uuid.UUID] = None ): """试运行 Agent 流式返回(向后兼容接口)""" service = AppService(db) async for event in service.draft_run_stream( - app_id=app_id, - message=message, - conversation_id=conversation_id, - user_id=user_id, - variables=variables, - workspace_id=workspace_id + app_id=app_id, + message=message, + conversation_id=conversation_id, + user_id=user_id, + variables=variables, + workspace_id=workspace_id ): yield event - - # ==================== 依赖注入函数 ==================== def get_app_service( diff --git a/api/app/services/workflow_service.py b/api/app/services/workflow_service.py index 974d5418..6cff6844 100644 --- a/api/app/services/workflow_service.py +++ b/api/app/services/workflow_service.py @@ -4,7 +4,7 @@ import datetime import logging import uuid -from typing import Any, Annotated, AsyncGenerator +from typing import Any, Annotated, AsyncGenerator, Optional from deprecated import deprecated from fastapi import Depends @@ -266,6 +266,7 @@ class WorkflowService: workflow_config_id: uuid.UUID, app_id: uuid.UUID, trigger_type: str, + release_id: uuid.UUID | None = None, triggered_by: uuid.UUID | None = None, conversation_id: uuid.UUID | None = None, input_data: dict[str, Any] | None = None @@ -273,6 +274,7 @@ class WorkflowService: """创建工作流执行记录 Args: + release_id: 应用发布 ID workflow_config_id: 工作流配置 ID app_id: 应用 ID trigger_type: 触发类型 @@ -289,6 +291,7 @@ class WorkflowService: execution = WorkflowExecution( workflow_config_id=workflow_config_id, app_id=app_id, + release_id=release_id, conversation_id=conversation_id, execution_id=execution_id, trigger_type=trigger_type, @@ -414,12 +417,14 @@ class WorkflowService: payload: DraftRunRequest, config: WorkflowConfig, workspace_id: uuid.UUID, + release_id: uuid.UUID | None = None, ): """运行工作流 Args: - workspace_id: - config: + release_id: 发布 ID + workspace_id:工作空间 ID + config: 配置 payload: app_id: 应用 ID @@ -463,7 +468,8 @@ class WorkflowService: trigger_type="manual", triggered_by=None, conversation_id=conversation_id_uuid, - input_data=input_data + input_data=input_data, + release_id=release_id, ) # 3. 构建工作流配置字典 @@ -562,10 +568,12 @@ class WorkflowService: payload: DraftRunRequest, config: WorkflowConfig, workspace_id: uuid.UUID, + release_id: Optional[uuid.UUID] = None, ): """运行工作流(流式) Args: + release_id: 发布id workspace_id: app_id: 应用 ID payload: 请求对象(包含 message, variables, conversation_id 等) @@ -611,7 +619,8 @@ class WorkflowService: trigger_type="manual", triggered_by=None, conversation_id=conversation_id_uuid, - input_data=input_data + input_data=input_data, + release_id=release_id, ) # 3. 构建工作流配置字典 diff --git a/api/app/utils/app_config_utils.py b/api/app/utils/app_config_utils.py index 4a35a4cc..514e4565 100644 --- a/api/app/utils/app_config_utils.py +++ b/api/app/utils/app_config_utils.py @@ -120,12 +120,9 @@ def multi_agent_config_4_app_release(release: AppRelease) -> MultiAgentConfig: def workflow_config_4_app_release(release: AppRelease) -> WorkflowConfig: config_dict = release.config - with get_db_read() as db: - source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id) - source_config_id = source_config.id config = WorkflowConfig( - id=source_config_id, + id=config_dict.get("id"), app_id=release.app_id, nodes=config_dict.get("nodes", []), edges=config_dict.get("edges", []), From 07760d55b7670328d53edb6b4d13d810580cb7c1 Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Mon, 19 Jan 2026 10:19:02 +0800 Subject: [PATCH 02/14] perf(workflow): optimize default values for LLM node configuration --- api/app/core/workflow/nodes/llm/config.py | 2 +- api/app/services/app_service.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/api/app/core/workflow/nodes/llm/config.py b/api/app/core/workflow/nodes/llm/config.py index f65d5879..265724f3 100644 --- a/api/app/core/workflow/nodes/llm/config.py +++ b/api/app/core/workflow/nodes/llm/config.py @@ -66,7 +66,7 @@ class LLMNodeConfig(BaseNodeConfig): ) memory: MemoryWindowSetting = Field( - ..., + default_factory=MemoryWindowSetting, description="对话上下文窗口" ) diff --git a/api/app/services/app_service.py b/api/app/services/app_service.py index c91e9153..2ac9ac05 100644 --- a/api/app/services/app_service.py +++ b/api/app/services/app_service.py @@ -758,8 +758,7 @@ class AppService: ) # 构建查询条件 - filters = [] - filters.append(App.is_active == True) + filters = [App.is_active == True] if type: filters.append(App.type == type) if visibility: @@ -948,8 +947,12 @@ class AppService: # 只读操作,允许访问共享应用 self._validate_app_accessible(app, workspace_id) - stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by( - AgentConfig.updated_at.desc()) + 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() if config: @@ -1265,7 +1268,7 @@ class AppService: raise BusinessException("应用缺少有效配置,无法发布", BizCode.CONFIG_MISSING) config = { - "id": workflow_cfg.id, + "id": str(workflow_cfg.id), "nodes": workflow_cfg.nodes, "edges": workflow_cfg.edges, "variables": workflow_cfg.variables, @@ -2063,8 +2066,12 @@ def publish(db: Session, *, app_id: uuid.UUID, publisher_id: uuid.UUID, workspac 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]: +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) From 0489013dddb868329d012a27a832441feb8117db Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Mon, 19 Jan 2026 10:21:56 +0800 Subject: [PATCH 03/14] feat(workflow): support token usage metrics and subgraph state output - expose token consumption for workflow runs - enable loop nodes to output subgraph states - enhance executor logging --- .../controllers/public_share_controller.py | 1 + api/app/core/workflow/executor.py | 38 +++++++++++-------- .../workflow/nodes/cycle_graph/iteration.py | 16 +++++--- .../core/workflow/nodes/cycle_graph/loop.py | 13 +++++-- api/app/core/workflow/nodes/llm/node.py | 17 +++++---- api/app/services/workflow_service.py | 38 +++++++++++-------- 6 files changed, 74 insertions(+), 49 deletions(-) diff --git a/api/app/controllers/public_share_controller.py b/api/app/controllers/public_share_controller.py index 464e602b..17ad70a7 100644 --- a/api/app/controllers/public_share_controller.py +++ b/api/app/controllers/public_share_controller.py @@ -567,6 +567,7 @@ async def chat( with get_db_read() as db: source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id) config.id = source_config.id + config.id = uuid.UUID(config.id) if payload.stream: async def event_generator(): async for event in app_chat_service.workflow_chat_stream( diff --git a/api/app/core/workflow/executor.py b/api/app/core/workflow/executor.py index c048f447..ad03fec1 100644 --- a/api/app/core/workflow/executor.py +++ b/api/app/core/workflow/executor.py @@ -8,6 +8,7 @@ import logging import uuid from typing import Any +from langchain_core.runnables import RunnableConfig from langgraph.graph.state import CompiledStateGraph from app.core.workflow.graph_builder import GraphBuilder @@ -53,11 +54,11 @@ class WorkflowExecutor: self.edges = workflow_config.get("edges", []) self.execution_config = workflow_config.get("execution_config", {}) - self.checkpoint_config = { - "configurable": { + self.checkpoint_config = RunnableConfig( + configurable={ "thread_id": uuid.uuid4(), } - } + ) def _prepare_initial_state(self, input_data: dict[str, Any]) -> WorkflowState: """准备初始状态(注入系统变量和会话变量) @@ -214,13 +215,13 @@ class WorkflowExecutor: return { "status": "completed", "output": final_output, + "variables": result.get("variables", {}), "node_outputs": node_outputs, "messages": result.get("messages", []), "conversation_id": conversation_id, "elapsed_time": elapsed_time, "token_usage": token_usage, "error": result.get("error"), - "variables": result.get("variables", {}), } def build_graph(self, stream=False) -> CompiledStateGraph: @@ -326,11 +327,10 @@ class WorkflowExecutor: } # 1. 构建图 - graph = self.build_graph(True) + graph = self.build_graph(stream=True) # 2. 初始化状态(自动注入系统变量) initial_state = self._prepare_initial_state(input_data) - # 3. Execute workflow try: chunk_count = 0 @@ -346,14 +346,16 @@ class WorkflowExecutor: mode, data = event else: # Unexpected format, log and skip - logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}") + logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}" + f"- execution_id: {self.execution_id}") continue if mode == "custom": # Handle custom streaming events (chunks from nodes via stream writer) chunk_count += 1 event_type = data.get("type", "node_chunk") # "message" or "node_chunk" - logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}") + logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}" + f"- execution_id: {self.execution_id}") yield { "event": event_type, # "message" or "node_chunk" "data": { @@ -380,7 +382,8 @@ class WorkflowExecutor: variables_sys = variables.get("sys", {}) conversation_id = input_data.get("conversation_id") execution_id = variables_sys.get("execution_id") - logger.info(f"[DEBUG] Node starts execution: {node_name}") + logger.info(f"[NODE-START] Node starts execution: {node_name} " + f"- execution_id: {self.execution_id}") yield { "event": "node_start", @@ -399,7 +402,8 @@ class WorkflowExecutor: variables_sys = variables.get("sys", {}) conversation_id = input_data.get("conversation_id") execution_id = variables_sys.get("execution_id") - logger.info(f"[DEBUG] Node execution completed: {node_name}") + logger.info(f"[NODE-END] Node execution completed: {node_name} " + f"- execution_id: {self.execution_id}") yield { "event": "node_end", @@ -407,13 +411,15 @@ class WorkflowExecutor: "node_id": node_name, "conversation_id": conversation_id, "execution_id": execution_id, - "timestamp": data.get("timestamp") + "timestamp": data.get("timestamp"), + "state": result.get("node_outputs", {}).get(node_name), } } elif mode == "updates": # Handle state updates - store final state - logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())}") + logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())} " + f"- execution_id: {self.execution_id}") # 计算耗时 end_time = datetime.datetime.now() @@ -421,7 +427,7 @@ class WorkflowExecutor: result = graph.get_state(self.checkpoint_config).values logger.info( f"Workflow execution completed (streaming), " - f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s" + f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s, execution_id: {self.execution_id}" ) # 发送 workflow_end 事件 @@ -449,7 +455,8 @@ class WorkflowExecutor: } } - def _extract_final_output(self, node_outputs: dict[str, Any]) -> str | None: + @staticmethod + def _extract_final_output(node_outputs: dict[str, Any]) -> str | None: """从节点输出中提取最终输出 优先级: @@ -473,7 +480,8 @@ class WorkflowExecutor: return None - def _aggregate_token_usage(self, node_outputs: dict[str, Any]) -> dict[str, int] | None: + @staticmethod + def _aggregate_token_usage(node_outputs: dict[str, Any]) -> dict[str, int] | None: """聚合所有节点的 token 使用情况 Args: diff --git a/api/app/core/workflow/nodes/cycle_graph/iteration.py b/api/app/core/workflow/nodes/cycle_graph/iteration.py index 4ae8e118..66c3a700 100644 --- a/api/app/core/workflow/nodes/cycle_graph/iteration.py +++ b/api/app/core/workflow/nodes/cycle_graph/iteration.py @@ -21,6 +21,7 @@ class IterationRuntime: optional parallel execution, flattening of output, and loop control via the workflow state. """ + def __init__( self, graph: CompiledStateGraph, @@ -87,6 +88,7 @@ class IterationRuntime: self.result.append(output) if not result["looping"]: self.looping = False + return result def _create_iteration_tasks(self, array_obj, idx): """ @@ -124,7 +126,7 @@ class IterationRuntime: array_obj = VariablePool(self.state).get(input_expression) if not isinstance(array_obj, list): raise RuntimeError("Cannot iterate over a non-list variable") - + child_state = [] idx = 0 if self.typed_config.parallel: # Execute iterations in parallel batches @@ -132,15 +134,14 @@ class IterationRuntime: tasks = self._create_iteration_tasks(array_obj, idx) logger.info(f"Iteration node {self.node_id}: running, concurrency {len(tasks)}") idx += self.typed_config.parallel_count - await asyncio.gather(*tasks) - logger.info(f"Iteration node {self.node_id}: execution completed") - return self.result + child_state.extend(await asyncio.gather(*tasks)) else: # Execute iterations sequentially while idx < len(array_obj) and self.looping: logger.info(f"Iteration node {self.node_id}: running") item = array_obj[idx] result = await self.graph.ainvoke(self._init_iteration_state(item, idx)) + child_state.append(result) output = VariablePool(result).get(self.output_value) if isinstance(output, list) and self.typed_config.flatten: self.result.extend(output) @@ -150,5 +151,8 @@ class IterationRuntime: self.looping = False idx += 1 - logger.info(f"Iteration node {self.node_id}: execution completed") - return self.result + logger.info(f"Iteration node {self.node_id}: execution completed") + return { + "output": self.result, + "__child_state": child_state + } diff --git a/api/app/core/workflow/nodes/cycle_graph/loop.py b/api/app/core/workflow/nodes/cycle_graph/loop.py index 2e2ab4fb..38d4b21c 100644 --- a/api/app/core/workflow/nodes/cycle_graph/loop.py +++ b/api/app/core/workflow/nodes/cycle_graph/loop.py @@ -67,7 +67,9 @@ class LoopRuntime: variables=pool.get_all_conversation_vars(), node_outputs=pool.get_all_node_outputs(), system_vars=pool.get_all_system_vars(), - ) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type) + ) + if variable.input_type == ValueInputType.VARIABLE + else TypeTransformer.transform(variable.value, variable.type) for variable in self.typed_config.cycle_vars } self.state["node_outputs"][self.node_id] = { @@ -76,7 +78,9 @@ class LoopRuntime: variables=pool.get_all_conversation_vars(), node_outputs=pool.get_all_node_outputs(), system_vars=pool.get_all_system_vars(), - ) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type) + ) + if variable.input_type == ValueInputType.VARIABLE + else TypeTransformer.transform(variable.value, variable.type) for variable in self.typed_config.cycle_vars } loopstate = WorkflowState( @@ -171,10 +175,11 @@ class LoopRuntime: """ loopstate = self._init_loop_state() loop_time = self.typed_config.max_loop + child_state = [] while self.evaluate_conditional(loopstate) and loopstate["looping"] and loop_time > 0: logger.info(f"loop node {self.node_id}: running") - await self.graph.ainvoke(loopstate) + child_state.append(await self.graph.ainvoke(loopstate)) loop_time -= 1 logger.info(f"loop node {self.node_id}: execution completed") - return loopstate["runtime_vars"][self.node_id] + return loopstate["runtime_vars"][self.node_id] | {"__child_state": child_state} diff --git a/api/app/core/workflow/nodes/llm/node.py b/api/app/core/workflow/nodes/llm/node.py index e25bd35d..061a0f6a 100644 --- a/api/app/core/workflow/nodes/llm/node.py +++ b/api/app/core/workflow/nodes/llm/node.py @@ -269,12 +269,16 @@ class LLMNode(BaseNode): chunk_count = 0 # 调用 LLM(流式,支持字符串或消息列表) - async for chunk in llm.astream(prompt_or_messages): + last_meta_data = {} + async for chunk in llm.astream(prompt_or_messages, stream_usage=True): # 提取内容 if hasattr(chunk, 'content'): content = chunk.content else: content = str(chunk) + if hasattr(chunk, 'response_metadata'): + if chunk.response_metadata: + last_meta_data = chunk.response_metadata # 只有当内容不为空时才处理 if content: @@ -288,13 +292,10 @@ class LLMNode(BaseNode): logger.info(f"节点 {self.node_id} LLM 调用完成,输出长度: {len(full_response)}, 总 chunks: {chunk_count}") # 构建完整的 AIMessage(包含元数据) - if isinstance(last_chunk, AIMessage): - final_message = AIMessage( - content=full_response, - response_metadata=last_chunk.response_metadata if hasattr(last_chunk, 'response_metadata') else {} - ) - else: - final_message = AIMessage(content=full_response) + final_message = AIMessage( + content=full_response, + response_metadata=last_meta_data + ) # yield 完成标记 yield {"__final__": True, "result": final_message} diff --git a/api/app/services/workflow_service.py b/api/app/services/workflow_service.py index 6cff6844..b7d5df02 100644 --- a/api/app/services/workflow_service.py +++ b/api/app/services/workflow_service.py @@ -14,15 +14,14 @@ from app.core.error_codes import BizCode from app.core.exceptions import BusinessException from app.core.workflow.validator import validate_workflow_config from app.db import get_db -from app.models.conversation_model import Message from app.models.workflow_model import WorkflowConfig, WorkflowExecution -from app.repositories.conversation_repository import MessageRepository from app.repositories.workflow_repository import ( WorkflowConfigRepository, WorkflowExecutionRepository, WorkflowNodeExecutionRepository ) from app.schemas import DraftRunRequest +from app.services.conversation_service import ConversationService from app.services.multi_agent_service import convert_uuids_to_str logger = logging.getLogger(__name__) @@ -36,7 +35,7 @@ class WorkflowService: self.config_repo = WorkflowConfigRepository(db) self.execution_repo = WorkflowExecutionRepository(db) self.node_execution_repo = WorkflowNodeExecutionRepository(db) - self.message_repo = MessageRepository(db) + self.conversation_service = ConversationService(db) # ==================== 配置管理 ==================== @@ -340,6 +339,7 @@ class WorkflowService: self, execution_id: str, status: str, + token_usage: int | None = None, output_data: dict[str, Any] | None = None, error_message: str | None = None, error_node_id: str | None = None @@ -349,6 +349,7 @@ class WorkflowService: Args: execution_id: 执行 ID status: 状态 + token_usage: token消耗 output_data: 输出数据 error_message: 错误信息 error_node_id: 出错节点 ID @@ -367,6 +368,8 @@ class WorkflowService: ) execution.status = status + if token_usage is not None: + execution.token_usage = token_usage if output_data is not None: execution.output_data = convert_uuids_to_str(output_data) if error_message is not None: @@ -513,20 +516,20 @@ class WorkflowService: # 更新执行结果 if result.get("status") == "completed": + token_usage = result.get("token_usage", {}) or {} self.update_execution_status( execution.execution_id, "completed", - output_data=result + output_data=result, + token_usage=token_usage.get("total_tokens", None) ) final_messages = result.get("messages", [])[init_message_length:] for message in final_messages: - message_obj = Message( + self.conversation_service.add_message( conversation_id=conversation_id_uuid, role=message["role"], - content=message["content"], + content=message["content"] ) - self.message_repo.add_message(message_obj) - self.db.commit() logger.info(f"Workflow Run Success, " f"execution_id: {execution.execution_id}, message count: {len(final_messages)}") else: @@ -662,21 +665,21 @@ class WorkflowService: if event.get("event") == "workflow_end": status = event.get("data", {}).get("status") + token_usage = event.get("data", {}).get("token_usage", {}) or {} if status == "completed": self.update_execution_status( execution.execution_id, "completed", - output_data=event.get("data") + output_data=event.get("data"), + token_usage=token_usage.get("total_tokens", None) ) final_messages = event.get("data", {}).get("messages", [])[init_message_length:] for message in final_messages: - message_obj = Message( + self.conversation_service.add_message( conversation_id=conversation_id_uuid, role=message["role"], - content=message["content"], + content=message["content"] ) - self.message_repo.add_message(message_obj) - self.db.commit() logger.info(f"Workflow Run Success, " f"execution_id: {execution.execution_id}, message count: {len(final_messages)}") elif status == "failed": @@ -793,10 +796,12 @@ class WorkflowService: # 更新执行结果 if result.get("status") == "completed": + token_usage = result.get("data").get("token_usage", {}) or {} self.update_execution_status( execution.execution_id, "completed", - output_data=result.get("node_outputs", {}) + output_data=result.get("node_outputs", {}), + token_usage=token_usage.get("total_tokens", None) ) else: self.update_execution_status( @@ -891,13 +896,14 @@ class WorkflowService: ): # 直接转发事件(executor 已经返回正确格式) if event.get("event") == "workflow_end": - + token_usage = event.get("data").get("token_usage", {}) or {} status = event.get("data", {}).get("status") if status == "completed": self.update_execution_status( execution_id, "completed", - output_data=event.get("data") + output_data=event.get("data"), + token_usage=token_usage.get("total_tokens", None) ) elif status == "failed": self.update_execution_status( From 49f6f27ffc45eee779c5a4524453a4acd9e80e69 Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Mon, 19 Jan 2026 12:24:13 +0800 Subject: [PATCH 04/14] fix(workflow): correct style of default template variable configuration --- api/app/templates/workflows/simple_qa/template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/templates/workflows/simple_qa/template.yml b/api/app/templates/workflows/simple_qa/template.yml index 14de4a73..2cf0f9b1 100644 --- a/api/app/templates/workflows/simple_qa/template.yml +++ b/api/app/templates/workflows/simple_qa/template.yml @@ -53,7 +53,7 @@ nodes: type: end name: 结束 config: - output: "{{ llm_qa.output }}" + output: "{{llm_qa.output}}" position: x: 900 y: 100 From 46752420da006922cae762de88b39bc6be615272 Mon Sep 17 00:00:00 2001 From: lixiangcheng1 Date: Mon, 19 Jan 2026 13:33:06 +0800 Subject: [PATCH 05/14] [ADD]transcribing the content of MP3 audio files into text and precisely marking the timestamps --- api/app/core/rag/llm/sequence2txt_model.py | 30 +++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/api/app/core/rag/llm/sequence2txt_model.py b/api/app/core/rag/llm/sequence2txt_model.py index be4d3649..468dda55 100644 --- a/api/app/core/rag/llm/sequence2txt_model.py +++ b/api/app/core/rag/llm/sequence2txt_model.py @@ -60,6 +60,34 @@ class QWenSeq2txt(Base): from dashscope import MultiModalConversation audio_path = f"file://{audio_path}" + prompt_ch = """ + 你是一名专业的音频转录助手,能够将MP3音频文件的内容转写为文本,并**精确标记每句话或每个段落对应的时间戳**(开始时间-结束时间)。\n + **任务要求**: + 1.输入是MP3,解析带时间戳的文本。 + 2.时间戳格式为 `[HH:MM:SS.mmm]`(毫秒可选),例如 `[00:01:23.456]`。 + 3.时间戳需尽可能贴近实际语音的起止时间,误差不超过1秒。 + 4.如果无法确定具体时间,请根据上下文合理估算。 + 5.最后总结:这段音频在说什么? + + **示例输出**: + [00:00:00.000] 今天天气真好, + [00:00:02.500] 我们一起去公园散步吧。 + [00:00:05.800] 公园里的花开得非常漂亮。 + 这段音频讲述的是一个关于**“吃水不忘挖井人”**的感人故事,主 ...""" + prompt_en = """ + You are a professional audio transcription assistant, capable of transcribing the content of MP3 audio files into text and **precisely marking the timestamps (start time - end time) corresponding to each sentence or paragraph**. + **Task requirements**: + 1. Input is MP3, parse text with timestamps. + 2. The timestamp format is `[HH:MM:SS.mmm]` (milliseconds are optional), for example, `[00:01:23.456]`. + 3. The timestamp should be as close as possible to the actual start and end time of the voice, with an error not exceeding 1 second. + 4. If a specific time cannot be determined, please make a reasonable estimation based on the context. + 5. Final summary: What is this audio talking about? + + **Example Output**: + [00:00:00.000] The weather is really nice today, + [00:00:02.500] let's go for a walk in the park together. + [00:00:05.800] The flowers in the park are blooming beautifully. + This audio tells a touching story about **"Remembering the one who dug the well when drinking water"** ..""" messages = [ { "role": "user", @@ -68,7 +96,7 @@ class QWenSeq2txt(Base): "audio": audio_path }, { - "text": "这段音频在说什么?" if self.lang.lower() == "chinese" else "What is this audio saying?", + "text": prompt_ch if self.lang.lower() == "chinese" else prompt_en, }, ], } From 6ba4b9e7bd28f8388b5aabf6523a2f5d0b36adbf Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Mon, 19 Jan 2026 15:11:57 +0800 Subject: [PATCH 06/14] fix(workflow): fix message merging in parallel states and ensure LLM node parameter validation errors are properly thrown --- api/app/core/workflow/nodes/base_node.py | 2 +- api/app/core/workflow/nodes/llm/node.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/app/core/workflow/nodes/base_node.py b/api/app/core/workflow/nodes/base_node.py index 72fd0bb5..b31213d8 100644 --- a/api/app/core/workflow/nodes/base_node.py +++ b/api/app/core/workflow/nodes/base_node.py @@ -25,7 +25,7 @@ class WorkflowState(TypedDict): The state object passed between nodes in a workflow, containing messages, variables, node outputs, etc. """ # List of messages (append mode) - messages: list[dict[str, str]] + messages: Annotated[list[dict[str, str]], lambda x, y: y] # Set of loop node IDs, used for assigning values in loop nodes cycle_nodes: list diff --git a/api/app/core/workflow/nodes/llm/node.py b/api/app/core/workflow/nodes/llm/node.py index 061a0f6a..a74e0b60 100644 --- a/api/app/core/workflow/nodes/llm/node.py +++ b/api/app/core/workflow/nodes/llm/node.py @@ -85,6 +85,7 @@ class LLMNode(BaseNode): """ # 1. 处理消息格式(优先使用 messages) + self.typed_config = LLMNodeConfig(**self.config) messages_config = self.typed_config.messages if messages_config: @@ -167,7 +168,7 @@ class LLMNode(BaseNode): Returns: LLM 响应消息 """ - self.typed_config = LLMNodeConfig(**self.config) + # self.typed_config = LLMNodeConfig(**self.config) llm, prompt_or_messages = self._prepare_llm(state, True) logger.info(f"节点 {self.node_id} 开始执行 LLM 调用(非流式)") From eb58e0ea6386efe1ecdb67f279e4e5ebe87146f9 Mon Sep 17 00:00:00 2001 From: lixiangcheng1 Date: Mon, 19 Jan 2026 15:27:54 +0800 Subject: [PATCH 07/14] [ADD]transcribing the content of MP4 video files into text and precisely marking the timestamps --- api/app/core/rag/llm/cv_model.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/api/app/core/rag/llm/cv_model.py b/api/app/core/rag/llm/cv_model.py index 24d4a35b..4207304a 100644 --- a/api/app/core/rag/llm/cv_model.py +++ b/api/app/core/rag/llm/cv_model.py @@ -243,6 +243,33 @@ class QWenCV(GptV4): tmp_path = tmp.name video_path = f"file://{tmp_path}" + prompt_ch = """ + 你是一名专业的视频转录助手,能够将视频文件的内容转写为文本,并**精确标记每句话或每个段落对应的时间戳**(开始时间-结束时间)。\n + **任务要求**: + 1.输入是MP4等视频文件,解析带时间戳的文本。 + 2.时间戳格式为 `[HH:MM:SS.mmm]`(毫秒可选),例如 `[00:01:23.456]`。 + 3.时间戳需尽可能贴近实际视频的起止时间,误差不超过1秒。 + 4.如果无法确定具体时间,请根据上下文合理估算。 + 5.最后总结:这段视频的内容是什么?,并用恰当的句子总结这个视频。 + + **示例输出**: + [00:00:00.000] 今天天气真好, + [00:00:02.500] 我们一起去公园散步吧。 + [00:00:05.800] 公园里的花开得非常漂亮。 + 这段视频的内容是关于如何在CREAMS系统中进行楼宇管理集合的编辑或删除操作。视频演示了 ...""" + prompt_en = """ + You are a professional video transcription assistant, capable of transcribing the content of video files into text and **precisely marking the timestamp (start time-end time) corresponding to each sentence or paragraph**. + **Task requirements**: + 1. Input is MP4 or other video files, and parse the text with timestamps. + 2. The timestamp format is `[HH:MM:SS.mmm]` (milliseconds are optional), for example, `[00:01:23.456]`. + 3. The timestamp should be as close as possible to the actual start and end time of the video, with an error not exceeding 1 second. + 4. If the specific time cannot be determined, please make a reasonable estimation based on the context. + 5. Final summary: What is the content of this video? Summarize this video in an appropriate sentence. + + **Example output**: + [00:00:00.000] The weather is really nice today, [00:00:02.500] let's go for a walk in the park together. + [00:00:05.800] The flowers in the park are blooming beautifully. + The content of this video is about how to edit or delete building management collections in the CREAMS system. The video demonstrates ..""" messages = [ { "role": "user", @@ -252,7 +279,7 @@ class QWenCV(GptV4): "fps": 2, }, { - "text": "视频的内容是什么?,并且,请用恰当的句子总结这个视频。" if self.lang.lower() == "chinese" else "What is the content of the video? And please summarize this video in proper sentences.", + "text": prompt_ch if self.lang.lower() == "chinese" else prompt_en, }, ], } From 9353053a238dd57f191ddceb273dc7b0d7b5441b Mon Sep 17 00:00:00 2001 From: zhaoying Date: Mon, 19 Jan 2026 14:08:31 +0800 Subject: [PATCH 08/14] feat(web): extract and replace Switch Form components --- web/src/components/FormItem/DescWrapper.tsx | 12 +++++ web/src/components/FormItem/LabelWrapper.tsx | 13 ++++++ .../components/FormItem/SwitchFormItem.tsx | 45 +++++++++++++++++++ web/src/views/EmotionEngine/index.tsx | 25 +++++------ web/src/views/ForgettingEngine/index.tsx | 31 ++++--------- .../views/MemoryExtractionEngine/index.tsx | 22 ++++----- web/src/views/SelfReflectionEngine/index.tsx | 24 +++++----- 7 files changed, 106 insertions(+), 66 deletions(-) create mode 100644 web/src/components/FormItem/DescWrapper.tsx create mode 100644 web/src/components/FormItem/LabelWrapper.tsx create mode 100644 web/src/components/FormItem/SwitchFormItem.tsx diff --git a/web/src/components/FormItem/DescWrapper.tsx b/web/src/components/FormItem/DescWrapper.tsx new file mode 100644 index 00000000..300fc2b6 --- /dev/null +++ b/web/src/components/FormItem/DescWrapper.tsx @@ -0,0 +1,12 @@ +import clsx from "clsx"; +import type { FC, ReactNode } from "react"; + +const DescWrapper: FC<{desc: string | ReactNode, className?: string}> = ({desc, className}) => { + return ( +
+ {desc} +
+ ) +} + +export default DescWrapper \ No newline at end of file diff --git a/web/src/components/FormItem/LabelWrapper.tsx b/web/src/components/FormItem/LabelWrapper.tsx new file mode 100644 index 00000000..461250d8 --- /dev/null +++ b/web/src/components/FormItem/LabelWrapper.tsx @@ -0,0 +1,13 @@ +import clsx from "clsx"; +import type { FC, ReactNode } from "react"; + +const LabelWrapper: FC<{ title: string | ReactNode, className?: string; children?: ReactNode}> = ({title, className, children}) => { + return ( +
+
{title}
+ {children} +
+ ) +} + +export default LabelWrapper \ No newline at end of file diff --git a/web/src/components/FormItem/SwitchFormItem.tsx b/web/src/components/FormItem/SwitchFormItem.tsx new file mode 100644 index 00000000..e17a8728 --- /dev/null +++ b/web/src/components/FormItem/SwitchFormItem.tsx @@ -0,0 +1,45 @@ +import { Switch, Form, ConfigProvider } from "antd"; +import useSize from 'antd/lib/config-provider/hooks/useSize' +import type { FC, ReactNode } from "react"; +import { useContext } from "react"; + +import LabelWrapper from './LabelWrapper' +import DescWrapper from './DescWrapper' + +interface SwitchFormItemProps { + title: string | ReactNode; + desc?: string | ReactNode; + name: string | string[]; + size?: 'small' | 'default' + className?: string; + disabled?: boolean; +} + +const SwitchFormItem: FC = ({ + title, + desc, + name, + size = 'default', + className, + disabled +}) => { + const componentSize = useSize() + console.log('componentSize', componentSize) + + return ( +
+ + {desc && } + + + + +
+ ) +} + +export default SwitchFormItem \ No newline at end of file diff --git a/web/src/views/EmotionEngine/index.tsx b/web/src/views/EmotionEngine/index.tsx index ae1cd4c6..73bfd376 100644 --- a/web/src/views/EmotionEngine/index.tsx +++ b/web/src/views/EmotionEngine/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Row, Col, Form, Slider, Button, Alert, message, Switch, Space } from 'antd'; +import { Row, Col, Form, Slider, Button, Alert, message, Space } from 'antd'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import RbCard from '@/components/RbCard/Card'; @@ -9,6 +9,7 @@ import type { ConfigForm } from './types' import CustomSelect from '@/components/CustomSelect'; import { getModelListUrl } from '@/api/models' import Tag from '@/components/Tag' +import SwitchFormItem from '@/components/FormItem/SwitchFormItem' const configList = [ { @@ -158,23 +159,17 @@ const EmotionEngine: React.FC = () => { ) } - return ( -
-
- {t(`emotionEngine.${config.key}`)} + {config.hasSubTitle &&
{t(`emotionEngine.${config.key}_subTitle`)}
}
{t(`emotionEngine.${config.key}_desc`)}
-
- - - -
+ } + className="rb:mb-6" + disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'} + /> ) })} diff --git a/web/src/views/ForgettingEngine/index.tsx b/web/src/views/ForgettingEngine/index.tsx index fed71da1..05745b4b 100644 --- a/web/src/views/ForgettingEngine/index.tsx +++ b/web/src/views/ForgettingEngine/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Row, Col, Form, Slider, Button, Space, message, Switch } from 'antd'; +import { Row, Col, Form, Slider, Button, Space, message } from 'antd'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import RbCard from '@/components/RbCard/Card'; @@ -7,6 +7,7 @@ import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimula import LineChart from './components/LineChart' import { getMemoryForgetConfig, updateMemoryForgetConfig } from '@/api/memory' import type { ConfigForm } from './types' +import SwitchFormItem from '@/components/FormItem/SwitchFormItem' const configList = [ { @@ -155,26 +156,12 @@ const ForgettingEngine: React.FC = () => { {configList.map(config => { if (config.type === 'button') { return ( -
-
-
- {t(`forgettingEngine.${config.key}`)} -
- - - -
-
- - {config.range && {t(`forgettingEngine.range`)}: {config.range?.join('-')}} - {config.type && {t(`forgettingEngine.type`)}: {config.type}} - -
-
+ {t(`forgettingEngine.type`)}: {config.type}} + className="rb:mb-2" + /> ) } return ( @@ -191,8 +178,6 @@ const ForgettingEngine: React.FC = () => { > {config.type === 'decimal' ? - : config.type === 'button' - ? : null } diff --git a/web/src/views/MemoryExtractionEngine/index.tsx b/web/src/views/MemoryExtractionEngine/index.tsx index adade5da..3d67270c 100644 --- a/web/src/views/MemoryExtractionEngine/index.tsx +++ b/web/src/views/MemoryExtractionEngine/index.tsx @@ -11,6 +11,7 @@ import { getModelList } from '@/api/models'; import type { Model } from '@/views/ModelManagement/types' import { configList } from './constant' import Result from './components/Result' +import SwitchFormItem from '@/components/FormItem/SwitchFormItem' const keys = [ // 'example', @@ -173,25 +174,18 @@ const MemoryExtractionEngine: FC = () => { } )} > -
{t(`memoryExtractionEngine.${vo.title}`)}
+
{t(`memoryExtractionEngine.${vo.title}`)}
{t(`memoryExtractionEngine.${vo.title}SubTitle`)}
{vo.list.map(config => (
{config.control === 'button' && -
-
- -{t(`memoryExtractionEngine.${config.label}`)} - -
- - - -
+ -{t(`memoryExtractionEngine.${config.label}`)}} + name={config.variableName} + desc={} + className="rb:mt-6" + /> } {config.control === 'select' && <> diff --git a/web/src/views/SelfReflectionEngine/index.tsx b/web/src/views/SelfReflectionEngine/index.tsx index 952450b2..784f066c 100644 --- a/web/src/views/SelfReflectionEngine/index.tsx +++ b/web/src/views/SelfReflectionEngine/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Row, Col, Form, App, Button, Switch, Space, Select } from 'antd'; +import { Row, Col, Form, App, Button, Space, Select } from 'antd'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -11,6 +11,7 @@ import CustomSelect from '@/components/CustomSelect'; import { getModelListUrl } from '@/api/models' import Tag from '@/components/Tag' import { useI18n } from '@/store/locale'; +import SwitchFormItem from '@/components/FormItem/SwitchFormItem' const configList = [ // 启用反思引擎 @@ -219,21 +220,16 @@ const SelfReflectionEngine: React.FC = () => { } return ( -
-
- {t(`reflectionEngine.${config.key}`)} + {(config as any).hasSubTitle &&
{t(`reflectionEngine.${config.key}_subTitle`)}
}
{t(`reflectionEngine.${config.key}_desc`)}
-
- - - -
+ } + className="rb:mb-6" + disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'} + /> ) })} From 2891f2c0680d4c0becb6d806f36881dbe2acfd47 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Mon, 19 Jan 2026 14:10:29 +0800 Subject: [PATCH 09/14] feat(web): markdown support copy --- web/src/components/Markdown/index.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/src/components/Markdown/index.tsx b/web/src/components/Markdown/index.tsx index d16b72e4..58650207 100644 --- a/web/src/components/Markdown/index.tsx +++ b/web/src/components/Markdown/index.tsx @@ -150,9 +150,19 @@ const RbMarkdown: FC = ({ ) } + // 处理键盘快捷键 + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'c') { + const selection = window.getSelection() + if (selection && selection.toString()) { + navigator.clipboard.writeText(selection.toString()) + } + } + } + // 预览模式 return ( -
+