Files
MoviePilot/app/application/workflow.py
Aqr-K 951cdc5b27 fix: arbitrate subscription identity and restore workflow state (#7147)
* fix: arbitrate subscription identity and restore workflow state

* test: preserve legacy empty-owner subscription transactions

* fix: align shared atomic configuration method with lifecycle prerequisite

---------

Co-authored-by: jxxghp <jxxghp@gmail.com>
2026-10-23 08:14:15 +08:00

1162 lines
39 KiB
Python
Raw Permalink 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.
"""
工作流状态与定义写操作应用用例。
"""
import json
from collections import deque
from collections.abc import Awaitable
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Callable, List, Mapping, Optional, Protocol, TypeVar
from app.schemas.common import JsonData
WORKFLOW_TRIGGER_TIMER = "timer"
WORKFLOW_TRIGGER_EVENT = "event"
WORKFLOW_TRIGGER_MANUAL = "manual"
SUPPORTED_WORKFLOW_TRIGGERS = {
WORKFLOW_TRIGGER_TIMER,
WORKFLOW_TRIGGER_EVENT,
WORKFLOW_TRIGGER_MANUAL,
}
def _workflow_graph_entries(value: object, kind: str) -> tuple[Mapping[str, object], ...]:
"""
规范化动作或流程列表,并拒绝共享 JSON 中格式错误的条目。
"""
if value is None:
return ()
if not isinstance(value, (list, tuple)):
raise ValueError(f"工作流{kind}必须为列表")
entries: list[Mapping[str, object]] = []
for item in value:
if not isinstance(item, Mapping):
raise ValueError(f"工作流{kind}定义格式错误")
entries.append(item)
return tuple(entries)
def _workflow_action_ids(actions: tuple[Mapping[str, object], ...]) -> set[str]:
"""
提取非空且唯一的动作 ID,避免执行器字典静默覆盖节点。
"""
action_ids: set[str] = set()
for action in actions:
action_id = action.get("id")
if not isinstance(action_id, str) or not action_id.strip():
raise ValueError("工作流动作缺少 ID")
if action_id in action_ids:
raise ValueError(f"工作流动作 ID 重复:{action_id}")
action_ids.add(action_id)
return action_ids
def _workflow_flow_endpoint(value: object, action_ids: set[str]) -> str:
"""
校验每个流程端点都指向图中已有的动作。
"""
if not isinstance(value, str) or not value.strip():
raise ValueError("工作流流程端点不能为空")
if value not in action_ids:
raise ValueError(f"工作流流程端点不存在:{value}")
return value
def _workflow_graph_edges(
flows: tuple[Mapping[str, object], ...], action_ids: set[str],
) -> tuple[dict[str, list[str]], dict[str, int]]:
"""
校验流程 ID 和端点,并构造拓扑校验需要的出边与入度表。
"""
outgoing: dict[str, list[str]] = {action_id: [] for action_id in action_ids}
incoming = dict.fromkeys(action_ids, 0)
flow_ids: set[str] = set()
for flow in flows:
flow_id = flow.get("id")
if flow_id is not None:
if not isinstance(flow_id, str):
raise ValueError("工作流流程 ID 格式错误")
if flow_id and flow_id in flow_ids:
raise ValueError(f"工作流流程 ID 重复:{flow_id}")
if flow_id:
flow_ids.add(flow_id)
source = _workflow_flow_endpoint(flow.get("source"), action_ids)
target = _workflow_flow_endpoint(flow.get("target"), action_ids)
outgoing[source].append(target)
incoming[target] += 1
return outgoing, incoming
def validate_workflow_graph(actions: object, flows: object) -> None:
"""
校验工作流有向无环图;允许无动作草稿,但拒绝重复 ID 和无效流程端点。
"""
action_ids = _workflow_action_ids(_workflow_graph_entries(actions, "动作"))
outgoing, incoming = _workflow_graph_edges(_workflow_graph_entries(flows, "流程"), action_ids)
ready = deque(action_id for action_id, degree in incoming.items() if degree == 0)
visited = 0
while ready:
source = ready.popleft()
visited += 1
for target in outgoing[source]:
incoming[target] -= 1
if incoming[target] == 0:
ready.append(target)
if visited != len(action_ids):
raise ValueError("工作流不能包含循环")
@dataclass(frozen=True, slots=True)
class WorkflowSnapshot:
"""
工作流查询返回的脱离数据库会话的冻结快照。
:param id: 工作流持久化主键
:param name: 工作流展示名称
:param description: 工作流用途说明,未填写时为 None
:param timer: 定时触发使用的计划表达式,非定时工作流可为 None
:param trigger_type: 触发类型:timer、event 或 manual,旧记录可能未填写
:param event_type: 事件触发的事件类型名称,非事件工作流可为 None
:param event_conditions: 事件触发时需要匹配的条件映射
:param state: 持久化状态:W 等待、R 运行中、P 暂停、S 成功、F 失败
:param current_action: 已执行动作标识组成的逗号分隔列表,尚未执行时可为 None
:param result: 最近执行结果摘要,尚未产生时为 None
:param run_count: 累计运行次数,旧记录可能未填写
:param actions: 按存储顺序冻结的动作节点定义
:param flows: 连接动作节点的有向流程边定义
:param context: 工作流持久化的业务上下文
:param execution_config: 动作执行时采用的运行配置
:param execution_state: 节点执行和恢复所需的持久化状态
:param add_time: 工作流创建时间字符串,未记录时为 None
:param last_time: 最近运行时间字符串,未运行时为 None
"""
id: int
name: str
description: Optional[str]
timer: Optional[str]
trigger_type: Optional[str]
event_type: Optional[str]
event_conditions: Mapping[str, JsonData]
state: str
current_action: Optional[str]
result: Optional[str]
run_count: Optional[int]
actions: tuple[Mapping[str, JsonData], ...]
flows: tuple[Mapping[str, JsonData], ...]
context: Mapping[str, JsonData]
execution_config: Mapping[str, JsonData]
execution_state: Mapping[str, JsonData]
add_time: Optional[str]
last_time: Optional[str]
class WorkflowRuntime(Protocol):
"""
声明宿主入口与 Chain 消费的工作流运行时能力。
"""
def execute(self, *args: Any, **kwargs: Any) -> Any:
"""
执行单个工作流动作,参数与具体管理器保持一致。
:param args: 位置参数依次为工作流标识、动作定义及可选执行上下文
:param kwargs: 可选动作输入 inputs、运行参数 runtime、取消令牌 cancel_token 等命名参数
:return: 包含动作成功状态、结果信息和后续上下文的执行结果
"""
...
def list_actions(self) -> list[dict[str, Any]]:
"""
返回当前运行时登记的工作流动作定义。
:return: 已注册动作的类型、名称、参数等定义列表
"""
...
def load_workflow_events(self, workflow_id: Optional[int] = None) -> None:
"""
加载全部或指定工作流的事件触发器。
:param workflow_id: 需要重新加载的工作流主键,None 表示加载全部事件工作流
"""
...
def remove_workflow_event(
self,
workflow_id: Optional[int] = None,
event_type_str: Optional[str] = None,
) -> None:
"""
移除全部或指定工作流的事件触发器。
:param workflow_id: 需要移除监听的工作流主键,None 表示移除范围内全部工作流
:param event_type_str: 限定移除的事件类型,None 表示遍历全部已登记事件类型
"""
...
def update_workflow_event(self, workflow: WorkflowSnapshot) -> None:
"""
按最新定义刷新工作流事件触发器。
:param workflow: 已持久化的工作流最新定义快照
"""
...
def register_execution(self, owner: "WorkflowExecutionOwner") -> bool:
"""
原子登记活动执行,同 ID 已在运行或停机封口后返回 False。
:param owner: 当前执行实例的停止、等待和工作流标识能力
:return: 成功登记时为 True,同工作流已有执行或宿主停止接收时为 False
"""
...
def unregister_execution(self, owner: "WorkflowExecutionOwner") -> None:
"""
在工作流执行真实终止后释放 owner。
:param owner: 已真实结束的执行实例;只能释放该实例拥有的活动登记
"""
...
class WorkflowExecutionOwner(Protocol):
"""
声明具体工作流管理器需要持有的执行生命周期能力。
"""
@property
def workflow_id(self) -> int:
"""
返回同一工作流执行互斥使用的稳定 ID。
:return: 用于阻止同一工作流并发重复运行的持久化主键
"""
...
def request_stop(self) -> None:
"""
请求停止继续调度,并通知支持取消的活动动作。
"""
...
def wait_stopped(self, timeout: float) -> bool:
"""
有限等待执行及其节点线程池真实终止。
:param timeout: 最多等待的秒数
:return: 执行及节点线程池已真实结束时为 True,超时仍未结束时为 False
"""
...
WorkflowRuntimeProvider = Callable[[], WorkflowRuntime]
def _unconfigured_workflow_runtime() -> WorkflowRuntime:
"""
拒绝在启动组合根装配前隐式创建工作流管理器。
"""
raise RuntimeError("工作流运行时尚未由启动组合根装配")
_workflow_runtime_provider: WorkflowRuntimeProvider = _unconfigured_workflow_runtime
def configure_workflow_runtime(
provider: Optional[WorkflowRuntimeProvider],
) -> WorkflowRuntimeProvider:
"""
登记工作流运行时 provider,并返回先前 provider 供失败回滚。
"""
global _workflow_runtime_provider
previous = _workflow_runtime_provider
_workflow_runtime_provider = provider or _unconfigured_workflow_runtime
return previous
def reset_workflow_runtime() -> None:
"""
恢复未装配 provider,禁止重复 lifespan 复用旧运行时。
"""
configure_workflow_runtime(None)
def get_workflow_manager() -> WorkflowRuntime:
"""
返回组合根提供的工作流运行时,避免消费者直接定位 Singleton。
"""
return _workflow_runtime_provider()
class WorkflowQueryRepository(Protocol):
"""
工作流查询用例需要的同步与异步快照端口。
"""
def get(self, workflow_id: int) -> Optional[WorkflowSnapshot]:
"""
按 ID 读取工作流快照。
:param workflow_id: 工作流持久化主键
:return: 脱离数据库会话的工作流快照,不存在时为 None
"""
...
def list_enabled(self) -> List[WorkflowSnapshot]:
"""
读取全部启用的工作流快照。
:return: 全部未暂停的工作流快照
"""
...
def list_timer_enabled(self) -> List[WorkflowSnapshot]:
"""
读取启用的定时工作流快照。
:return: 全部未暂停的定时工作流,包含未声明触发类型的旧记录
"""
...
def list_event_enabled(self) -> List[WorkflowSnapshot]:
"""
读取启用的事件工作流快照。
:return: 全部未暂停的事件工作流快照
"""
...
async def async_list(
self,
*,
state: Optional[str] = None,
name: Optional[str] = None,
trigger_type: Optional[str] = None,
page: Optional[int] = None,
count: Optional[int] = None,
) -> List[WorkflowSnapshot]:
"""
按可选筛选与分页窗口异步读取工作流快照。
:param state: 状态筛选:W 等待、R 运行中、P 暂停、S 成功、F 失败;None 表示不限制
:param name: 名称的字面量包含匹配,None 或空字符串表示不限制
:param trigger_type: timer、event 或 manual 触发类型;timer 兼容未声明类型的旧记录,None 不限制
:param page: 页码,从 1 开始;必须与 count 同时提供才启用分页
:param count: 每页条数;None 或未提供 page 时读取全部匹配记录
:return: 按工作流主键排序的匹配快照列表
"""
...
async def async_count(
self,
*,
state: Optional[str] = None,
name: Optional[str] = None,
trigger_type: Optional[str] = None,
) -> int:
"""
按与列表一致的筛选条件统计工作流数量。
:param state: 状态筛选:W 等待、R 运行中、P 暂停、S 成功、F 失败;None 表示不限制
:param name: 名称的字面量包含匹配,None 或空字符串表示不限制
:param trigger_type: timer、event 或 manual 触发类型;timer 兼容未声明类型的旧记录,None 不限制
:return: 符合相同筛选条件的记录总数,不受分页窗口影响
"""
...
async def async_get(self, workflow_id: int) -> Optional[WorkflowSnapshot]:
"""
异步按 ID 读取工作流快照。
:param workflow_id: 工作流持久化主键
:return: 脱离数据库会话的工作流快照,不存在时为 None
"""
...
class WorkflowCachePort(Protocol):
"""
工作流重置所需的同步与异步配置缓存端口。
"""
def delete(self, key: Any) -> Any:
"""
删除配置缓存。
:param key: 要删除的工作流执行配置缓存键
:return: 底层缓存删除结果,调用方不据此判断工作流是否存在
"""
...
async def async_delete(self, key: Any) -> Any:
"""
通过异步数据库执行端口删除配置缓存。
:param key: 要删除的工作流执行配置缓存键
:return: 底层缓存删除结果,调用方不据此判断工作流是否存在
"""
...
class WorkflowQueryService:
"""
提供工作流列表和详情查询,隔离 API 与数据库会话。
"""
def __init__(self, repository: WorkflowQueryRepository) -> None:
"""
保存可返回脱离会话快照的查询端口。
"""
self._repository = repository
async def list(
self,
*,
state: Optional[str] = None,
name: Optional[str] = None,
trigger_type: Optional[str] = None,
page: Optional[int] = None,
count: Optional[int] = None,
) -> List[WorkflowSnapshot]:
"""
按可选筛选与分页窗口返回工作流快照。
"""
if all(
value is None
for value in (state, name, trigger_type, page, count)
):
return await self._repository.async_list()
return await self._repository.async_list(
state=state,
name=name,
trigger_type=trigger_type,
page=page,
count=count,
)
async def count(
self,
*,
state: Optional[str] = None,
name: Optional[str] = None,
trigger_type: Optional[str] = None,
) -> int:
"""
按与列表一致的筛选条件返回工作流总数。
"""
return await self._repository.async_count(
state=state,
name=name,
trigger_type=trigger_type,
)
async def get(self, workflow_id: int) -> Optional[WorkflowSnapshot]:
"""
返回指定工作流快照。
"""
return await self._repository.async_get(workflow_id)
def get_sync(self, workflow_id: int) -> Optional[WorkflowSnapshot]:
"""
同步返回指定工作流快照。
"""
return self._repository.get(workflow_id)
def list_enabled(self) -> List[WorkflowSnapshot]:
"""
同步返回全部启用的工作流快照。
"""
return self._repository.list_enabled()
def list_timer_enabled(self) -> List[WorkflowSnapshot]:
"""
同步返回启用的定时工作流快照。
"""
return self._repository.list_timer_enabled()
def list_event_enabled(self) -> List[WorkflowSnapshot]:
"""
同步返回启用的事件工作流快照。
"""
return self._repository.list_event_enabled()
_configured_workflow_query: WorkflowQueryService | None = None
def configure_workflow_query(service: WorkflowQueryService) -> None:
"""
由启动组合根登记工作流查询服务。
"""
global _configured_workflow_query
_configured_workflow_query = service
def reset_workflow_query() -> None:
"""
清除当前 lifespan 的工作流查询服务。
"""
global _configured_workflow_query
_configured_workflow_query = None
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:
"""
描述工作流写操作是否成功及兼容提示信息。
:param success: 工作流修改是否完成
:param message: 可向用户展示的修改结果或拒绝原因
"""
success: bool
message: str = ""
class WorkflowMutationRepository(Protocol):
"""
工作流写用例需要的最小持久化端口。
"""
def get(self, workflow_id: int) -> Optional[Any]:
"""
读取工作流。
:param workflow_id: 工作流持久化主键
:return: 当前事务内的工作流对象,不存在时为 None
"""
...
def stage_state(self, workflow_id: int, state: str) -> bool:
"""
暂存工作流状态变更。
:param workflow_id: 工作流持久化主键
:param state: 待写入的工作流状态,如 W 启用等待、P 暂停
:return: 找到记录并暂存状态时为 True,不存在时为 False;事务由调用方提交
"""
...
def stage_update(self, workflow_id: int, payload: Mapping[str, Any]) -> Optional[Any]:
"""
暂存工作流定义更新并返回更新后的对象。
:param workflow_id: 工作流持久化主键
:param payload: 已通过用例校验的工作流定义字段,主键不参与修改
:return: 当前事务中更新后的对象,不存在时为 None;本方法不提交事务
"""
...
def stage_delete(self, workflow_id: int) -> None:
"""
暂存工作流删除。
:param workflow_id: 工作流持久化主键
"""
...
class UnitOfWork(Protocol):
"""
同步工作流写用例使用的事务端口。
"""
def commit(self) -> None:
"""
提交当前事务。
"""
...
def rollback(self) -> None:
"""
回滚当前事务。
"""
...
class WorkflowExecutionPort(Protocol):
"""
工作流 Chain 提交执行状态所需的类型化事务端口。
"""
def start(self, workflow_id: int) -> bool:
"""
提交工作流运行中状态。
:param workflow_id: 工作流持久化主键
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,返回前已提交事务
"""
...
def success(
self,
workflow_id: int,
result: Optional[str] = None,
) -> bool:
"""
提交工作流成功状态。
:param workflow_id: 工作流持久化主键
:param result: 执行成功的结果摘要,None 表示没有补充结果
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,返回前已提交事务
"""
...
def fail(self, workflow_id: int, result: str) -> bool:
"""
提交工作流失败状态。
:param workflow_id: 工作流持久化主键
:param result: 执行失败原因摘要
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,返回前已提交事务
"""
...
def step(
self,
workflow_id: int,
action_id: str,
context: dict[str, Any],
execution_state: Optional[dict[str, Any]] = None,
) -> bool:
"""
提交工作流动作进度。
:param workflow_id: 工作流持久化主键
:param action_id: 本次已执行的动作标识,追加到去重后的已执行动作列表
:param context: 本次动作完成后的业务上下文快照
:param execution_state: 结构化节点执行状态,None 表示保留已有状态
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,返回前已提交事务
"""
...
def reset(self, workflow_id: int, reset_count: bool = False) -> bool:
"""
提交工作流执行状态重置。
:param workflow_id: 工作流持久化主键
:param reset_count: 是否同时将累计运行次数归零,默认保留次数
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,返回前已提交事务
"""
...
_configured_workflow_execution: Optional[WorkflowExecutionPort] = None
def configure_workflow_execution(service: WorkflowExecutionPort) -> None:
"""
由启动组合根登记唯一工作流执行状态事务服务。
"""
global _configured_workflow_execution
_configured_workflow_execution = service
def reset_workflow_execution() -> None:
"""
清除当前 lifespan 的工作流执行状态事务服务。
"""
global _configured_workflow_execution
_configured_workflow_execution = None
def get_configured_workflow_execution() -> WorkflowExecutionPort:
"""
返回启动阶段登记的工作流执行状态事务服务。
"""
if _configured_workflow_execution is None:
raise RuntimeError("工作流执行状态事务服务尚未配置")
return _configured_workflow_execution
class WorkflowExecutionRepository(Protocol):
"""
工作流执行状态写入所需的最小暂存端口。
"""
def stage_start(self, workflow_id: int) -> bool:
"""
暂存运行中状态。
:param workflow_id: 工作流持久化主键
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,事务由调用方提交
"""
...
def stage_success(
self,
workflow_id: int,
result: Optional[str] = None,
) -> bool:
"""
暂存成功状态和执行次数。
:param workflow_id: 工作流持久化主键
:param result: 执行成功的结果摘要,None 表示没有补充结果
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,事务由调用方提交
"""
...
def stage_fail(self, workflow_id: int, result: str) -> bool:
"""
暂存失败状态和错误信息。
:param workflow_id: 工作流持久化主键
:param result: 执行失败原因摘要
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,事务由调用方提交
"""
...
def stage_step(
self,
workflow_id: int,
action_id: str,
context: dict[str, Any],
execution_state: Optional[dict[str, Any]] = None,
) -> bool:
"""
暂存动作进度和执行上下文。
:param workflow_id: 工作流持久化主键
:param action_id: 本次已执行的动作标识,追加到去重后的已执行动作列表
:param context: 本次动作完成后的业务上下文快照
:param execution_state: 结构化节点执行状态,None 表示保留已有状态
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,事务由调用方提交
"""
...
def stage_execution_reset(
self,
workflow_id: int,
reset_count: bool = False,
) -> bool:
"""
暂存执行状态重置。
:param workflow_id: 工作流持久化主键
:param reset_count: 是否同时将累计运行次数归零,默认保留次数
:return: 状态写入操作已完成时为 True;不表示数据库中一定存在匹配记录,事务由调用方提交
"""
...
_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, "工作流不存在")
try:
validate_workflow_graph(
values.get("actions", getattr(current, "actions", None)),
values.get("flows", getattr(current, "flows", None)),
)
except ValueError as err:
return WorkflowMutationResult(False, str(err))
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 and getattr(updated, "state", None) != "P":
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]:
"""
按名称读取工作流。
:param name: 用于同名校验的完整工作流名称
:return: 名称匹配的工作流对象,不存在时为 None
"""
...
async def stage_create(self, payload: Mapping[str, Any]) -> Any:
"""
暂存新工作流。
:param payload: 已校验的工作流名称、图定义和触发配置等创建字段
:return: 已暂存并取得主键的新工作流对象,事务由调用方提交
"""
...
async def stage_reset(self, workflow_id: int, reset_count: bool = False) -> Optional[Any]:
"""
暂存工作流重置。
:param workflow_id: 工作流持久化主键
:param reset_count: 是否同时将累计运行次数归零,默认保留次数
:return: 清空执行进度、上下文并置为等待状态后的对象,不存在时为 None;不提交事务
"""
...
async def async_get(self, workflow_id: int) -> Optional[Any]:
"""
读取指定工作流。
:param workflow_id: 工作流持久化主键
:return: 当前事务中的工作流对象,不存在时为 None
"""
...
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)
try:
validate_workflow_graph(values.get("actions"), values.get("flows"))
except ValueError as err:
return WorkflowMutationResult(False, str(err))
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:
"""
解析共享工作流内容并在提交后更新远程复用次数。
只复用动作、流程和触发配置;执行上下文属于分享者本机的运行状态,对新工作流没有意义,
且来自外部不可信,因此忽略载荷中的 ``context``,新工作流固定从空上下文开始。
"""
values = dict(payload)
if not values.get("name"):
return WorkflowMutationResult(False, "工作流名称不能为空")
parsed = {}
for field, default, error_message in (
("actions", "[]", "actions字段JSON格式错误"),
("flows", "[]", "flows字段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": {},
"state": "P",
}
try:
validate_workflow_graph(workflow_values["actions"], workflow_values["flows"])
except ValueError as err:
return WorkflowMutationResult(False, str(err))
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