feat(workflow): support single-node execution and MCP Streamable HTTP protocol

- Add `run_single_node` method in workflow service for isolated node execution
- Refactor MCP client to support Streamable HTTP protocol (2025-03-26) with session ID handling, SSE/JSON response parsing, and proper initialized notification
- Update iteration node to conditionally initialize stream writer based on stream flag
- Improve cycle graph node invocation with checkpoint config passing
This commit is contained in:
Timebomb2018
2026-05-07 17:18:21 +08:00
parent 595c3517e3
commit 8d3da2fd0e
7 changed files with 302 additions and 28 deletions

View File

@@ -1,5 +1,6 @@
import uuid
import io
import json
from typing import Optional, Annotated
import yaml
@@ -1068,6 +1069,62 @@ async def draft_run_compare(
return success(data=app_schema.DraftRunCompareResponse(**result))
@router.post("/{app_id}/workflow/nodes/{node_id}/run", summary="单节点试运行")
@cur_workspace_access_guard()
async def run_single_workflow_node(
app_id: uuid.UUID,
node_id: str,
payload: app_schema.NodeRunRequest,
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
workflow_service: Annotated[WorkflowService, Depends(get_workflow_service)] = None,
):
"""单独执行工作流中的某个节点
inputs 支持以下 key 格式:
- 节点变量: "node_id.var_name"
- 系统变量: "sys.message""sys.files"
"""
workspace_id = current_user.current_workspace_id
config = workflow_service.check_config(app_id)
raw_inputs = payload.inputs or {}
input_data = {
"message": raw_inputs.pop("sys.message", ""),
"files": raw_inputs.pop("sys.files", []),
"user_id": raw_inputs.pop("sys.user_id", str(current_user.id)),
"inputs": raw_inputs,
"conversation_id": "",
"conv_messages": [],
}
if payload.stream:
async def event_generator():
async for event in workflow_service.run_single_node_stream(
app_id=app_id,
node_id=node_id,
config=config,
workspace_id=workspace_id,
input_data=input_data,
):
yield f"event: {event['event']}\ndata: {json.dumps(event['data'], ensure_ascii=False)}\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"}
)
result = await workflow_service.run_single_node(
app_id=app_id,
node_id=node_id,
config=config,
workspace_id=workspace_id,
input_data=input_data,
)
return success(data=result)
@router.get("/{app_id}/workflow")
@cur_workspace_access_guard()
async def get_workflow_config(