fc / apps /documents /services /code_placeholders /catalog_service.py
jun2024's picture
Upload 2241 files
b4f2238 verified
Raw History Blame Contribute Delete
8.62 kB
"""Business logic services."""
import ast
import logging
import re
from functools import lru_cache
from pathlib import Path
from typing import Any
from .registry import CodePlaceholderDefinition, CodePlaceholderRegistry
logger = logging.getLogger(__name__)
class CodePlaceholderCatalogService:
_KEY_PATTERN = re.compile(r"^[\w\u4e00-\u9fff][\w\u4e00-\u9fff\.\(\)]*$")
def list_definitions(self) -> list[CodePlaceholderDefinition]:
definitions: dict[str, CodePlaceholderDefinition] = {}
for definition in self._from_code_registry():
definitions.setdefault(definition.key, definition)
for definition in self._from_registry():
definitions.setdefault(definition.key, definition)
for definition in self._from_spec_files_scan():
definitions.setdefault(definition.key, definition)
for definition in self._from_evidence_list():
definitions.setdefault(definition.key, definition)
for definition in self._from_generation_ast_scan():
definitions.setdefault(definition.key, definition)
return sorted(definitions.values(), key=lambda d: d.key)
def get_definition(self, key: str) -> CodePlaceholderDefinition | None:
for definition in self.list_definitions():
if definition.key == key:
return definition
return None
def list_keys(self) -> list[str]:
return [d.key for d in self.list_definitions()]
def _from_code_registry(self) -> list[CodePlaceholderDefinition]:
return CodePlaceholderRegistry().list_definitions()
def _from_registry(self) -> list[CodePlaceholderDefinition]:
from apps.documents.services.placeholders import PlaceholderRegistry
registry = PlaceholderRegistry()
result: list[CodePlaceholderDefinition] = []
for service in registry.get_all_services():
source = getattr(service, "display_name", "") or getattr(service, "name", "") or service.__class__.__name__
category = getattr(service, "category", "") or "general"
service_description = getattr(service, "description", "") or ""
metadata: dict[str, Any] = {}
if hasattr(service, "get_placeholder_metadata"):
metadata = service.get_placeholder_metadata() or {}
for key in service.get_placeholder_keys():
key_meta = metadata.get(key, {}) or {}
result.append(
CodePlaceholderDefinition(
key=key,
source=source,
category=category,
display_name=key_meta.get("display_name") or "",
description=key_meta.get("description") or service_description,
example_value=key_meta.get("example_value") or "",
)
)
return result
def _from_spec_files_scan(self) -> list[CodePlaceholderDefinition]:
apps_root = Path(__file__).resolve().parents[2]
if not apps_root.exists():
return []
return _scan_placeholder_spec_files(apps_root)
def _from_evidence_list(self) -> list[CodePlaceholderDefinition]:
from apps.documents.services.evidence.evidence_list_placeholder_service import EvidenceListPlaceholderService
service = EvidenceListPlaceholderService()
keys = list(service.get_placeholder_keys() or [])
return [
CodePlaceholderDefinition(
key=key,
source="证据清单",
category="evidence",
display_name=key,
description="证据清单导出占位符",
)
for key in keys
]
def _from_generation_ast_scan(self) -> list[CodePlaceholderDefinition]:
root = Path(__file__).resolve().parents[1] / "services"
if not root.exists():
return []
keys = self._scan_python_files_for_context_keys(root)
result: list[CodePlaceholderDefinition] = []
for key in sorted(keys):
result.append(
CodePlaceholderDefinition(
key=key,
source="生成器扫描",
category="generated",
display_name=key,
description="从生成器代码中自动提取的上下文键",
)
)
return result
def _scan_python_files_for_context_keys(self, root: Path) -> set[str]:
collected: set[str] = set()
for file_path in root.rglob("*.py"):
try:
content = file_path.read_text(encoding="utf-8")
except Exception:
logger.exception("操作失败")
continue
if "DocxTemplate" not in content and "docxtpl" not in content:
continue
try:
tree = ast.parse(content, filename=str(file_path))
except SyntaxError:
continue
visitor = _ContextDictKeyVisitor()
visitor.visit(tree)
for key in visitor.keys:
if not self._is_placeholder_key_candidate(key):
continue
if not self._looks_like_template_placeholder(key):
continue
collected.add(key)
return collected
def _is_placeholder_key_candidate(self, key: str) -> bool:
return bool(key and self._KEY_PATTERN.match(key))
def _looks_like_template_placeholder(self, key: str) -> bool:
return any("\u4e00" <= ch <= "\u9fff" for ch in key)
class _ContextDictKeyVisitor(ast.NodeVisitor):
def __init__(self) -> None:
self.keys: set[str] = set()
def visit_Return(self, node: ast.Return) -> None:
self._collect_from_value(node.value)
self.generic_visit(node)
def visit_Assign(self, node: ast.Assign) -> None:
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "context":
self._collect_from_value(node.value)
self.generic_visit(node)
def _collect_from_value(self, value: ast.AST | None) -> None:
if isinstance(value, ast.Dict):
for k in value.keys:
if isinstance(k, ast.Constant) and isinstance(k.value, str):
self.keys.add(k.value)
@lru_cache(maxsize=1)
def _scan_placeholder_spec_files(apps_root: Path) -> list[CodePlaceholderDefinition]:
result: list[CodePlaceholderDefinition] = []
for spec_path in apps_root.rglob("placeholders/spec.py"):
result.extend(_extract_definitions_from_spec(spec_path))
dedup: dict[str, CodePlaceholderDefinition] = {}
for definition in result:
dedup.setdefault(definition.key, definition)
return list(dedup.values())
def _extract_definitions_from_spec(spec_path: Path) -> list[CodePlaceholderDefinition]:
try:
content = spec_path.read_text(encoding="utf-8")
except Exception:
logger.exception("操作失败")
return []
try:
tree = ast.parse(content, filename=str(spec_path))
except SyntaxError:
return []
source, category, description = _spec_metadata(spec_path)
definitions: list[CodePlaceholderDefinition] = []
for node in tree.body:
if not isinstance(node, ast.ClassDef) or not node.name.endswith("PlaceholderKeys"):
continue
for stmt in node.body:
key = _extract_string_assignment(stmt)
if key:
definitions.append(
CodePlaceholderDefinition(
key=key,
source=source,
category=category,
display_name=key,
description=description,
)
)
return definitions
def _spec_metadata(spec_path: Path) -> tuple[str, str, str]:
app_name = spec_path.parts[-3] if len(spec_path.parts) >= 3 else "unknown"
if app_name == "litigation_ai":
return "诉讼文书", "litigation", "诉讼文书生成上下文占位符"
return f"{app_name} 占位符", app_name, "从 placeholders/spec.py 自动发现的占位符键"
def _extract_string_assignment(stmt: ast.stmt) -> str | None:
if not isinstance(stmt, ast.Assign) or not stmt.targets:
return None
if isinstance(stmt.value, ast.Constant) and isinstance(stmt.value.value, str) and stmt.value.value:
return stmt.value.value
return None