PMFlow/backend/app/services/task_brief/mixin_templates.py

191 lines
6.5 KiB
Python

from __future__ import annotations
from .shared import *
class TaskBriefTemplatesMixin:
@classmethod
def _project_root_candidates(cls) -> list[Path]:
service_file = Path(__file__).resolve()
cwd = Path.cwd().resolve()
raw_candidates = [
service_file.parents[3],
service_file.parents[2],
cwd,
cwd.parent,
]
deduped: list[Path] = []
seen: set[str] = set()
for path in raw_candidates:
key = str(path)
if key in seen:
continue
seen.add(key)
deduped.append(path)
return deduped
@staticmethod
def _safe_dict(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
try:
# 深拷贝,避免浅拷贝触发 JSON 嵌套对象原地修改导致的“提交不落库”。
return json.loads(json.dumps(value))
except Exception:
return dict(value)
if isinstance(value, str):
try:
parsed = json.loads(value)
return parsed if isinstance(parsed, dict) else {}
except Exception:
return {}
return {}
@classmethod
def _project_root(cls) -> Path:
for root in cls._project_root_candidates():
if (root / "docs" / "architecture").exists():
return root
return cls._project_root_candidates()[0]
@staticmethod
def _normalize_template_text(content: str) -> str:
return "\n".join(line.rstrip() for line in str(content or "").strip().splitlines()).strip()
@classmethod
def _is_default_template_fallback(cls, content: str) -> bool:
return cls._normalize_template_text(content) == cls._normalize_template_text(DEFAULT_TEMPLATE_FALLBACK)
@classmethod
def _load_default_template_content(cls) -> str:
template_path = cls._project_root() / "docs/architecture/TASK_BRIEF_TEMPLATE.md"
if template_path.exists():
try:
return template_path.read_text(encoding="utf-8").strip() or DEFAULT_TEMPLATE_FALLBACK
except Exception:
return DEFAULT_TEMPLATE_FALLBACK
return DEFAULT_TEMPLATE_FALLBACK
@classmethod
def _resolve_git_ref(cls, ref: str) -> str:
try:
output = subprocess.check_output(
["git", "rev-parse", "--short", ref],
cwd=str(cls._project_root()),
stderr=subprocess.DEVNULL,
text=True,
timeout=1.5,
).strip()
return output or "-"
except Exception:
return "-"
@staticmethod
def _to_markdown_list(items: list[str], *, empty_text: str) -> str:
cleaned = [str(item).strip() for item in items if str(item).strip()]
if not cleaned:
return f"- {empty_text}"
return "\n".join(f"- {item}" for item in cleaned)
@classmethod
def _render_template_content(
cls,
*,
template_content: str,
generated_at: str,
base_ref: str,
head_ref: str,
changed_files: list[str],
scope_summary: str,
risks: list[str],
) -> str:
content = str(template_content or "").strip() or cls._load_default_template_content()
replacements = {
"{{GENERATED_AT}}": str(generated_at or "-"),
"{{BASE_REF}}": str(base_ref or "-"),
"{{HEAD_REF}}": str(head_ref or "-"),
"{{CHANGED_FILES}}": cls._to_markdown_list(changed_files, empty_text="(none)"),
"{{SCOPE_SUMMARY}}": str(scope_summary or "-"),
"{{RISKS}}": cls._to_markdown_list(risks, empty_text="待补充风险评估"),
}
rendered = content
for token, value in replacements.items():
rendered = rendered.replace(token, value)
return rendered.strip()
@classmethod
def _build_default_template(cls) -> dict[str, Any]:
now_text = now_cn_naive().isoformat()
return {
"id": DEFAULT_TEMPLATE_ID,
"name": DEFAULT_TEMPLATE_NAME,
"description": DEFAULT_TEMPLATE_DESCRIPTION,
"content": cls._load_default_template_content(),
"updated_at": now_text,
"updated_by": 0,
"created_at": now_text,
}
@staticmethod
def _build_template_item(
*,
template_id: str,
name: str,
description: str,
content: str,
now_text: str,
) -> dict[str, Any]:
return {
"id": template_id,
"name": name,
"description": description,
"content": content,
"updated_at": now_text,
"updated_by": 0,
"created_at": now_text,
}
@classmethod
def _build_system_templates(cls) -> list[dict[str, Any]]:
now_text = now_cn_naive().isoformat()
return [
cls._build_default_template(),
cls._build_template_item(
template_id=LOW_TEMPLATE_ID,
name=LOW_TEMPLATE_NAME,
description=LOW_TEMPLATE_DESCRIPTION,
content=LOW_TEMPLATE_CONTENT,
now_text=now_text,
),
cls._build_template_item(
template_id=HIGH_TEMPLATE_ID,
name=HIGH_TEMPLATE_NAME,
description=HIGH_TEMPLATE_DESCRIPTION,
content=HIGH_TEMPLATE_CONTENT,
now_text=now_text,
),
cls._build_template_item(
template_id=HIGH_CONTRACT_TEMPLATE_ID,
name=HIGH_CONTRACT_TEMPLATE_NAME,
description=HIGH_CONTRACT_TEMPLATE_DESCRIPTION,
content=HIGH_CONTRACT_TEMPLATE_CONTENT,
now_text=now_text,
),
]
@classmethod
def _ensure_system_templates(
cls,
templates: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], bool]:
merged = [item for item in templates if isinstance(item, dict)]
known_ids = {str(item.get("id") or "").strip() for item in merged if str(item.get("id") or "").strip()}
changed = False
for preset in cls._build_system_templates():
template_id = str(preset.get("id") or "").strip()
if not template_id or template_id in known_ids:
continue
merged.append(preset)
known_ids.add(template_id)
changed = True
return merged, changed