You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
655 lines
28 KiB
655 lines
28 KiB
import uuid
|
|
import time
|
|
import json
|
|
import hashlib
|
|
import secrets
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Query
|
|
from fastapi.responses import StreamingResponse
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from database import get_db
|
|
from models import FlowDefinition, FlowVersion, FlowApiKey, FlowExecution, User
|
|
from schemas import (
|
|
FlowDefinitionCreate, FlowDefinitionUpdate, FlowDefinitionOut,
|
|
FlowVersionOut, FlowApiKeyCreate, FlowApiKeyOut,
|
|
FlowExecuteRequest, FlowChatMessageRequest,
|
|
)
|
|
from modules.flow_engine.engine import FlowEngine, ToolNodeAgent
|
|
from agentscope.message import Msg
|
|
from dependencies import get_current_user
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/flow", tags=["flow"])
|
|
|
|
|
|
def _build_flow_out(f) -> FlowDefinitionOut:
|
|
return FlowDefinitionOut(
|
|
id=f.id, name=f.name, description=f.description,
|
|
version=f.version, status=f.status,
|
|
definition_json=f.definition_json,
|
|
published_version_id=f.published_version_id,
|
|
published_to_wecom=f.published_to_wecom,
|
|
published_to_web=f.published_to_web,
|
|
created_at=f.created_at, updated_at=f.updated_at,
|
|
)
|
|
|
|
|
|
# ============================== CRUD ==============================
|
|
|
|
|
|
@router.get("/definitions", response_model=list[FlowDefinitionOut])
|
|
async def list_flows(request: Request, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(FlowDefinition).order_by(FlowDefinition.updated_at.desc())
|
|
)
|
|
return [_build_flow_out(f) for f in result.scalars().all()]
|
|
|
|
|
|
@router.get("/definitions/{flow_id}", response_model=FlowDefinitionOut)
|
|
async def get_flow(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
return _build_flow_out(f)
|
|
|
|
|
|
@router.post("/definitions", response_model=FlowDefinitionOut)
|
|
async def create_flow(req: FlowDefinitionCreate, request: Request, db: AsyncSession = Depends(get_db)):
|
|
user_ctx = request.state.user
|
|
definition_json = {
|
|
"nodes": [n.model_dump() for n in req.nodes],
|
|
"edges": [e.model_dump() for e in req.edges],
|
|
"trigger": req.trigger,
|
|
}
|
|
flow = FlowDefinition(
|
|
name=req.name,
|
|
description=req.description,
|
|
definition_json=definition_json,
|
|
draft_definition_json=definition_json,
|
|
creator_id=uuid.UUID(user_ctx["id"]),
|
|
)
|
|
db.add(flow)
|
|
await db.flush()
|
|
return _build_flow_out(flow)
|
|
|
|
|
|
@router.put("/definitions/{flow_id}", response_model=FlowDefinitionOut)
|
|
async def update_flow(flow_id: uuid.UUID, req: FlowDefinitionUpdate, request: Request, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
if req.name is not None:
|
|
f.name = req.name
|
|
if req.description is not None:
|
|
f.description = req.description
|
|
if req.nodes is not None and req.edges is not None:
|
|
new_def = {
|
|
"nodes": [n.model_dump() for n in req.nodes],
|
|
"edges": [e.model_dump() for e in req.edges],
|
|
"trigger": req.trigger or f.definition_json.get("trigger", {}),
|
|
}
|
|
f.version += 1
|
|
f.draft_definition_json = new_def
|
|
f.definition_json = new_def
|
|
return _build_flow_out(f)
|
|
|
|
|
|
@router.delete("/definitions/{flow_id}")
|
|
async def delete_flow(flow_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
await db.delete(f)
|
|
return {"code": 200, "message": "已删除"}
|
|
|
|
|
|
# ============================== 发布 (创建快照) ==============================
|
|
|
|
|
|
async def _snapshot_publish(flow: FlowDefinition, db: AsyncSession, user_id: str,
|
|
publish_wecom: bool = False, publish_web: bool = False, changelog: str = ""):
|
|
new_version = FlowVersion(
|
|
flow_id=flow.id,
|
|
version=flow.version,
|
|
definition_json=json.loads(json.dumps(flow.definition_json)),
|
|
changelog=changelog,
|
|
published_by=uuid.UUID(user_id),
|
|
published_to_wecom=publish_wecom,
|
|
published_to_web=publish_web,
|
|
)
|
|
db.add(new_version)
|
|
await db.flush()
|
|
|
|
flow.published_version_id = new_version.id
|
|
flow.status = "published"
|
|
if publish_wecom:
|
|
flow.published_to_wecom = True
|
|
if publish_web:
|
|
flow.published_to_web = True
|
|
return new_version
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/publish")
|
|
async def publish_flow(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
nodes = f.definition_json.get("nodes", [])
|
|
if not nodes:
|
|
raise HTTPException(400, "流定义中没有节点")
|
|
user_ctx = request.state.user
|
|
await _snapshot_publish(f, db, user_ctx["id"], publish_wecom=True)
|
|
return {"code": 200, "message": "流已上架到企微", "data": {"status": "published", "version": f.version}}
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/publish-web")
|
|
async def publish_flow_to_web(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
user_ctx = request.state.user
|
|
prev_version_id = f.published_version_id
|
|
await _snapshot_publish(f, db, user_ctx["id"], publish_web=True)
|
|
return {"code": 200, "message": "流已上架到网页", "data": {"status": "published", "version": f.version}}
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/unpublish")
|
|
async def unpublish_flow(flow_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
f.status = "draft"
|
|
f.published_to_wecom = False
|
|
f.published_version_id = None
|
|
return {"code": 200, "message": "流已下架"}
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/unpublish-web")
|
|
async def unpublish_flow_from_web(flow_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
f.published_to_web = False
|
|
if not f.published_to_wecom:
|
|
f.status = "draft"
|
|
f.published_version_id = None
|
|
return {"code": 200, "message": "流已从网页下架"}
|
|
|
|
|
|
# ============================== 版本管理 ==============================
|
|
|
|
|
|
@router.get("/definitions/{flow_id}/versions", response_model=list[FlowVersionOut])
|
|
async def list_versions(flow_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(FlowVersion)
|
|
.where(FlowVersion.flow_id == flow_id)
|
|
.order_by(FlowVersion.version.desc())
|
|
.limit(50)
|
|
)
|
|
return [FlowVersionOut(
|
|
id=v.id, flow_id=v.flow_id, version=v.version,
|
|
definition_json=v.definition_json, changelog=v.changelog or "",
|
|
published_to_wecom=v.published_to_wecom, published_to_web=v.published_to_web,
|
|
published_by=v.published_by, created_at=v.created_at,
|
|
) for v in result.scalars().all()]
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/rollback/{version_id}")
|
|
async def rollback_flow(flow_id: uuid.UUID, version_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
target = await db.get(FlowVersion, version_id)
|
|
if not target or str(target.flow_id) != str(flow_id):
|
|
raise HTTPException(404, "版本不存在")
|
|
|
|
f.definition_json = json.loads(json.dumps(target.definition_json))
|
|
f.draft_definition_json = f.definition_json
|
|
f.published_version_id = target.id
|
|
f.published_to_wecom = target.published_to_wecom
|
|
f.published_to_web = target.published_to_web
|
|
f.status = "published" if (target.published_to_wecom or target.published_to_web) else "draft"
|
|
f.version = target.version
|
|
return {"code": 200, "message": f"已回滚到版本 v{target.version}", "data": {"version": target.version}}
|
|
|
|
|
|
# ============================== 执行 (加版本快照) ==============================
|
|
|
|
|
|
def _get_definition_json(flow: FlowDefinition, db_session) -> dict:
|
|
"""优先加载 published_version 快照,不存在则使用当前 definition_json"""
|
|
return flow.definition_json
|
|
|
|
|
|
async def _get_published_definition(flow: FlowDefinition, db: AsyncSession) -> dict:
|
|
if flow.published_version_id:
|
|
result = await db.execute(select(FlowVersion).where(FlowVersion.id == flow.published_version_id))
|
|
published = result.scalar_one_or_none()
|
|
if published:
|
|
return json.loads(json.dumps(published.definition_json))
|
|
return flow.definition_json
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/execute")
|
|
async def execute_flow(flow_id: uuid.UUID, request: Request, payload: dict, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
|
|
user_ctx = request.state.user
|
|
input_text = payload.get("input", payload.get("message", ""))
|
|
|
|
definition = await _get_published_definition(f, db)
|
|
await ToolNodeAgent.load_custom_tools(db)
|
|
engine = FlowEngine(definition)
|
|
input_msg = Msg(name="user", content=input_text, role="user")
|
|
context = {
|
|
"user_id": user_ctx["id"],
|
|
"username": user_ctx.get("username", ""),
|
|
"trigger_data": payload.get("trigger", {}),
|
|
"_node_results": {},
|
|
}
|
|
|
|
start_time = time.time()
|
|
try:
|
|
result_msg = await engine.execute(input_msg, context)
|
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
|
output_text = result_msg.get_text_content() if hasattr(result_msg, 'get_text_content') else str(result_msg)
|
|
|
|
execution = FlowExecution(
|
|
flow_id=f.id,
|
|
version=_get_published_version_number(f),
|
|
trigger_type=payload.get("trigger_type", "manual"),
|
|
trigger_user_id=uuid.UUID(user_ctx["id"]),
|
|
input_data={"input": input_text},
|
|
output_data={"output": output_text},
|
|
status="completed",
|
|
latency_ms=elapsed_ms,
|
|
finished_at=datetime.utcnow(),
|
|
)
|
|
db.add(execution)
|
|
return {
|
|
"code": 200,
|
|
"data": {
|
|
"output": output_text,
|
|
"node_results": context.get("_node_results", {}),
|
|
"execution_id": str(execution.id),
|
|
"latency_ms": elapsed_ms,
|
|
},
|
|
}
|
|
except Exception as e:
|
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
|
execution = FlowExecution(
|
|
flow_id=f.id,
|
|
version=_get_published_version_number(f),
|
|
trigger_type="manual",
|
|
trigger_user_id=uuid.UUID(user_ctx["id"]),
|
|
input_data={"input": input_text},
|
|
status="failed",
|
|
latency_ms=elapsed_ms,
|
|
error_message=str(e)[:2000],
|
|
finished_at=datetime.utcnow(),
|
|
)
|
|
db.add(execution)
|
|
raise HTTPException(500, f"流执行失败: {str(e)}")
|
|
|
|
|
|
def _get_published_version_number(flow: FlowDefinition) -> int | None:
|
|
return flow.version
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/test")
|
|
async def test_flow(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
|
|
definition = await _get_published_definition(f, db)
|
|
nodes = definition.get("nodes", [])
|
|
edges = definition.get("edges", [])
|
|
|
|
validation = {"valid": True, "node_count": len(nodes), "edge_count": len(edges),
|
|
"node_types": list(set(n.get("type", "unknown") for n in nodes)), "issues": []}
|
|
node_ids = {n["id"] for n in nodes}
|
|
for edge in edges:
|
|
s = edge.get("source") or edge.get("from")
|
|
t = edge.get("target") or edge.get("to")
|
|
if s and s not in node_ids:
|
|
validation["issues"].append(f"边源节点 {s} 不存在")
|
|
if t and t not in node_ids:
|
|
validation["issues"].append(f"边目标节点 {t} 不存在")
|
|
if validation["issues"]:
|
|
validation["valid"] = False
|
|
if not any(n.get("type") == "trigger" for n in nodes):
|
|
validation["issues"].append("流缺少触发节点")
|
|
return {"code": 200, "data": validation}
|
|
|
|
|
|
# ============================== SSE 流式执行 ==============================
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/stream")
|
|
async def execute_flow_stream(flow_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
|
|
body = await request.json()
|
|
input_text = body.get("input", body.get("message", ""))
|
|
user_ctx = request.state.user
|
|
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
|
|
definition = await _get_published_definition(f, db)
|
|
await ToolNodeAgent.load_custom_tools(db)
|
|
|
|
async def event_generator():
|
|
import asyncio
|
|
engine = FlowEngine(definition)
|
|
context = {
|
|
"user_id": user_ctx["id"],
|
|
"username": user_ctx.get("username", ""),
|
|
"trigger_data": body.get("trigger", {}),
|
|
"_node_results": {},
|
|
"_stream_callback": None,
|
|
}
|
|
input_msg = Msg(name="user", content=input_text, role="user")
|
|
start_time = time.time()
|
|
|
|
# bind stream callback
|
|
stream_chunks = []
|
|
|
|
async def stream_callback(event_type: str, data: dict):
|
|
chunk_data = json.dumps({"event": event_type, "data": data}, ensure_ascii=False)
|
|
stream_chunks.append(f"data: {chunk_data}\n\n")
|
|
|
|
context["_stream_callback"] = stream_callback
|
|
|
|
try:
|
|
yield f"data: {json.dumps({'event': 'workflow_started', 'data': {'flow_id': str(flow_id)}}, ensure_ascii=False)}\n\n"
|
|
|
|
# get execution order
|
|
graph = engine._build_graph()
|
|
start = engine._find_start_nodes(graph)
|
|
if start:
|
|
yield f"data: {json.dumps({'event': 'node_started', 'data': {'node_id': start[0], 'node_type': definition.get('nodes', [{}])[0].get('type', 'unknown'), 'label': definition.get('nodes', [{}])[0].get('label', '开始')}}, ensure_ascii=False)}\n\n"
|
|
|
|
result_msg = await asyncio.wait_for(
|
|
engine.execute(input_msg, context),
|
|
timeout=engine.FLOW_TIMEOUT_SECONDS,
|
|
)
|
|
output_text = result_msg.get_text_content() if hasattr(result_msg, 'get_text_content') else str(result_msg)
|
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
|
|
|
yield f"data: {json.dumps({'event': 'text_chunk', 'data': {'content': output_text}}, ensure_ascii=False)}\n\n"
|
|
|
|
yield f"data: {json.dumps({'event': 'workflow_finished', 'data': {'output': output_text, 'node_results': {k: str(v)[:200] for k, v in context.get('_node_results', {}).items()}, 'latency_ms': elapsed_ms}}, ensure_ascii=False)}\n\n"
|
|
|
|
execution = FlowExecution(
|
|
flow_id=f.id,
|
|
version=_get_published_version_number(f),
|
|
trigger_type=body.get("trigger_type", "manual"),
|
|
trigger_user_id=uuid.UUID(user_ctx["id"]),
|
|
input_data={"input": input_text},
|
|
output_data={"output": output_text},
|
|
status="completed",
|
|
latency_ms=elapsed_ms,
|
|
finished_at=datetime.utcnow(),
|
|
)
|
|
db.add(execution)
|
|
except asyncio.TimeoutError:
|
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
|
yield f"data: {json.dumps({'event': 'error', 'data': {'message': '执行超时'}}, ensure_ascii=False)}\n\n"
|
|
execution = FlowExecution(
|
|
flow_id=f.id,
|
|
version=_get_published_version_number(f),
|
|
trigger_type="manual",
|
|
trigger_user_id=uuid.UUID(user_ctx["id"]),
|
|
input_data={"input": input_text},
|
|
status="failed",
|
|
latency_ms=elapsed_ms,
|
|
error_message="执行超时",
|
|
finished_at=datetime.utcnow(),
|
|
)
|
|
db.add(execution)
|
|
except Exception as e:
|
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
|
yield f"data: {json.dumps({'event': 'error', 'data': {'message': str(e)}}, ensure_ascii=False)}\n\n"
|
|
execution = FlowExecution(
|
|
flow_id=f.id,
|
|
version=_get_published_version_number(f),
|
|
trigger_type="manual",
|
|
trigger_user_id=uuid.UUID(user_ctx["id"]),
|
|
input_data={"input": input_text},
|
|
status="failed",
|
|
latency_ms=elapsed_ms,
|
|
error_message=str(e)[:2000],
|
|
finished_at=datetime.utcnow(),
|
|
)
|
|
db.add(execution)
|
|
finally:
|
|
yield "data: [DONE]\n\n"
|
|
|
|
return StreamingResponse(
|
|
event_generator(),
|
|
media_type="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"Connection": "keep-alive",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
)
|
|
|
|
|
|
# ============================== API Key 管理 ==============================
|
|
|
|
|
|
def _generate_api_key() -> tuple[str, str, str]:
|
|
raw = "flow-" + secrets.token_urlsafe(24)
|
|
key_hash = hashlib.sha256(raw.encode()).hexdigest()
|
|
return raw, key_hash, raw[:14]
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/api-keys", response_model=dict)
|
|
async def create_api_key(flow_id: uuid.UUID, body: FlowApiKeyCreate, request: Request, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
user_ctx = request.state.user
|
|
raw, key_hash, key_prefix = _generate_api_key()
|
|
|
|
api_key = FlowApiKey(
|
|
flow_id=flow_id,
|
|
name=body.name,
|
|
key_hash=key_hash,
|
|
key_prefix=key_prefix,
|
|
created_by=uuid.UUID(user_ctx["id"]),
|
|
)
|
|
db.add(api_key)
|
|
await db.flush()
|
|
|
|
return {
|
|
"code": 200,
|
|
"data": {
|
|
"id": str(api_key.id),
|
|
"name": body.name,
|
|
"key_prefix": key_prefix,
|
|
"api_key": raw,
|
|
"created_at": str(api_key.created_at),
|
|
},
|
|
}
|
|
|
|
|
|
@router.get("/definitions/{flow_id}/api-keys", response_model=list[FlowApiKeyOut])
|
|
async def list_api_keys(flow_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(FlowApiKey).where(FlowApiKey.flow_id == flow_id).order_by(FlowApiKey.created_at.desc())
|
|
)
|
|
return [FlowApiKeyOut(
|
|
id=k.id, flow_id=k.flow_id, name=k.name,
|
|
key_prefix=k.key_prefix, last_used_at=k.last_used_at, created_at=k.created_at,
|
|
) for k in result.scalars().all()]
|
|
|
|
|
|
@router.delete("/api-keys/{key_id}")
|
|
async def delete_api_key(key_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
k = await db.get(FlowApiKey, key_id)
|
|
if not k:
|
|
raise HTTPException(404, "API Key不存在")
|
|
await db.delete(k)
|
|
return {"code": 200, "message": "API Key已删除"}
|
|
|
|
|
|
# ============================== 执行历史 ==============================
|
|
|
|
|
|
@router.get("/executions")
|
|
async def list_executions(
|
|
db: AsyncSession = Depends(get_db),
|
|
flow_id: str | None = Query(None),
|
|
page: int = Query(1),
|
|
page_size: int = Query(20),
|
|
):
|
|
query = select(FlowExecution).order_by(FlowExecution.started_at.desc())
|
|
if flow_id:
|
|
query = query.where(FlowExecution.flow_id == uuid.UUID(flow_id))
|
|
total_result = await db.execute(query)
|
|
total = len(total_result.scalars().all())
|
|
|
|
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
|
executions = result.scalars().all()
|
|
return {
|
|
"code": 200,
|
|
"data": [{
|
|
"id": str(e.id),
|
|
"flow_id": str(e.flow_id),
|
|
"version": e.version,
|
|
"trigger_type": e.trigger_type,
|
|
"status": e.status,
|
|
"latency_ms": e.latency_ms,
|
|
"token_usage": e.token_usage,
|
|
"error_message": e.error_message,
|
|
"started_at": str(e.started_at),
|
|
"finished_at": str(e.finished_at) if e.finished_at else None,
|
|
} for e in executions],
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
}
|
|
|
|
|
|
# ============================== 模板 ==============================
|
|
|
|
|
|
FLOW_TEMPLATES = [
|
|
{
|
|
"id": "tpl_doc_process", "name": "文档处理流", "description": "自动解析文档内容,提取关键信息并生成摘要", "icon": "Document",
|
|
"nodes": [
|
|
{"id": "n1", "type": "trigger", "label": "文档上传", "config": {"event_type": "document_upload"}, "position": {"x": 100, "y": 100}},
|
|
{"id": "n2", "type": "tool", "label": "解析文档", "config": {"tool_name": "parse_document"}, "position": {"x": 400, "y": 100}},
|
|
{"id": "n3", "type": "llm", "label": "生成摘要", "config": {"system_prompt": "请为以下文档内容生成简洁摘要", "model": "gpt-4o-mini", "temperature": 0.5}, "position": {"x": 700, "y": 100}},
|
|
{"id": "n4", "type": "output", "label": "输出结果", "config": {"format": "text"}, "position": {"x": 1000, "y": 100}},
|
|
],
|
|
"edges": [
|
|
{"source": "n1", "target": "n2", "sourceHandle": "source"},
|
|
{"source": "n2", "target": "n3", "sourceHandle": "source"},
|
|
{"source": "n3", "target": "n4", "sourceHandle": "source"},
|
|
],
|
|
},
|
|
{
|
|
"id": "tpl_wecom_notify", "name": "企微通知流", "description": "接收触发后查询数据并推送企微通知", "icon": "Bell",
|
|
"nodes": [
|
|
{"id": "n1", "type": "trigger", "label": "定时触发", "config": {"event_type": "scheduled"}, "position": {"x": 100, "y": 100}},
|
|
{"id": "n2", "type": "tool", "label": "查询任务", "config": {"tool_name": "list_tasks"}, "position": {"x": 400, "y": 100}},
|
|
{"id": "n3", "type": "condition", "label": "有待办任务?", "config": {"condition": "tasks.length > 0"}, "position": {"x": 700, "y": 100}},
|
|
{"id": "n4", "type": "wecom_notify", "label": "推送通知", "config": {"message_template": "您有{{tasks.length}}条待办任务", "target": "@all"}, "position": {"x": 1000, "y": 50}},
|
|
{"id": "n5", "type": "output", "label": "无任务", "config": {"format": "text"}, "position": {"x": 1000, "y": 200}},
|
|
],
|
|
"edges": [
|
|
{"source": "n1", "target": "n2", "sourceHandle": "source"},
|
|
{"source": "n2", "target": "n3", "sourceHandle": "source"},
|
|
{"source": "n3", "target": "n4", "sourceHandle": "true"},
|
|
{"source": "n3", "target": "n5", "sourceHandle": "false"},
|
|
],
|
|
},
|
|
{
|
|
"id": "tpl_data_analysis", "name": "数据分析流", "description": "查询员工数据并生成效率分析报告", "icon": "DataAnalysis",
|
|
"nodes": [
|
|
{"id": "n1", "type": "trigger", "label": "分析请求", "config": {"event_type": "button_click"}, "position": {"x": 100, "y": 100}},
|
|
{"id": "n2", "type": "tool", "label": "查询下属", "config": {"tool_name": "list_subordinates"}, "position": {"x": 400, "y": 100}},
|
|
{"id": "n3", "type": "tool", "label": "统计数据", "config": {"tool_name": "get_task_statistics"}, "position": {"x": 700, "y": 100}},
|
|
{"id": "n4", "type": "llm", "label": "生成报告", "config": {"system_prompt": "基于以下数据生成团队效率分析报告", "model": "gpt-4o", "temperature": 0.7}, "position": {"x": 1000, "y": 100}},
|
|
{"id": "n5", "type": "output", "label": "报告输出", "config": {"format": "json"}, "position": {"x": 1300, "y": 100}},
|
|
],
|
|
"edges": [
|
|
{"source": "n1", "target": "n2", "sourceHandle": "source"},
|
|
{"source": "n2", "target": "n3", "sourceHandle": "source"},
|
|
{"source": "n3", "target": "n4", "sourceHandle": "source"},
|
|
{"source": "n4", "target": "n5", "sourceHandle": "source"},
|
|
],
|
|
},
|
|
{
|
|
"id": "tpl_rag_qa", "name": "知识库问答流", "description": "从知识库检索信息后由LLM回答", "icon": "Search",
|
|
"nodes": [
|
|
{"id": "n1", "type": "trigger", "label": "问题触发", "config": {"event_type": "text_message"}, "position": {"x": 100, "y": 100}},
|
|
{"id": "n2", "type": "rag", "label": "知识检索", "config": {"knowledge_base": "default", "top_k": 5}, "position": {"x": 400, "y": 100}},
|
|
{"id": "n3", "type": "llm", "label": "生成回答", "config": {"system_prompt": "基于知识库检索结果回答用户问题", "model": "gpt-4o-mini", "temperature": 0.3}, "position": {"x": 700, "y": 100}},
|
|
{"id": "n4", "type": "output", "label": "输出答案", "config": {"format": "text"}, "position": {"x": 1000, "y": 100}},
|
|
],
|
|
"edges": [
|
|
{"source": "n1", "target": "n2", "sourceHandle": "source"},
|
|
{"source": "n2", "target": "n3", "sourceHandle": "source"},
|
|
{"source": "n3", "target": "n4", "sourceHandle": "source"},
|
|
],
|
|
},
|
|
{
|
|
"id": "tpl_task_auto", "name": "任务自动分配流", "description": "根据描述自动创建任务并分派给合适人员", "icon": "Tools",
|
|
"nodes": [
|
|
{"id": "n1", "type": "trigger", "label": "任务描述", "config": {"event_type": "text_message"}, "position": {"x": 100, "y": 100}},
|
|
{"id": "n2", "type": "llm", "label": "分析任务", "config": {"system_prompt": "分析以下任务描述,提取标题、优先级、负责人", "model": "gpt-4o-mini", "temperature": 0.5}, "position": {"x": 400, "y": 100}},
|
|
{"id": "n3", "type": "tool", "label": "创建任务", "config": {"tool_name": "create_task"}, "position": {"x": 700, "y": 100}},
|
|
{"id": "n4", "type": "wecom_notify", "label": "通知负责人", "config": {"message_template": "您有新任务: {{task_title}}", "target": "@all"}, "position": {"x": 1000, "y": 100}},
|
|
{"id": "n5", "type": "output", "label": "完成", "config": {"format": "text"}, "position": {"x": 1300, "y": 100}},
|
|
],
|
|
"edges": [
|
|
{"source": "n1", "target": "n2", "sourceHandle": "source"},
|
|
{"source": "n2", "target": "n3", "sourceHandle": "source"},
|
|
{"source": "n3", "target": "n4", "sourceHandle": "source"},
|
|
{"source": "n4", "target": "n5", "sourceHandle": "source"},
|
|
],
|
|
},
|
|
]
|
|
|
|
|
|
@router.get("/market", response_model=list[FlowDefinitionOut])
|
|
async def flow_market(db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(FlowDefinition).where(FlowDefinition.status == "published").order_by(FlowDefinition.updated_at.desc())
|
|
)
|
|
return [_build_flow_out(f) for f in result.scalars().all()]
|
|
|
|
|
|
@router.get("/templates")
|
|
async def get_flow_templates(request: Request):
|
|
return {"code": 200, "data": FLOW_TEMPLATES}
|
|
|
|
|
|
@router.post("/templates/{template_id}/use")
|
|
async def use_flow_template(template_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
|
template = next((t for t in FLOW_TEMPLATES if t["id"] == template_id), None)
|
|
if not template:
|
|
raise HTTPException(404, "模板不存在")
|
|
user_ctx = request.state.user
|
|
definition_json = {"nodes": template["nodes"], "edges": template["edges"], "trigger": {}}
|
|
flow = FlowDefinition(
|
|
name=template["name"] + " (副本)",
|
|
description=template["description"],
|
|
definition_json=definition_json,
|
|
draft_definition_json=definition_json,
|
|
creator_id=uuid.UUID(user_ctx["id"]),
|
|
)
|
|
db.add(flow)
|
|
await db.flush()
|
|
return _build_flow_out(flow)
|