ForcePilot/backend/package/yuxi/channel/approval/templates.py
Kris 79ee930957 feat(approval): 新增审批流程核心模块
实现了完整的审批功能链路,包含审批请求投递、模板渲染、回复解析、引擎管理以及原生交互式卡片支持
2026-05-21 10:22:45 +08:00

165 lines
5.6 KiB
Python

"""审批展示模板 — 可自定义的审批信息展示格式"""
from __future__ import annotations
import html as _html
import logging
import re
from dataclasses import dataclass
from enum import StrEnum
from yuxi.channel.protocols import ApprovalAction, ApprovalDecision, ApprovalRequest, ApprovalResult
logger = logging.getLogger(__name__)
_MD_ESCAPE_RE = re.compile(r"([\\`*_{}\[\]()#+\-.!])")
class TemplateStyle(StrEnum):
PLAIN = "plain"
MARKDOWN = "markdown"
HTML = "html"
@dataclass
class ApprovalTemplateConfig:
title: str = "审批请求"
show_request_id: bool = True
show_action: bool = True
show_description: bool = True
show_initiator: bool = True
show_expiry: bool = True
style: TemplateStyle = TemplateStyle.PLAIN
approve_label: str = "批准"
deny_label: str = "拒绝"
timeout_minutes: int = 5
_DEFAULT_CONFIG = ApprovalTemplateConfig()
class ApprovalTemplateRenderer:
def __init__(self, config: ApprovalTemplateConfig | None = None):
self._config = config or _DEFAULT_CONFIG
@property
def config(self) -> ApprovalTemplateConfig:
return self._config
def render_request(self, request: ApprovalRequest) -> str:
cfg = self._config
lines: list[str] = [f"🔐 {self._bold(cfg.title)}"]
if cfg.show_request_id:
lines.append(f"请求 ID: {self._code(request.id)}")
if cfg.show_action:
lines.append(f"操作类型: {_action_label(request.action)}")
if cfg.show_description:
lines.append(f"描述: {self._escape(request.description)}")
if cfg.show_initiator:
lines.append(f"发起人: {self._escape(request.initiator_peer_id)}")
if cfg.show_expiry and cfg.timeout_minutes > 0:
lines.append(f"请在 {cfg.timeout_minutes} 分钟内回复")
lines.append("")
lines.append(f"回复「{self._bold(cfg.approve_label)}」或「{self._bold(cfg.deny_label)}」做出决定")
return "\n".join(lines)
def render_result(self, result: ApprovalResult, request: ApprovalRequest | None = None) -> str:
if result.decision == ApprovalDecision.APPROVED:
icon = ""
text = "审批已通过"
elif result.decision == ApprovalDecision.DENIED:
icon = ""
text = "审批已拒绝"
elif result.decision == ApprovalDecision.CANCELLED:
icon = "🚫"
text = "审批已取消"
elif result.decision == ApprovalDecision.EXPIRED:
icon = ""
text = "审批已过期"
else:
icon = ""
text = "审批状态未知"
lines = [f"{icon} {self._bold(text)}"]
lines.append(f"请求 ID: {self._code(result.request_id)}")
if result.reason:
lines.append(f"原因: {self._escape(result.reason)}")
if result.decided_by:
lines.append(f"决定人: {self._escape(result.decided_by)}")
return "\n".join(lines)
def render_expiry_warning(self, request: ApprovalRequest, remaining_seconds: int) -> str:
request_id = self._code(request.id)
if remaining_seconds <= 0:
return f"⏳ 审批请求 {request_id} 已过期。"
if remaining_seconds < 60:
return f"⏳ 审批请求 {request_id} 将在 {remaining_seconds} 秒后过期,请尽快处理。"
minutes = remaining_seconds // 60
return f"⏳ 审批请求 {request_id} 将在 {minutes} 分钟后过期,请尽快处理。"
def _bold(self, text: str) -> str:
style = self._config.style
if style == TemplateStyle.MARKDOWN:
return f"**{text}**"
elif style == TemplateStyle.HTML:
return f"<b>{_html.escape(text)}</b>"
return text
def _code(self, text: str) -> str:
style = self._config.style
if style == TemplateStyle.MARKDOWN:
return f"`{text}`"
elif style == TemplateStyle.HTML:
return f"<code>{_html.escape(text)}</code>"
return f"'{text}'"
def _escape(self, text: str) -> str:
style = self._config.style
if style == TemplateStyle.HTML:
return _html.escape(text)
elif style == TemplateStyle.MARKDOWN:
return _MD_ESCAPE_RE.sub(r"\\\1", text)
return text
def _action_label(action: ApprovalAction) -> str:
labels: dict[ApprovalAction, str] = {
ApprovalAction.APPROVE_EXEC: "执行操作",
ApprovalAction.APPROVE_READ: "读取操作",
}
return labels.get(action, action.value)
_renderer_cache: dict[str, ApprovalTemplateRenderer] = {}
def get_template_renderer(style: str = "default") -> ApprovalTemplateRenderer:
if style not in _renderer_cache:
if style == "default":
_renderer_cache[style] = ApprovalTemplateRenderer()
elif style == "compact":
_renderer_cache[style] = ApprovalTemplateRenderer(
ApprovalTemplateConfig(
title="审批",
show_request_id=False,
show_action=False,
show_initiator=False,
show_expiry=False,
)
)
elif style == "detailed":
_renderer_cache[style] = ApprovalTemplateRenderer(
ApprovalTemplateConfig(
title="操作审批请求",
style=TemplateStyle.MARKDOWN,
timeout_minutes=10,
)
)
else:
logger.warning("未知模板风格 '%s',使用默认配置", style)
_renderer_cache[style] = ApprovalTemplateRenderer()
return _renderer_cache[style]