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.
756 lines
28 KiB
756 lines
28 KiB
import uuid
|
|
import time
|
|
import json
|
|
import asyncio
|
|
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, MemoryMessage, FlowTemplate
|
|
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=getattr(f, 'published_version_id', None),
|
|
published_to_wecom=getattr(f, 'published_to_wecom', False),
|
|
published_to_web=getattr(f, 'published_to_web', False),
|
|
flow_mode=getattr(f, 'flow_mode', 'chatflow') or 'chatflow',
|
|
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"]),
|
|
flow_mode=req.flow_mode,
|
|
)
|
|
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
|
|
|
|
|
|
def validate_flow_definition(definition: dict, flow_mode: str = "chatflow") -> list[str]:
|
|
errors = []
|
|
nodes = definition.get("nodes", [])
|
|
edges = definition.get("edges", [])
|
|
|
|
if not nodes:
|
|
errors.append("流定义中没有节点")
|
|
return errors
|
|
|
|
if not any(n.get("type") == "trigger" for n in nodes):
|
|
errors.append("缺少触发/起始节点 (trigger)")
|
|
|
|
if flow_mode == "chatflow" and not any(n.get("type") == "llm" for n in nodes):
|
|
errors.append("对话型流必须包含至少一个 LLM 节点")
|
|
|
|
node_ids = {n["id"] for n in nodes}
|
|
connected_ids = set()
|
|
for e in edges:
|
|
connected_ids.add(e.get("source", ""))
|
|
connected_ids.add(e.get("target", ""))
|
|
for n in nodes:
|
|
if n["id"] not in connected_ids and len(nodes) > 1:
|
|
errors.append(f"节点 '{n.get('label', n['id'])}' 未连接")
|
|
|
|
return errors
|
|
|
|
|
|
@router.post("/definitions/{flow_id}/validate")
|
|
async def validate_flow(flow_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
f = await db.get(FlowDefinition, flow_id)
|
|
if not f:
|
|
raise HTTPException(404, "流定义不存在")
|
|
flow_mode = getattr(f, 'flow_mode', 'chatflow') or 'chatflow'
|
|
errors = validate_flow_definition(f.definition_json, flow_mode)
|
|
return {"code": 200, "valid": len(errors) == 0, "errors": errors}
|
|
|
|
|
|
@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, "流定义中没有节点")
|
|
flow_mode = getattr(f, 'flow_mode', 'chatflow') or 'chatflow'
|
|
errors = validate_flow_definition(f.definition_json, flow_mode)
|
|
if errors:
|
|
raise HTTPException(400, f"流校验失败: {'; '.join(errors)}")
|
|
user_ctx = request.state.user
|
|
body = {}
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
pass
|
|
changelog = body.get("changelog", "")
|
|
await _snapshot_publish(f, db, user_ctx["id"], publish_wecom=True, changelog=changelog)
|
|
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:
|
|
published_version_id = getattr(flow, 'published_version_id', None)
|
|
if published_version_id:
|
|
result = await db.execute(select(FlowVersion).where(FlowVersion.id == 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", ""))
|
|
session_id = payload.get("session_id") or str(uuid.uuid4())
|
|
|
|
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", ""),
|
|
"session_id": session_id,
|
|
"trigger_data": payload.get("trigger", {}),
|
|
"_node_results": {},
|
|
}
|
|
|
|
try:
|
|
from modules.memory.manager import get_memory_manager
|
|
mm = get_memory_manager()
|
|
flow_mode = getattr(f, 'flow_mode', 'chatflow') or 'chatflow'
|
|
if flow_mode == 'chatflow':
|
|
await mm.inject_memory(
|
|
user_id=user_ctx["id"],
|
|
flow_id=str(flow_id),
|
|
session_id=session_id,
|
|
context=context,
|
|
)
|
|
except Exception as e:
|
|
logger.debug(f"记忆注入跳过: {e}")
|
|
|
|
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)
|
|
|
|
try:
|
|
from modules.memory.manager import get_memory_manager
|
|
mm = get_memory_manager()
|
|
if flow_mode == 'chatflow':
|
|
asyncio.create_task(mm.record_exchange(
|
|
user_id=user_ctx["id"],
|
|
flow_id=str(flow_id),
|
|
session_id=session_id,
|
|
user_msg=input_text,
|
|
assistant_msg=output_text,
|
|
flow_name=f.name,
|
|
))
|
|
except Exception as e:
|
|
logger.debug(f"记忆记录跳过: {e}")
|
|
|
|
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,
|
|
"session_id": session_id,
|
|
"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", ""))
|
|
session_id = body.get("session_id") or str(uuid.uuid4())
|
|
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)
|
|
|
|
try:
|
|
from modules.memory.manager import get_memory_manager
|
|
mm_stream = get_memory_manager()
|
|
except Exception:
|
|
mm_stream = None
|
|
|
|
flow_mode_stream = getattr(f, 'flow_mode', 'chatflow') or 'chatflow'
|
|
|
|
async def event_generator():
|
|
engine = FlowEngine(definition)
|
|
context = {
|
|
"user_id": user_ctx["id"],
|
|
"username": user_ctx.get("username", ""),
|
|
"session_id": session_id,
|
|
"trigger_data": body.get("trigger", {}),
|
|
"_node_results": {},
|
|
"_stream_callback": None,
|
|
}
|
|
|
|
if mm_stream and flow_mode_stream == 'chatflow':
|
|
try:
|
|
await mm_stream.inject_memory(
|
|
user_id=user_ctx["id"],
|
|
flow_id=str(flow_id),
|
|
session_id=session_id,
|
|
context=context,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
input_msg = Msg(name="user", content=input_text, role="user")
|
|
start_time = time.time()
|
|
|
|
token_queue: asyncio.Queue = asyncio.Queue()
|
|
|
|
async def stream_callback(event_type: str, data: dict):
|
|
await token_queue.put((event_type, data))
|
|
|
|
context["_stream_callback"] = stream_callback
|
|
|
|
yield f"data: {json.dumps({'event': 'workflow_started', 'data': {'flow_id': str(flow_id)}}, ensure_ascii=False)}\n\n"
|
|
|
|
execution_task = asyncio.create_task(
|
|
asyncio.wait_for(
|
|
engine.execute(input_msg, context),
|
|
timeout=engine.FLOW_TIMEOUT_SECONDS,
|
|
)
|
|
)
|
|
|
|
result_msg = None
|
|
exec_error = None
|
|
while True:
|
|
done = asyncio.ensure_future(token_queue.get())
|
|
try:
|
|
wait_done, _ = await asyncio.wait(
|
|
[asyncio.ensure_future(execution_task), done],
|
|
return_when=asyncio.FIRST_COMPLETED,
|
|
)
|
|
except Exception as e:
|
|
exec_error = e
|
|
break
|
|
|
|
if execution_task.done():
|
|
try:
|
|
result_msg = execution_task.result()
|
|
except asyncio.TimeoutError:
|
|
exec_error = asyncio.TimeoutError("执行超时")
|
|
except Exception as e:
|
|
exec_error = e
|
|
while not token_queue.empty():
|
|
ev_type, ev_data = token_queue.get_nowait()
|
|
chunk_data = json.dumps({"event": ev_type, "data": ev_data}, ensure_ascii=False)
|
|
yield f"data: {chunk_data}\n\n"
|
|
break
|
|
|
|
ev_type, ev_data = done.result()
|
|
chunk_data = json.dumps({"event": ev_type, "data": ev_data}, ensure_ascii=False)
|
|
yield f"data: {chunk_data}\n\n"
|
|
|
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
|
|
|
if exec_error:
|
|
yield f"data: {json.dumps({'event': 'error', 'data': {'message': str(exec_error)}}, 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(exec_error)[:2000] if not isinstance(exec_error, asyncio.TimeoutError) else "执行超时",
|
|
finished_at=datetime.utcnow(),
|
|
)
|
|
db.add(execution)
|
|
return
|
|
|
|
output_text = result_msg.get_text_content() if hasattr(result_msg, 'get_text_content') else str(result_msg)
|
|
|
|
yield f"data: {json.dumps({'event': 'workflow_finished', 'data': {'output': output_text, 'session_id': session_id, 'node_results': {k: str(v)[:200] for k, v in context.get('_node_results', {}).items()}, 'latency_ms': elapsed_ms}}, ensure_ascii=False)}\n\n"
|
|
|
|
if mm_stream and flow_mode_stream == 'chatflow':
|
|
try:
|
|
asyncio.create_task(mm_stream.record_exchange(
|
|
user_id=user_ctx["id"],
|
|
flow_id=str(flow_id),
|
|
session_id=session_id,
|
|
user_msg=input_text,
|
|
assistant_msg=output_text,
|
|
flow_name=f.name,
|
|
))
|
|
except Exception:
|
|
pass
|
|
|
|
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)
|
|
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,
|
|
}
|
|
|
|
|
|
# ============================== 模板 ==============================
|
|
|
|
|
|
@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(db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(FlowTemplate).order_by(FlowTemplate.sort_order.asc(), FlowTemplate.created_at.desc())
|
|
)
|
|
templates = result.scalars().all()
|
|
return {"code": 200, "data": [
|
|
{
|
|
"id": str(t.id), "name": t.name, "description": t.description,
|
|
"category": t.category, "definition_json": t.definition_json,
|
|
"icon": t.icon, "is_builtin": t.is_builtin, "usage_count": t.usage_count,
|
|
}
|
|
for t in templates
|
|
]}
|
|
|
|
|
|
@router.post("/templates")
|
|
async def create_flow_template(request: Request, db: AsyncSession = Depends(get_db)):
|
|
body = await request.json()
|
|
user_ctx = request.state.user
|
|
ft = FlowTemplate(
|
|
name=body.get("name", ""),
|
|
description=body.get("description", ""),
|
|
category=body.get("category", "general"),
|
|
definition_json=body.get("definition_json", {}),
|
|
icon=body.get("icon", ""),
|
|
sort_order=body.get("sort_order", 0),
|
|
created_by=uuid.UUID(user_ctx["id"]),
|
|
)
|
|
db.add(ft)
|
|
await db.commit()
|
|
await db.refresh(ft)
|
|
return {"code": 200, "data": {"id": str(ft.id), "name": ft.name}}
|
|
|
|
|
|
@router.put("/templates/{template_id}")
|
|
async def update_flow_template(template_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
|
|
ft = await db.get(FlowTemplate, template_id)
|
|
if not ft:
|
|
raise HTTPException(404, "模板不存在")
|
|
body = await request.json()
|
|
for field in ("name", "description", "category", "definition_json", "icon", "sort_order"):
|
|
if field in body:
|
|
setattr(ft, field, body[field])
|
|
await db.commit()
|
|
return {"code": 200, "data": {"id": str(ft.id)}}
|
|
|
|
|
|
@router.delete("/templates/{template_id}")
|
|
async def delete_flow_template(template_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
|
ft = await db.get(FlowTemplate, template_id)
|
|
if not ft:
|
|
raise HTTPException(404, "模板不存在")
|
|
if ft.is_builtin:
|
|
raise HTTPException(400, "内置模板不可删除")
|
|
await db.delete(ft)
|
|
await db.commit()
|
|
return {"code": 200, "message": "模板已删除"}
|
|
|
|
|
|
@router.post("/templates/{template_id}/use")
|
|
async def use_flow_template(template_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
|
|
ft = await db.get(FlowTemplate, template_id)
|
|
if not ft:
|
|
raise HTTPException(404, "模板不存在")
|
|
ft.usage_count = (ft.usage_count or 0) + 1
|
|
user_ctx = request.state.user
|
|
flow = FlowDefinition(
|
|
name=ft.name + " (副本)",
|
|
description=ft.description or "",
|
|
definition_json=ft.definition_json,
|
|
draft_definition_json=ft.definition_json,
|
|
creator_id=uuid.UUID(user_ctx["id"]),
|
|
)
|
|
db.add(flow)
|
|
await db.flush()
|
|
await db.commit()
|
|
return _build_flow_out(flow)
|