chore(repo): initialize team collaboration repository
CI / Python 3.12 (push) Waiting to run
CI / Python 3.9 (push) Waiting to run

This commit is contained in:
2026-07-27 20:40:12 +08:00
commit c91a64fddb
109 changed files with 21121 additions and 0 deletions
@@ -0,0 +1,5 @@
"""无 I/O 的编排规则。"""
from .states import IllegalTransition, assert_run_transition
__all__ = ["IllegalTransition", "assert_run_transition"]
@@ -0,0 +1,159 @@
"""Materialize a published Workflow snapshot into sequential RunStep rows.
The planner deliberately supports a small, deterministic subset of the workflow
schema. It never evaluates expressions and it never turns free-form text into a
command. Host Agents only receive already-published adapter/template references.
"""
from __future__ import annotations
import re
from typing import Any, Dict, List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from ..config import get_settings
from ..enums import OnFail, StepType
from ..models import Run, RunStep, Workflow
_PARAM = re.compile(r"^\{\{\s*params\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}$")
_LOOP_INDEX = re.compile(r"^\{\{\s*loop\.index\s*\}\}$")
_HOST_STEP_TYPES = {
StepType.COMMAND.value,
StepType.DEVICE_CHECK.value,
StepType.WAIT.value,
}
class WorkflowPlanningError(ValueError):
"""The published workflow cannot be safely materialized."""
def _resolve(value: Any, run_params: Dict[str, Any], loop_index: Optional[int]) -> Any:
"""Resolve only exact, allow-listed template tokens; no expression evaluation."""
if isinstance(value, str):
parameter = _PARAM.fullmatch(value)
if parameter:
key = parameter.group(1)
if key not in run_params:
raise WorkflowPlanningError("工作流参数不存在: {0}".format(key))
return run_params[key]
if _LOOP_INDEX.fullmatch(value):
if loop_index is None:
raise WorkflowPlanningError("loop.index 只能在循环体内使用")
return loop_index
if "{{" in value or "}}" in value:
raise WorkflowPlanningError("模板只允许完整的 params.X 或 loop.index 占位符")
return value
if isinstance(value, dict):
return {key: _resolve(item, run_params, loop_index) for key, item in value.items()}
if isinstance(value, list):
return [_resolve(item, run_params, loop_index) for item in value]
return value
def _step_row(
*,
run: Run,
spec: Dict[str, Any],
seq: int,
loop_index: Optional[int],
checkpoint_after: bool,
) -> RunStep:
step_type = str(spec.get("type", ""))
if step_type not in _HOST_STEP_TYPES:
raise WorkflowPlanningError("Host Agent 不支持步骤类型: {0}".format(step_type or "<empty>"))
key = str(spec.get("key", "")).strip()
adapter = str(spec.get("adapter", "")).strip()
if not key or not adapter:
raise WorkflowPlanningError("步骤 key 与 adapter 不能为空")
retry = int(spec.get("retry", 0))
timeout_s = float(spec.get("timeout_s", get_settings().timing.step_default_timeout_s))
if retry < 0 or retry > 20:
raise WorkflowPlanningError("步骤 retry 必须在 0..20 之间")
if timeout_s <= 0:
raise WorkflowPlanningError("步骤 timeout_s 必须大于 0")
return RunStep(
run_id=run.id,
seq=seq,
loop_index=loop_index,
step_key=key,
step_type=step_type,
adapter=adapter,
template=spec.get("template"),
params=_resolve(dict(spec.get("params") or {}), run.params, loop_index),
max_retry=retry,
timeout_s=timeout_s,
idempotent=bool(spec.get("idempotent", False)),
danger=str(spec.get("danger", "none")),
on_fail=str(spec.get("on_fail", OnFail.RETRY.value)),
checkpoint_after=checkpoint_after,
)
def plan_run_steps(run: Run, workflow: Workflow) -> List[RunStep]:
"""Expand one workflow snapshot into the exact ordered steps for a Run."""
if not workflow.published:
raise WorkflowPlanningError("工作流尚未发布")
raw_steps = workflow.spec.get("steps") if isinstance(workflow.spec, dict) else None
if not isinstance(raw_steps, list) or not raw_steps:
raise WorkflowPlanningError("工作流没有可执行步骤")
planned: List[RunStep] = []
seq = 1
for raw in raw_steps:
if not isinstance(raw, dict):
raise WorkflowPlanningError("工作流步骤必须是 object")
step_type = str(raw.get("type", ""))
# Safety preflight and evidence/report generation live in the control plane.
if step_type in {StepType.PRECHECK.value, StepType.REPORT.value}:
continue
if step_type == StepType.LOOP.value:
body = raw.get("body")
if not isinstance(body, list) or not body:
raise WorkflowPlanningError("循环步骤必须包含非空 body")
for loop_index in range(1, run.loop_target + 1):
for index, child in enumerate(body):
if not isinstance(child, dict):
raise WorkflowPlanningError("循环体步骤必须是 object")
planned.append(
_step_row(
run=run,
spec=child,
seq=seq,
loop_index=loop_index,
checkpoint_after=bool(child.get("checkpoint", False))
or (bool(raw.get("checkpoint", False)) and index == len(body) - 1),
)
)
seq += 1
continue
planned.append(
_step_row(
run=run,
spec=raw,
seq=seq,
loop_index=None,
checkpoint_after=bool(raw.get("checkpoint", False)),
)
)
seq += 1
if not planned:
raise WorkflowPlanningError("工作流没有 Host Agent 可执行步骤")
return planned
async def materialize_run_steps(
session: AsyncSession, run: Run, workflow: Workflow
) -> List[RunStep]:
planned = plan_run_steps(run, workflow)
session.add_all(planned)
await session.flush()
return planned
@@ -0,0 +1,37 @@
"""五级恢复阶梯的纯函数决策。"""
from __future__ import annotations
from dataclasses import dataclass
from ..enums import RecoveryLevel, RecoveryTrigger
@dataclass(frozen=True)
class RecoveryContext:
trigger: RecoveryTrigger
attempt: int
agent_online: bool
host_network_online: bool
oob_online: bool
def next_recovery_level(context: RecoveryContext) -> RecoveryLevel:
if context.trigger == RecoveryTrigger.DATA_INTEGRITY:
return RecoveryLevel.FREEZE
ladder = []
if context.agent_online:
ladder.append(RecoveryLevel.AGENT_SOFT)
if context.host_network_online:
ladder.append(RecoveryLevel.OS_REBOOT)
if context.oob_online:
ladder.extend(
[
RecoveryLevel.OOB_RESET,
RecoveryLevel.OOB_ATX_POWER,
RecoveryLevel.OOB_AC_CYCLE,
]
)
if context.attempt >= len(ladder):
return RecoveryLevel.FREEZE
return ladder[context.attempt]
@@ -0,0 +1,44 @@
"""Run 状态转移表。"""
from __future__ import annotations
from typing import Dict, FrozenSet
from ..enums import RunState
class IllegalTransition(ValueError):
pass
RUN_TRANSITIONS: Dict[RunState, FrozenSet[RunState]] = {
RunState.QUEUED: frozenset({RunState.PREFLIGHT, RunState.ABORTED}),
RunState.PREFLIGHT: frozenset(
{RunState.RUNNING, RunState.REJECTED, RunState.ABORTED}
),
RunState.RUNNING: frozenset(
{
RunState.RECOVERING,
RunState.PAUSED,
RunState.FROZEN,
RunState.COMPLETED,
RunState.ABORTED,
}
),
RunState.RECOVERING: frozenset(
{RunState.RUNNING, RunState.FROZEN, RunState.ABORTED}
),
RunState.PAUSED: frozenset({RunState.RUNNING, RunState.ABORTED}),
RunState.FROZEN: frozenset(),
RunState.COMPLETED: frozenset(),
RunState.ABORTED: frozenset(),
RunState.REJECTED: frozenset(),
}
def assert_run_transition(current: str, target: str) -> None:
current_state = RunState(current)
target_state = RunState(target)
if target_state not in RUN_TRANSITIONS[current_state]:
raise IllegalTransition(
"Run 状态不允许从 {0} 转到 {1}".format(current_state.value, target_state.value)
)