- 新增六层UI自动化架构:从Backend到Capabilities的完整分层实现 - 添加WeChat 4.0分辨率适配Profile与图像模板资源 - 实现幂等缓存、熔断器、重试策略、链路追踪与监控指标 - 新增头像下载安全校验、发布朋友圈路径白名单防护 - 优化密钥缓存、DB校验逻辑与初始化流程 - 补充完整错误码体系与启动清场机制
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
"""L6 Capability API:对外能力接口(SendResult + WeChatCapabilities)。
|
||
|
||
P1 阶段:WeChatCapabilities.send_text 委托给旧 xdotool_driver.send_text,
|
||
把返回的 local_send_id 包成 SendResult(success=True, verified=True)。
|
||
P3 阶段:替换为 FlowOrchestrator.send_text,统一走状态机 + DB 校验 + 幂等。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional
|
||
|
||
logger = logging.getLogger("woc-bridge")
|
||
|
||
|
||
@dataclass
|
||
class SendResult:
|
||
"""发送文本结果。
|
||
|
||
所有字段带默认值,避免构造时必填字段顺序问题。
|
||
diagnostics 用 field(default=None) + Optional 避免可变默认值陷阱。
|
||
|
||
Attributes:
|
||
success: 是否成功(含校验通过)
|
||
local_id: 本地生成的消息 ID(如 local_<unix>_<rand>)
|
||
verified: 是否经 DB 校验确认(P1 阶段默认 True,P3 接入真实校验)
|
||
skipped: 是否因幂等命中而跳过实际发送
|
||
error: 错误描述(成功时为空)
|
||
error_code: 错误码(如 WINDOW_NOT_FOUND / SEND_TIMEOUT)
|
||
retryable: 是否可安全重试
|
||
diagnostics: 诊断信息字典(trace_id / image_confidence / duration_ms 等)
|
||
"""
|
||
|
||
success: bool = False
|
||
local_id: str = ""
|
||
verified: bool = False
|
||
skipped: bool = False
|
||
error: str = ""
|
||
error_code: str = ""
|
||
retryable: bool = False
|
||
diagnostics: Optional[dict] = field(default=None)
|
||
|
||
|
||
class WeChatCapabilities:
|
||
"""L6 对外能力 API:业务层(路由 / outbox)通过此类调用 UI 自动化。
|
||
|
||
P1 阶段:xdotool_driver 非空、orchestrator 为 None,send_text 委托给
|
||
旧 xdotool_driver.send_text,包成 SendResult。
|
||
P3 阶段:orchestrator 非空、xdotool_driver 为 None,send_text 委托给
|
||
FlowOrchestrator.send_text(TODO: P3 时统一走 orchestrator)。
|
||
"""
|
||
|
||
def __init__(self, xdotool_driver=None, orchestrator=None) -> None:
|
||
"""初始化能力 API。
|
||
|
||
Args:
|
||
xdotool_driver: 旧 XdotoolDriver 实例(P1 阶段非空)
|
||
orchestrator: P3 阶段的 FlowOrchestrator 实例(P1 阶段为 None)
|
||
"""
|
||
self.xdotool_driver = xdotool_driver
|
||
self.orchestrator = orchestrator
|
||
|
||
async def send_text(
|
||
self,
|
||
to_wxid: str,
|
||
content: str,
|
||
display_name: Optional[str] = None,
|
||
client_request_id: str = "",
|
||
) -> SendResult:
|
||
"""发送文本消息。
|
||
|
||
P1 阶段:若 orchestrator 非空则委托 orchestrator.send_text;
|
||
否则调 xdotool_driver.send_text,把返回的 local_send_id 包成
|
||
SendResult(success=True, local_id=..., verified=True)。
|
||
|
||
TODO: P3 时统一走 orchestrator,移除 xdotool_driver 分支。
|
||
|
||
Args:
|
||
to_wxid: 目标 wxid
|
||
content: 文本内容
|
||
display_name: 用于搜索定位会话的显示名(备注/昵称/微信号)
|
||
client_request_id: 幂等键维度(P1 阶段忽略,P3 接入 IdemCache)
|
||
|
||
Returns:
|
||
SendResult:发送结果
|
||
"""
|
||
# TODO: P3 时统一走 orchestrator
|
||
if self.orchestrator is not None:
|
||
return await self.orchestrator.send_text(
|
||
to_wxid=to_wxid,
|
||
content=content,
|
||
display_name=display_name,
|
||
client_request_id=client_request_id,
|
||
)
|
||
|
||
# P1 阶段:委托给旧 xdotool_driver
|
||
if self.xdotool_driver is None:
|
||
return SendResult(
|
||
success=False,
|
||
error="no backend available (xdotool_driver and orchestrator both None)",
|
||
error_code="BRIDGE_NOT_INITIALIZED",
|
||
retryable=False,
|
||
)
|
||
|
||
try:
|
||
local_send_id = await self.xdotool_driver.send_text(
|
||
to_wxid=to_wxid,
|
||
content=content,
|
||
display_name=display_name,
|
||
)
|
||
return SendResult(
|
||
success=True,
|
||
local_id=str(local_send_id),
|
||
verified=True,
|
||
)
|
||
except Exception as exc:
|
||
logger.error(
|
||
"[capability] send_text failed to=%s: %s", to_wxid, exc
|
||
)
|
||
return SendResult(
|
||
success=False,
|
||
error=str(exc),
|
||
error_code="SEND_FAILED",
|
||
retryable=True,
|
||
)
|