mirror of
https://github.com/cita-777/metapi.git
synced 2026-10-31 19:47:31 +08:00
216 lines
6.8 KiB
Python
Executable File
216 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Workflow 阶段提取。
|
||
|
||
从 .trellis/workflow.md 提取步骤级内容,并按需筛选平台专属区块。
|
||
|
||
workflow.md 中的平台标记语法:
|
||
|
||
[Claude Code, Cursor, ...]
|
||
支持 agent 的内容
|
||
[/Claude Code, Cursor, ...]
|
||
|
||
提供:
|
||
get_phase_index - 提取 Phase Index 段落(不传 --step)
|
||
get_step - 提取单个步骤(#### X.X)段落
|
||
filter_platform - 删除不包含指定平台名称的平台区块
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
from .paths import DIR_WORKFLOW, get_repo_root
|
||
|
||
|
||
def _workflow_md_path():
|
||
return get_repo_root() / DIR_WORKFLOW / "workflow.md"
|
||
|
||
# 匹配平台标记行:"[A, B, C]" 或 "[/A, B, C]"。
|
||
_MARKER_RE = re.compile(r"^\[(/?)([A-Za-z][^\[\]]*)\]\s*$")
|
||
|
||
# 步骤标题:"#### 1.0 标题" 或 "#### 1.0 ..."。
|
||
_STEP_HEADING_RE = re.compile(r"^####\s+(\d+\.\d+)\b.*$")
|
||
|
||
# Phase Index 从这里开始;后面是 Phase 1/2/3 步骤正文,至 Breadcrumbs 结束。
|
||
_PHASE_INDEX_HEADING = "## Phase Index"
|
||
|
||
|
||
def _read_workflow() -> str:
|
||
path = _workflow_md_path()
|
||
if not path.exists():
|
||
raise FileNotFoundError(f"找不到 workflow.md:{path}")
|
||
return path.read_text(encoding="utf-8")
|
||
|
||
|
||
def _parse_marker(line: str) -> tuple[bool, list[str]] | None:
|
||
"""解析平台标记行。
|
||
|
||
返回:
|
||
如果是标记,返回 (is_closing, [platform_names]);否则返回 None。
|
||
"""
|
||
m = _MARKER_RE.match(line)
|
||
if not m:
|
||
return None
|
||
is_closing = m.group(1) == "/"
|
||
names = [p.strip() for p in m.group(2).split(",") if p.strip()]
|
||
return is_closing, names
|
||
|
||
|
||
def get_phase_index() -> str:
|
||
"""返回 workflow.md 中精简的 Phase Index 摘要。
|
||
|
||
SessionStart 和未指定步骤的 phase context 使用此摘要定位。Phase 1/2/3 的详细
|
||
指令通过 ``get_step`` 按需加载。``[workflow-state:STATUS]`` 标签块由每轮 hook
|
||
单独消费,因此从此输出中移除。
|
||
"""
|
||
text = _read_workflow()
|
||
lines = text.splitlines()
|
||
|
||
start: int | None = None
|
||
end: int | None = None
|
||
for i, line in enumerate(lines):
|
||
stripped = line.strip()
|
||
if start is None and stripped == _PHASE_INDEX_HEADING:
|
||
start = i
|
||
continue
|
||
if start is not None and stripped == "## Phase 1: Plan":
|
||
end = i
|
||
break
|
||
|
||
if start is None:
|
||
return ""
|
||
if end is None:
|
||
end = len(lines)
|
||
|
||
section = "\n".join(lines[start:end]).rstrip()
|
||
# 移除 [workflow-state:STATUS]...[/workflow-state:STATUS] 区块;这些区块由
|
||
# inject-workflow-state.py 在每轮单独注入。
|
||
import re as _re
|
||
tag_re = _re.compile(
|
||
r"\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n.*?\n\s*\[/workflow-state:\1\]\n?",
|
||
_re.DOTALL,
|
||
)
|
||
return tag_re.sub("", section).rstrip() + "\n"
|
||
|
||
|
||
def get_step(step_id: str) -> str:
|
||
"""返回与 step_id 匹配的 `#### X.X` 段落(标题和正文)。
|
||
|
||
正文在下一个 `####`、`---` 或 `##` 标题处结束(取最先出现者)。
|
||
"""
|
||
text = _read_workflow()
|
||
lines = text.splitlines()
|
||
|
||
start: int | None = None
|
||
for i, line in enumerate(lines):
|
||
m = _STEP_HEADING_RE.match(line)
|
||
if m and m.group(1) == step_id:
|
||
start = i
|
||
break
|
||
if start is None:
|
||
return ""
|
||
|
||
end: int = len(lines)
|
||
for j in range(start + 1, len(lines)):
|
||
line = lines[j]
|
||
if line.startswith("#### "):
|
||
end = j
|
||
break
|
||
if line.startswith("## "):
|
||
end = j
|
||
break
|
||
# 第 0 列的水平分隔线。
|
||
if line.strip() == "---":
|
||
end = j
|
||
break
|
||
|
||
return "\n".join(lines[start:end]).rstrip() + "\n"
|
||
|
||
|
||
def _platform_matches(platform: str, block_names: list[str]) -> bool:
|
||
"""不区分大小写并兼容连字符/空格,例如 cursor、Cursor、claude-code、Claude Code。"""
|
||
needle = platform.lower().replace("-", "").replace("_", "").replace(" ", "")
|
||
for name in block_names:
|
||
hay = name.lower().replace("-", "").replace("_", "").replace(" ", "")
|
||
if needle == hay:
|
||
return True
|
||
return False
|
||
|
||
|
||
def resolve_effective_platform(platform: str, config: dict) -> str:
|
||
"""将 ``codex`` 映射为带 dispatch mode 命名空间的虚拟平台名称。
|
||
|
||
传入 ``--platform codex`` 时,默认返回 ``"codex-sub-agent"``;在
|
||
``.trellis/config.yaml`` 明确配置时返回 ``"codex-inline"``。
|
||
``sub-agent`` 仍是 ``auto`` 的别名。随后 ``filter_platform`` 只保留标记列表中
|
||
包含该命名空间名称的区块(例如 ``[codex-sub-agent, ...]`` 或
|
||
``[codex-inline, Kilo, Antigravity, Devin]``)。
|
||
|
||
Codex 原生上下文注入支持 ``auto`` 默认值。非法显式值安全回退到 ``inline``;
|
||
此 renderer 可能在普通 CLI 输出流程中运行,因此有意不发出 warning。
|
||
|
||
其他平台原样返回。
|
||
"""
|
||
if platform == "codex":
|
||
mode = "auto"
|
||
codex_cfg = config.get("codex") if isinstance(config, dict) else None
|
||
if codex_cfg is not None:
|
||
if not isinstance(codex_cfg, dict):
|
||
mode = "inline"
|
||
else:
|
||
cfg_mode = str(codex_cfg.get("dispatch_mode", mode)).strip().lower()
|
||
if cfg_mode == "inline":
|
||
mode = "inline"
|
||
elif cfg_mode in ("auto", "sub-agent"):
|
||
mode = "auto"
|
||
else:
|
||
mode = "inline"
|
||
return "codex-sub-agent" if mode == "auto" else "codex-inline"
|
||
return platform
|
||
|
||
|
||
def filter_platform(content: str, platform: str) -> str:
|
||
"""保留所有 ``[...]`` 区块外的行,以及包含指定平台的区块内行。
|
||
|
||
标记行本身会从输出中删除。
|
||
"""
|
||
lines = content.splitlines()
|
||
out: list[str] = []
|
||
|
||
in_block = False
|
||
keep_block = False
|
||
|
||
for line in lines:
|
||
marker = _parse_marker(line)
|
||
if marker is not None:
|
||
is_closing, names = marker
|
||
if not is_closing:
|
||
in_block = True
|
||
keep_block = _platform_matches(platform, names)
|
||
else:
|
||
in_block = False
|
||
keep_block = False
|
||
continue # 不把标记行本身写入输出。
|
||
|
||
if in_block:
|
||
if keep_block:
|
||
out.append(line)
|
||
continue
|
||
out.append(line)
|
||
|
||
# 压缩因移除标记而产生的连续空行(超过 2 行时折叠)。
|
||
collapsed: list[str] = []
|
||
blank_run = 0
|
||
for line in out:
|
||
if line.strip() == "":
|
||
blank_run += 1
|
||
if blank_run <= 2:
|
||
collapsed.append(line)
|
||
else:
|
||
blank_run = 0
|
||
collapsed.append(line)
|
||
|
||
return "\n".join(collapsed).rstrip() + "\n"
|