feat: expand module workflows, dynamic config, notifications and console API

This commit is contained in:
2026-09-04 09:49:37 +08:00
parent 8df5266abb
commit b124d757b0
309 changed files with 89358 additions and 6232 deletions
+52 -1
View File
@@ -42,6 +42,15 @@ class WorkflowDataFlow:
destinations: tuple[WorkflowDataEndpoint, ...]
@dataclass(frozen=True, slots=True)
class WorkflowTopicConfig:
"""Operator-facing topic input metadata for a workflow."""
default_keyword: str
label: str = "话题关键词"
hint: str = "输入完整话题或关键词,系统会在搜索结果中按包含关系选中。"
@dataclass(frozen=True, slots=True)
class WorkflowStepEntry:
step_id: str
@@ -52,8 +61,9 @@ class WorkflowStepEntry:
depends_on: tuple[str, ...] = ()
run_after_failure: bool = False
timeout_seconds: float | None = None
replay_policy: Literal["guarded", "idempotent"] | None = None
replay_policy: Literal["guarded", "idempotent", "repeatable"] | None = None
data_flow: WorkflowDataFlow | None = None
has_hyperlink: bool = False
@dataclass(frozen=True, slots=True)
@@ -67,6 +77,8 @@ class WorkflowEntry:
source_task_name: str | None = None
note: str | None = None
steps: tuple[WorkflowStepEntry, ...] = ()
notification_workflow_id: str | None = None
topic_config: WorkflowTopicConfig | None = None
@dataclass(frozen=True, slots=True)
@@ -205,6 +217,7 @@ def _parse_workflow(item: Any) -> WorkflowEntry:
task_name = provenance.get("task_name")
if trigger == "scheduled" and (not isinstance(task_name, str) or not task_name):
raise CatalogError(f"scheduled workflow requires source task name: {workflow_id}")
topic_config = _parse_topic_config(item.get("topic_config"), workflow_id)
return WorkflowEntry(
workflow_id=workflow_id,
module=module,
@@ -215,6 +228,37 @@ def _parse_workflow(item: Any) -> WorkflowEntry:
source_task_name=task_name,
note=item.get("note"),
steps=steps,
topic_config=topic_config,
)
def _parse_topic_config(value: Any, workflow_id: str) -> WorkflowTopicConfig | None:
if value is None:
return None
if not isinstance(value, dict):
raise CatalogError(f"topic_config must be an object for {workflow_id}")
default_keyword = value.get("default_keyword")
if (
not isinstance(default_keyword, str)
or not default_keyword.strip()
or len(default_keyword.strip()) > 120
):
raise CatalogError(
f"topic_config default_keyword is invalid for {workflow_id}"
)
fields: dict[str, str] = {}
for field, fallback in (
("label", "话题关键词"),
("hint", "输入完整话题或关键词,系统会在搜索结果中按包含关系选中。"),
):
raw = value.get(field, fallback)
if not isinstance(raw, str) or not raw.strip() or len(raw.strip()) > 240:
raise CatalogError(f"topic_config {field} is invalid for {workflow_id}")
fields[field] = raw.strip()
return WorkflowTopicConfig(
default_keyword=default_keyword.strip(),
label=fields["label"],
hint=fields["hint"],
)
@@ -263,6 +307,7 @@ def _parse_workflow_steps(
if replay_policy is not None and replay_policy not in {
"guarded",
"idempotent",
"repeatable",
}:
raise CatalogError(
f"workflow step replay_policy is invalid for {workflow_id}.{step_id}"
@@ -279,12 +324,18 @@ def _parse_workflow_steps(
raise CatalogError(
f"workflow step description is invalid for {workflow_id}.{step_id}"
)
has_hyperlink = raw_step.get("has_hyperlink", False)
if not isinstance(has_hyperlink, bool):
raise CatalogError(
f"workflow step has_hyperlink is invalid for {workflow_id}.{step_id}"
)
steps.append(
WorkflowStepEntry(
step_id=step_id,
entry=entry,
name=name.strip() if name is not None else None,
description=description.strip() if description is not None else None,
has_hyperlink=has_hyperlink,
args=_parse_args(raw_step.get("args", []), workflow_id),
depends_on=tuple(depends_on),
run_after_failure=run_after_failure,