feat(workflow): enhance HTTP request node with curl debugging support
- Augment HTTP request node capabilities and add generated curl commands for easier debugging. feat(log): implement workflow execution logs and search functionality - Add detailed logging for workflow node execution and enable search capabilities within application logs. feat(auth): introduce middleware to verify application publication status - Add a check to ensure the application is published before allowing access. fix(converter): rectify variable handling logic in Dify converter - Correct issues related to processing variables within the Dify converter module. refactor(model): remove quota check decorator from model update operations - Decouple quota validation from the model update process to streamline the logic.
This commit is contained in:
@@ -19,6 +19,7 @@ from app.core.exceptions import (
|
||||
)
|
||||
from app.core.error_codes import BizCode
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.models.app_model import App
|
||||
|
||||
logger = get_business_logger()
|
||||
|
||||
@@ -442,6 +443,17 @@ class ApiKeyAuthService:
|
||||
|
||||
return api_key_obj
|
||||
|
||||
@staticmethod
|
||||
def check_app_published(db: Session, api_key_obj: ApiKey) -> None:
|
||||
"""
|
||||
检查应用是否已发布,未发布则抛出异常
|
||||
"""
|
||||
if not api_key_obj.resource_id:
|
||||
return
|
||||
app = db.get(App, api_key_obj.resource_id)
|
||||
if not app or not app.current_release_id:
|
||||
raise BusinessException("应用未发布,不可用", BizCode.APP_NOT_PUBLISHED)
|
||||
|
||||
@staticmethod
|
||||
def check_scope(api_key: ApiKey, required_scope: str) -> bool:
|
||||
"""检查权限范围"""
|
||||
|
||||
@@ -3,11 +3,14 @@ import uuid
|
||||
from typing import Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.models.conversation_model import Conversation, Message
|
||||
from app.models.workflow_model import WorkflowExecution
|
||||
from app.repositories.conversation_repository import ConversationRepository, MessageRepository
|
||||
from app.schemas.app_log_schema import AppLogNodeExecution
|
||||
|
||||
logger = get_business_logger()
|
||||
|
||||
@@ -26,7 +29,8 @@ class AppLogService:
|
||||
workspace_id: uuid.UUID,
|
||||
page: int = 1,
|
||||
pagesize: int = 20,
|
||||
is_draft: Optional[bool] = None,
|
||||
is_draft: bool = False,
|
||||
keyword: Optional[str] = None,
|
||||
) -> Tuple[list[Conversation], int]:
|
||||
"""
|
||||
查询应用日志会话列表
|
||||
@@ -36,7 +40,8 @@ class AppLogService:
|
||||
workspace_id: 工作空间 ID
|
||||
page: 页码(从 1 开始)
|
||||
pagesize: 每页数量
|
||||
is_draft: 是否草稿会话(None 表示不过滤)
|
||||
is_draft: 是否草稿会话(默认False,即发布会话)
|
||||
keyword: 搜索关键词(匹配消息内容)
|
||||
|
||||
Returns:
|
||||
Tuple[list[Conversation], int]: (会话列表,总数)
|
||||
@@ -48,7 +53,8 @@ class AppLogService:
|
||||
"workspace_id": str(workspace_id),
|
||||
"page": page,
|
||||
"pagesize": pagesize,
|
||||
"is_draft": is_draft
|
||||
"is_draft": is_draft,
|
||||
"keyword": keyword
|
||||
}
|
||||
)
|
||||
|
||||
@@ -57,6 +63,7 @@ class AppLogService:
|
||||
app_id=app_id,
|
||||
workspace_id=workspace_id,
|
||||
is_draft=is_draft,
|
||||
keyword=keyword,
|
||||
page=page,
|
||||
pagesize=pagesize
|
||||
)
|
||||
@@ -77,9 +84,9 @@ class AppLogService:
|
||||
app_id: uuid.UUID,
|
||||
conversation_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID
|
||||
) -> Conversation:
|
||||
) -> Tuple[Conversation, dict[str, list[AppLogNodeExecution]]]:
|
||||
"""
|
||||
查询会话详情(包含消息)
|
||||
查询会话详情(包含消息和工作流节点执行记录)
|
||||
|
||||
Args:
|
||||
app_id: 应用 ID
|
||||
@@ -87,7 +94,8 @@ class AppLogService:
|
||||
workspace_id: 工作空间 ID
|
||||
|
||||
Returns:
|
||||
Conversation: 包含消息的会话对象
|
||||
Tuple[Conversation, dict[str, list[AppLogNodeExecution]]]:
|
||||
(包含消息的会话对象, 按消息ID分组的节点执行记录)
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: 当会话不存在时
|
||||
@@ -116,13 +124,116 @@ class AppLogService:
|
||||
# 将消息附加到会话对象
|
||||
conversation.messages = messages
|
||||
|
||||
# 查询工作流节点执行记录(按消息分组)
|
||||
_, node_executions_map = self._get_workflow_node_executions_with_map(
|
||||
conversation_id, messages
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"查询应用日志会话详情成功",
|
||||
extra={
|
||||
"app_id": str(app_id),
|
||||
"conversation_id": str(conversation_id),
|
||||
"message_count": len(messages)
|
||||
"message_count": len(messages),
|
||||
"message_with_nodes_count": len(node_executions_map)
|
||||
}
|
||||
)
|
||||
|
||||
return conversation
|
||||
return conversation, node_executions_map
|
||||
|
||||
def _get_workflow_node_executions_with_map(
|
||||
self,
|
||||
conversation_id: uuid.UUID,
|
||||
messages: list[Message]
|
||||
) -> Tuple[list[AppLogNodeExecution], dict[str, list[AppLogNodeExecution]]]:
|
||||
"""
|
||||
从 workflow_executions 表中提取节点执行记录,并按 assistant message 分组
|
||||
|
||||
Args:
|
||||
conversation_id: 会话 ID
|
||||
messages: 消息列表
|
||||
|
||||
Returns:
|
||||
Tuple[list[AppLogNodeExecution], dict[str, list[AppLogNodeExecution]]]:
|
||||
(所有节点执行记录列表, 按 message_id 分组的节点执行记录字典)
|
||||
"""
|
||||
node_executions = []
|
||||
node_executions_map: dict[str, list[AppLogNodeExecution]] = {}
|
||||
|
||||
# 查询该会话关联的所有工作流执行记录(按时间正序)
|
||||
stmt = select(WorkflowExecution).where(
|
||||
WorkflowExecution.conversation_id == conversation_id,
|
||||
WorkflowExecution.status == "completed"
|
||||
).order_by(WorkflowExecution.started_at.asc())
|
||||
|
||||
executions = self.db.scalars(stmt).all()
|
||||
|
||||
logger.info(
|
||||
f"查询到 {len(executions)} 条工作流执行记录",
|
||||
extra={
|
||||
"conversation_id": str(conversation_id),
|
||||
"execution_count": len(executions),
|
||||
"execution_ids": [str(e.id) for e in executions]
|
||||
}
|
||||
)
|
||||
|
||||
# 筛选出 workflow 执行产生的 assistant 消息(排除开场白)
|
||||
# workflow 结果的 meta_data 包含 usage,而开场白包含 suggested_questions
|
||||
assistant_messages = [
|
||||
m for m in messages
|
||||
if m.role == "assistant" and m.meta_data and "usage" in m.meta_data
|
||||
]
|
||||
|
||||
# 通过时序匹配,将 execution 和 assistant message 关联
|
||||
used_message_ids: set[str] = set()
|
||||
|
||||
for execution in executions:
|
||||
if not execution.output_data:
|
||||
continue
|
||||
|
||||
# 找到该 execution 对应的 assistant message
|
||||
# 逻辑:找 execution.started_at 之后最近的、未使用的 assistant message
|
||||
best_msg = None
|
||||
best_dt = None
|
||||
for msg in assistant_messages:
|
||||
msg_id_str = str(msg.id)
|
||||
if msg_id_str in used_message_ids:
|
||||
continue
|
||||
if msg.created_at and msg.created_at >= execution.started_at:
|
||||
dt = (msg.created_at - execution.started_at).total_seconds()
|
||||
if best_dt is None or dt < best_dt:
|
||||
best_dt = dt
|
||||
best_msg = msg
|
||||
|
||||
if not best_msg:
|
||||
continue
|
||||
|
||||
msg_id_str = str(best_msg.id)
|
||||
used_message_ids.add(msg_id_str)
|
||||
|
||||
# 提取节点输出
|
||||
output_data = execution.output_data
|
||||
if isinstance(output_data, dict):
|
||||
node_outputs = output_data.get("node_outputs", {})
|
||||
execution_nodes = []
|
||||
for node_id, node_data in node_outputs.items():
|
||||
if not isinstance(node_data, dict):
|
||||
continue
|
||||
node_execution = AppLogNodeExecution(
|
||||
node_id=node_data.get("node_id", node_id),
|
||||
node_type=node_data.get("node_type", "unknown"),
|
||||
node_name=node_data.get("node_name"),
|
||||
status=node_data.get("status", "unknown"),
|
||||
error=node_data.get("error"),
|
||||
input=node_data.get("input"),
|
||||
output=node_data.get("output"),
|
||||
elapsed_time=node_data.get("elapsed_time"),
|
||||
token_usage=node_data.get("token_usage"),
|
||||
)
|
||||
node_executions.append(node_execution)
|
||||
execution_nodes.append(node_execution)
|
||||
|
||||
# 将节点记录关联到 message_id
|
||||
node_executions_map[msg_id_str] = execution_nodes
|
||||
|
||||
return node_executions, node_executions_map
|
||||
|
||||
@@ -14,6 +14,7 @@ from app.core.exceptions import BusinessException
|
||||
from app.core.workflow.adapters.base_adapter import WorkflowImportResult, WorkflowParserResult
|
||||
from app.core.workflow.adapters.errors import UnsupportedPlatform, InvalidConfiguration
|
||||
from app.core.workflow.adapters.registry import PlatformAdapterRegistry
|
||||
from app.models.app_model import AppType
|
||||
from app.schemas import AppCreate
|
||||
from app.schemas.workflow_schema import WorkflowConfigCreate
|
||||
from app.services.app_service import AppService
|
||||
@@ -86,11 +87,12 @@ class WorkflowImportService:
|
||||
if config is None:
|
||||
raise BusinessException("Configuration import timed out. Please try again.")
|
||||
config = json.loads(config)
|
||||
unique_name = self.app_service._unique_app_name(name, workspace_id, AppType.WORKFLOW)
|
||||
app = self.app_service.create_app(
|
||||
user_id=user_id,
|
||||
workspace_id=workspace_id,
|
||||
data=AppCreate(
|
||||
name=name,
|
||||
name=unique_name,
|
||||
description=description,
|
||||
type="workflow",
|
||||
workflow_config=WorkflowConfigCreate(
|
||||
|
||||
Reference in New Issue
Block a user