yumoqing 2448ad45f7 refactor: 改造为通用产线执行引擎模块
- 去掉独立 aiohttp 服务器,改为标准模块(load_pipeline_service)
- 存储从文件系统改 MySQL(sqlor)
- 新增 3 张数据表:pipeline_tasks/task_steps/artifacts
- 多租户隔离(tenant_id)
- 通用 DAG 调度引擎(读 pipeline_steps 表,不硬编码业务)
- 可插拔步骤处理器(register_handler by step_type)
- artifact 版本管理 + 级联重跑
- init/data.json 标准 appcodes 格式
- 完整 README 文档
2026-06-11 17:30:06 +08:00

283 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""pipeline_service - 通用产线执行引擎模块。
把 Hermes Agent 验证过的业务流程固化为可重复、可并发的产线业务环境。
支持多租户隔离、DAG 步骤调度、可插拔步骤处理器、artifact 版本管理。
任何宿主应用都可以通过 load_pipeline_service() 加载本模块。
"""
import json
from ahserver.serverenv import ServerEnv
from appPublic.uniqueID import getID
from appPublic.log import debug
from .state import (
TASK_SUBMITTED, TASK_RUNNING, TASK_COMPLETED, TASK_FAILED, TASK_PAUSED, TASK_CANCELLED,
build_step_graph, get_cascade_rerun_steps, get_rerun_from_next,
)
from .storage import (
create_task, init_task_steps, get_task, get_task_steps,
get_artifact, get_all_artifacts, list_tasks,
update_task_state, update_task_version,
get_pipeline_steps, reset_steps, save_artifact,
)
from .executor import start_task, resume_task, stop_task, is_running
from .handler import register_handler, list_handlers, register_default_handler
MODULE_NAME = "pipeline_service"
MODULE_VERSION = "2.0.0"
async def pipeline_submit(tenant_id, pipeline_id, owner_id, title, params=None):
"""提交新产线任务。
Args:
tenant_id: 租户ID由宿主应用提供可以是 org_id、user_id 等)
pipeline_id: 产线定义ID来自 pipelines 表)
owner_id: 提交人ID
title: 任务标题
params: 提交参数dict
Returns:
JSON string with status, task_id
"""
result = {"success": False}
try:
if not tenant_id or not pipeline_id:
result["message"] = "缺少 tenant_id 或 pipeline_id"
return json.dumps(result, ensure_ascii=False)
params = params or {}
task_id = await create_task(tenant_id, pipeline_id, owner_id, title, params)
# Read step definitions from pipeline_steps table
step_records = await get_pipeline_steps(pipeline_id)
if not step_records:
result["message"] = f"产线 {pipeline_id} 没有步骤定义"
return json.dumps(result, ensure_ascii=False)
# Create step execution records
await init_task_steps(task_id, step_records)
# Start execution
await start_task(task_id)
result["success"] = True
result["task_id"] = task_id
result["message"] = "任务已提交并开始执行"
except Exception as e:
result["message"] = str(e)
return json.dumps(result, ensure_ascii=False)
async def pipeline_list(tenant_id, pipeline_id=None, limit=100):
"""查询租户的任务列表。"""
result = {"success": False}
try:
tasks = await list_tasks(tenant_id, pipeline_id, limit)
result["success"] = True
result["tasks"] = tasks
result["total"] = len(tasks)
except Exception as e:
result["message"] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)
async def pipeline_detail(tenant_id, task_id):
"""获取任务详情 + 步骤状态树。"""
result = {"success": False}
try:
task = await get_task(tenant_id, task_id)
if not task:
result["message"] = "任务不存在"
return json.dumps(result, ensure_ascii=False)
steps = await get_task_steps(task_id)
task["steps"] = steps
task["is_running"] = is_running(task_id)
result["success"] = True
result["task"] = task
except Exception as e:
result["message"] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)
async def pipeline_node(tenant_id, task_id, step_name, version=None):
"""获取某节点某版本的 input/output artifact。"""
result = {"success": False}
try:
task = await get_task(tenant_id, task_id)
if not task:
result["message"] = "任务不存在"
return json.dumps(result, ensure_ascii=False)
v = version or task.get("current_version", 1)
if isinstance(v, str):
v = int(v)
input_data = await get_artifact(task_id, v, step_name, "input")
output_data = await get_artifact(task_id, v, step_name, "output")
result["success"] = True
result["step_name"] = step_name
result["version"] = v
result["input"] = input_data
result["output"] = output_data
except Exception as e:
result["message"] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)
async def pipeline_modify(tenant_id, task_id, updates, rerun_from="node"):
"""修改节点 artifact 并触发级联重跑。
Args:
updates: {step_name: {content: {...}}, ...}
rerun_from: "node" = 从该节点重跑, "next" = 从下游节点重跑
"""
result = {"success": False}
try:
task = await get_task(tenant_id, task_id)
if not task:
result["message"] = "任务不存在"
return json.dumps(result, ensure_ascii=False)
if is_running(task_id):
result["message"] = "任务正在执行中,请先暂停"
return json.dumps(result, ensure_ascii=False)
pipeline_id = task.get("pipeline_id", task.get("Pipeline_id", ""))
current_version = task.get("current_version", task.get("current_Version", 1))
if isinstance(current_version, str):
current_version = int(current_version)
# Load step graph
step_records = await get_pipeline_steps(pipeline_id)
step_graph = build_step_graph(step_records)
# Calculate affected steps
all_rerun = set()
for step_name in updates:
if step_name not in step_graph:
result["message"] = f"未知步骤: {step_name}"
return json.dumps(result, ensure_ascii=False)
if rerun_from == "node":
affected = get_cascade_rerun_steps(step_graph, step_name)
else:
affected = get_rerun_from_next(step_graph, step_name)
all_rerun.update(affected)
# Create new version
new_version = current_version + 1
await update_task_version(task_id, new_version)
# Save modified artifacts
for step_name, step_update in updates.items():
content = step_update.get("content", step_update)
io_type = "input" if rerun_from == "node" else "output"
await save_artifact(task_id, new_version, step_name, io_type, content)
# Reset affected steps
all_rerun_list = sorted(all_rerun, key=lambda s: step_graph.get(s, {}).get("order", 999))
await reset_steps(task_id, all_rerun_list)
# Update task state and resume
await update_task_state(task_id, TASK_RUNNING)
await resume_task(task_id)
result["success"] = True
result["new_version"] = new_version
result["rerun_steps"] = all_rerun_list
result["message"] = f"创建 v{new_version},重跑 {len(all_rerun_list)} 个步骤"
except Exception as e:
result["message"] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)
async def pipeline_pause(tenant_id, task_id):
"""暂停任务。"""
result = {"success": False}
try:
task = await get_task(tenant_id, task_id)
if not task:
result["message"] = "任务不存在"
return json.dumps(result, ensure_ascii=False)
await stop_task(task_id)
await update_task_state(task_id, TASK_PAUSED)
result["success"] = True
result["message"] = "任务已暂停"
except Exception as e:
result["message"] = str(e)
return json.dumps(result, ensure_ascii=False)
async def pipeline_resume(tenant_id, task_id):
"""恢复任务。"""
result = {"success": False}
try:
task = await get_task(tenant_id, task_id)
if not task:
result["message"] = "任务不存在"
return json.dumps(result, ensure_ascii=False)
await update_task_state(task_id, TASK_RUNNING)
await resume_task(task_id)
result["success"] = True
result["message"] = "任务已恢复"
except Exception as e:
result["message"] = str(e)
return json.dumps(result, ensure_ascii=False)
async def pipeline_cancel(tenant_id, task_id):
"""取消任务。"""
result = {"success": False}
try:
task = await get_task(tenant_id, task_id)
if not task:
result["message"] = "任务不存在"
return json.dumps(result, ensure_ascii=False)
await stop_task(task_id)
await update_task_state(task_id, TASK_CANCELLED)
result["success"] = True
result["message"] = "任务已取消"
except Exception as e:
result["message"] = str(e)
return json.dumps(result, ensure_ascii=False)
def pipeline_handlers():
"""查看已注册的步骤处理器。"""
return json.dumps(list_handlers(), ensure_ascii=False)
def load_pipeline_service():
"""注册所有函数到 ServerEnv。任何宿主应用调用此函数即可使用产线引擎。"""
env = ServerEnv()
# Task lifecycle
env.pipeline_submit = pipeline_submit
env.pipeline_list = pipeline_list
env.pipeline_detail = pipeline_detail
env.pipeline_node = pipeline_node
env.pipeline_modify = pipeline_modify
env.pipeline_pause = pipeline_pause
env.pipeline_resume = pipeline_resume
env.pipeline_cancel = pipeline_cancel
# Handler management
env.pipeline_register_handler = register_handler
env.pipeline_handlers = pipeline_handlers
# Register default handler
register_default_handler()
debug(f"[{MODULE_NAME}] v{MODULE_VERSION} loaded — generic pipeline execution engine")
return True