Files
MoviePilot/app/application/workflow.py
T

543 lines
19 KiB
Python

"""工作流状态与定义写操作应用用例。"""
from dataclasses import dataclass
import json
from collections.abc import Awaitable
from datetime import datetime
from typing import Any, Callable, Mapping, Optional, Protocol, TypeVar
WORKFLOW_TRIGGER_TIMER = "timer"
WORKFLOW_TRIGGER_EVENT = "event"
WORKFLOW_TRIGGER_MANUAL = "manual"
SUPPORTED_WORKFLOW_TRIGGERS = {
WORKFLOW_TRIGGER_TIMER,
WORKFLOW_TRIGGER_EVENT,
WORKFLOW_TRIGGER_MANUAL,
}
class WorkflowRuntime(Protocol):
"""声明宿主入口与 Chain 消费的工作流运行时能力。"""
def execute(self, *args: Any, **kwargs: Any) -> Any:
"""执行单个工作流动作,参数与 concrete 管理器保持一致。"""
...
def list_actions(self) -> list[dict[str, Any]]:
"""返回当前运行时登记的工作流动作定义。"""
...
def load_workflow_events(self, workflow_id: Optional[int] = None) -> None:
"""加载全部或指定工作流的事件触发器。"""
...
def remove_workflow_event(
self,
workflow_id: Optional[int] = None,
event_type_str: Optional[str] = None,
) -> None:
"""移除全部或指定工作流的事件触发器。"""
...
def update_workflow_event(self, workflow: Any) -> None:
"""按最新定义刷新工作流事件触发器。"""
...
def register_execution(self, owner: "WorkflowExecutionOwner") -> bool:
"""登记活动工作流执行 owner;停机封口后返回 False。"""
...
def unregister_execution(self, owner: "WorkflowExecutionOwner") -> None:
"""在工作流执行真实终止后释放 owner。"""
...
class WorkflowExecutionOwner(Protocol):
"""声明 concrete 工作流管理器需要持有的执行生命周期能力。"""
def request_stop(self) -> None:
"""请求停止继续调度,并通知支持取消的活动动作。"""
...
def wait_stopped(self, timeout: float) -> bool:
"""有限等待执行及其节点线程池真实终止。"""
...
WorkflowRuntimeProvider = Callable[[], WorkflowRuntime]
def _unconfigured_workflow_runtime() -> WorkflowRuntime:
"""拒绝在启动组合根装配前隐式创建工作流管理器。"""
raise RuntimeError("工作流运行时尚未由启动组合根装配")
_workflow_runtime_provider: WorkflowRuntimeProvider = _unconfigured_workflow_runtime
def configure_workflow_runtime(provider: WorkflowRuntimeProvider) -> None:
"""由启动组合根登记工作流运行时实例提供器。"""
global _workflow_runtime_provider
_workflow_runtime_provider = provider
def get_workflow_manager() -> WorkflowRuntime:
"""返回组合根提供的工作流运行时,避免消费者直接定位 Singleton。"""
return _workflow_runtime_provider()
class AsyncWorkflowQueryRepository(Protocol):
"""工作流查询用例需要的异步读取端口。"""
async def async_list(self) -> list[Any]:
"""读取全部工作流。"""
...
async def async_get(self, workflow_id: int) -> Optional[Any]:
"""按 ID 读取工作流。"""
...
class WorkflowCachePort(Protocol):
"""工作流重置所需的同步与异步配置缓存端口。"""
def delete(self, key: Any) -> Any:
"""删除配置缓存。"""
...
async def async_delete(self, key: Any) -> Any:
"""通过异步数据库执行端口删除配置缓存。"""
...
class WorkflowQueryService:
"""提供工作流列表和详情查询,隔离 API 与数据库会话。"""
def __init__(self, repository: AsyncWorkflowQueryRepository) -> None:
"""保存请求级异步查询端口。"""
self._repository = repository
async def list(self) -> list[Any]:
"""返回全部工作流。"""
return await self._repository.async_list()
async def get(self, workflow_id: int) -> Optional[Any]:
"""返回指定工作流。"""
return await self._repository.async_get(workflow_id)
_configured_workflow_query: WorkflowQueryService | None = None
def configure_workflow_query(service: WorkflowQueryService) -> None:
"""由启动组合根登记工作流查询服务。"""
global _configured_workflow_query
_configured_workflow_query = service
def get_configured_workflow_query() -> WorkflowQueryService:
"""返回启动阶段登记的工作流查询服务。"""
if _configured_workflow_query is None:
raise RuntimeError("工作流查询服务尚未配置")
return _configured_workflow_query
@dataclass(frozen=True, slots=True)
class WorkflowMutationResult:
"""描述工作流写操作是否成功及兼容提示信息。"""
success: bool
message: str = ""
class WorkflowMutationRepository(Protocol):
"""工作流写用例需要的最小持久化端口。"""
def get(self, workflow_id: int) -> Optional[Any]:
"""读取工作流。"""
...
def stage_state(self, workflow_id: int, state: str) -> bool:
"""暂存工作流状态变更。"""
...
def stage_update(self, workflow_id: int, payload: Mapping[str, Any]) -> Optional[Any]:
"""暂存工作流定义更新并返回更新后的对象。"""
...
def stage_delete(self, workflow_id: int) -> None:
"""暂存工作流删除。"""
...
class UnitOfWork(Protocol):
"""同步工作流写用例使用的事务端口。"""
def commit(self) -> None:
"""提交当前事务。"""
...
def rollback(self) -> None:
"""回滚当前事务。"""
...
class WorkflowExecutionRepository(Protocol):
"""工作流执行状态写入所需的最小暂存端口。"""
def stage_start(self, workflow_id: int) -> bool:
"""暂存运行中状态。"""
...
def stage_success(
self,
workflow_id: int,
result: Optional[str] = None,
) -> bool:
"""暂存成功状态和执行次数。"""
...
def stage_fail(self, workflow_id: int, result: str) -> bool:
"""暂存失败状态和错误信息。"""
...
def stage_step(
self,
workflow_id: int,
action_id: str,
context: dict[str, Any],
execution_state: Optional[dict[str, Any]] = None,
) -> bool:
"""暂存动作进度和执行上下文。"""
...
def stage_execution_reset(
self,
workflow_id: int,
reset_count: bool = False,
) -> bool:
"""暂存执行状态重置。"""
...
_ExecutionResult = TypeVar("_ExecutionResult")
class WorkflowExecutionCommand:
"""在一个显式 UnitOfWork 中提交单次工作流执行状态变更。"""
def __init__(
self,
*,
repository: WorkflowExecutionRepository,
unit_of_work: UnitOfWork,
) -> None:
"""保存工作流执行仓储和事务端口。"""
self._repository = repository
self._unit_of_work = unit_of_work
def start(self, workflow_id: int) -> bool:
"""提交工作流运行中状态。"""
return self._commit(lambda: self._repository.stage_start(workflow_id))
def success(
self,
workflow_id: int,
result: Optional[str] = None,
) -> bool:
"""提交工作流成功状态。"""
return self._commit(
lambda: self._repository.stage_success(workflow_id, result)
)
def fail(self, workflow_id: int, result: str) -> bool:
"""提交工作流失败状态。"""
return self._commit(
lambda: self._repository.stage_fail(workflow_id, result)
)
def step(
self,
workflow_id: int,
action_id: str,
context: dict[str, Any],
execution_state: Optional[dict[str, Any]] = None,
) -> bool:
"""提交工作流动作进度。"""
return self._commit(
lambda: self._repository.stage_step(
workflow_id,
action_id,
context,
execution_state,
)
)
def reset(self, workflow_id: int, reset_count: bool = False) -> bool:
"""提交工作流执行状态重置。"""
return self._commit(
lambda: self._repository.stage_execution_reset(
workflow_id,
reset_count,
)
)
def _commit(self, operation: Callable[[], _ExecutionResult]) -> _ExecutionResult:
"""提交暂存操作;失败时回滚并原样传播异常。"""
try:
result = operation()
self._unit_of_work.commit()
return result
except Exception:
self._unit_of_work.rollback()
raise
class WorkflowMutationCommand:
"""协调工作流状态、定义、调度和事件注册变更。"""
def __init__(
self,
*,
repository: WorkflowMutationRepository,
unit_of_work: UnitOfWork,
add_timer: Callable[[Any], None],
remove_timer: Callable[[Any], None],
load_event: Callable[[int], None],
remove_event: Callable[[int, Optional[str]], None],
refresh_event: Callable[[Any], None],
stop_running: Callable[[int], None],
delete_cache: Callable[[int], None],
) -> None:
"""保存工作流事务和提交后运行时副作用端口。"""
self._repository = repository
self._unit_of_work = unit_of_work
self._add_timer = add_timer
self._remove_timer = remove_timer
self._load_event = load_event
self._remove_event = remove_event
self._refresh_event = refresh_event
self._stop_running = stop_running
self._delete_cache = delete_cache
def start(self, workflow_id: int) -> WorkflowMutationResult:
"""启用工作流,并在提交后登记定时器或事件触发器。"""
workflow = self._repository.get(workflow_id)
if not workflow:
return WorkflowMutationResult(False, "工作流不存在")
trigger_type = workflow.trigger_type or WORKFLOW_TRIGGER_TIMER
if trigger_type == WORKFLOW_TRIGGER_TIMER and not workflow.timer:
return WorkflowMutationResult(False, "定时工作流缺少定时器配置")
if trigger_type not in SUPPORTED_WORKFLOW_TRIGGERS:
return WorkflowMutationResult(False, "工作流触发类型不支持")
self._repository.stage_state(workflow_id, "W")
self._commit()
if trigger_type == WORKFLOW_TRIGGER_TIMER:
self._add_timer(workflow)
elif trigger_type == WORKFLOW_TRIGGER_EVENT:
self._load_event(workflow_id)
return WorkflowMutationResult(True)
def pause(self, workflow_id: int) -> WorkflowMutationResult:
"""停用工作流,并在提交后移除运行时触发器和执行状态。"""
workflow = self._repository.get(workflow_id)
if not workflow:
return WorkflowMutationResult(False, "工作流不存在")
self._repository.stage_state(workflow_id, "P")
self._commit()
if workflow.trigger_type == WORKFLOW_TRIGGER_TIMER:
self._remove_timer(workflow)
elif workflow.trigger_type == WORKFLOW_TRIGGER_EVENT:
self._remove_event(workflow_id, workflow.event_type)
self._stop_running(workflow_id)
return WorkflowMutationResult(True)
def update(self, payload: Mapping[str, Any]) -> WorkflowMutationResult:
"""更新工作流定义,并在提交后刷新调度器和事件注册。"""
values = dict(payload)
workflow_id = values.get("id")
if not workflow_id:
return WorkflowMutationResult(False, "工作流ID不能为空")
current = self._repository.get(workflow_id)
if not current:
return WorkflowMutationResult(False, "工作流不存在")
if not current.trigger_type:
values["trigger_type"] = WORKFLOW_TRIGGER_TIMER
updated = self._repository.stage_update(workflow_id, values)
if not updated:
self._unit_of_work.rollback()
return WorkflowMutationResult(False, "工作流不存在")
self._commit()
self._remove_timer(updated)
if (
not updated.trigger_type
or updated.trigger_type == WORKFLOW_TRIGGER_TIMER
) and updated.timer:
self._add_timer(updated)
self._refresh_event(updated)
return WorkflowMutationResult(True, "更新成功")
def delete(self, workflow_id: int) -> WorkflowMutationResult:
"""删除工作流,并在提交后清除缓存和运行时触发器。"""
workflow = self._repository.get(workflow_id)
if not workflow:
return WorkflowMutationResult(False, "工作流不存在")
self._repository.stage_delete(workflow_id)
self._commit()
self._delete_cache(workflow_id)
if not workflow.trigger_type or workflow.trigger_type == WORKFLOW_TRIGGER_TIMER:
self._remove_timer(workflow)
elif workflow.trigger_type == WORKFLOW_TRIGGER_EVENT:
self._remove_event(workflow_id, workflow.event_type)
return WorkflowMutationResult(True, "删除成功")
def _commit(self) -> None:
"""提交工作流事务,失败时回滚且不执行后续运行时副作用。"""
try:
self._unit_of_work.commit()
except Exception:
self._unit_of_work.rollback()
raise
class AsyncWorkflowDefinitionRepository(Protocol):
"""工作流创建、复用和重置需要的异步持久化端口。"""
async def async_get_by_name(self, name: str) -> Optional[Any]:
"""按名称读取工作流。"""
...
async def stage_create(self, payload: Mapping[str, Any]) -> Any:
"""暂存新工作流。"""
...
async def stage_reset(self, workflow_id: int, reset_count: bool = False) -> Optional[Any]:
"""暂存工作流重置。"""
...
async def async_get(self, workflow_id: int) -> Optional[Any]:
"""读取指定工作流。"""
...
class AsyncUnitOfWork(Protocol):
"""异步工作流定义用例使用的事务端口。"""
async def commit(self) -> None:
"""提交当前事务。"""
...
async def rollback(self) -> None:
"""回滚当前事务。"""
...
class WorkflowDefinitionCommand:
"""协调工作流创建、分享复用和重置的异步写用例。"""
def __init__(
self,
*,
repository: AsyncWorkflowDefinitionRepository,
unit_of_work: AsyncUnitOfWork,
stop_running: Callable[[int], None],
async_delete_cache: Callable[[int], Awaitable[Any]],
report_fork: Optional[Callable[[int], Awaitable[object]]] = None,
) -> None:
"""保存异步事务和提交后运行时副作用端口。"""
self._repository = repository
self._unit_of_work = unit_of_work
self._stop_running = stop_running
self._async_delete_cache = async_delete_cache
self._report_fork = report_fork
async def create(self, payload: Mapping[str, Any]) -> WorkflowMutationResult:
"""校验名称并暂存新工作流,提交失败时不产生运行时副作用。"""
values = dict(payload)
name = values.get("name")
if name and await self._repository.async_get_by_name(name):
return WorkflowMutationResult(False, "已存在相同名称的工作流")
if not values.get("add_time"):
values["add_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if not values.get("state"):
values["state"] = "P"
if not values.get("trigger_type"):
values["trigger_type"] = WORKFLOW_TRIGGER_TIMER
try:
await self._repository.stage_create(values)
await self._commit()
except Exception:
raise
return WorkflowMutationResult(True, "创建工作流成功")
async def fork(
self,
payload: Mapping[str, Any],
share_id: Optional[int] = None,
) -> WorkflowMutationResult:
"""解析共享工作流内容并在提交后更新远程复用次数。"""
values = dict(payload)
if not values.get("name"):
return WorkflowMutationResult(False, "工作流名称不能为空")
parsed = {}
for field, default, error_message in (
("actions", "[]", "actions字段JSON格式错误"),
("flows", "[]", "flows字段JSON格式错误"),
("context", "{}", "context字段JSON格式错误"),
("event_conditions", "{}", "event_conditions字段JSON格式错误"),
):
raw = values.get(field)
try:
parsed[field] = json.loads(raw or default)
except json.JSONDecodeError:
return WorkflowMutationResult(False, error_message)
workflow_values = {
"name": values["name"],
"description": values.get("description"),
"timer": values.get("timer"),
"trigger_type": values.get("trigger_type") or WORKFLOW_TRIGGER_TIMER,
"event_type": values.get("event_type"),
"event_conditions": parsed["event_conditions"],
"actions": parsed["actions"],
"flows": parsed["flows"],
"context": parsed["context"],
"state": "P",
}
if await self._repository.async_get_by_name(workflow_values["name"]):
return WorkflowMutationResult(False, "已存在相同名称的工作流")
try:
created = await self._repository.stage_create(workflow_values)
await self._commit()
except Exception:
raise
if created and share_id and self._report_fork:
try:
await self._report_fork(share_id)
except Exception:
return WorkflowMutationResult(True, "复用成功;共享统计上报失败")
return WorkflowMutationResult(True, "复用成功")
async def reset(self, workflow_id: int) -> WorkflowMutationResult:
"""重置工作流并在提交后停止运行态、清除缓存。"""
workflow = await self._repository.async_get(workflow_id)
if not workflow:
return WorkflowMutationResult(False, "工作流不存在")
await self._repository.stage_reset(workflow_id, reset_count=True)
await self._commit()
self._stop_running(workflow_id)
await self._async_delete_cache(workflow_id)
return WorkflowMutationResult(True)
async def _commit(self) -> None:
"""提交异步事务,失败时回滚且不继续执行运行时副作用。"""
try:
await self._unit_of_work.commit()
except Exception:
await self._unit_of_work.rollback()
raise