该提交实现了支持企业微信、微信公众号、个人微信桥接三种模式的完整微信渠道适配器,包含以下核心模块: 1. 基础认证与配置相关:auth_adapter、config_reload、setup_contract等 2. 消息处理与格式转换:format、attachment_adapter、outbound_adapter等 3. 多模式客户端支持:wecom/mp子模块,包含加解密、消息收发能力 4. 辅助能力:限速器、防抖、会话绑定、事件映射、模板渲染等 5. 扩展能力:二维码登录、消息读取、特权用户、心跳监控等 实现了完整的微信生态对接能力,支持消息收发、事件处理、API调用限流、配置热重载等功能。
163 lines
6.1 KiB
Python
163 lines
6.1 KiB
Python
from __future__ import annotations
|
||
|
||
from enum import Enum
|
||
from typing import Any
|
||
|
||
from yuxi.utils.logging_config import logger
|
||
|
||
|
||
class WizardStep(Enum):
|
||
MODE_SELECTION = "mode_selection"
|
||
CREDENTIALS = "credentials"
|
||
VALIDATION = "validation"
|
||
WEBHOOK = "webhook"
|
||
CONFIRMATION = "confirmation"
|
||
|
||
|
||
class WeChatSetupWizard:
|
||
def __init__(self):
|
||
self._current_step = WizardStep.MODE_SELECTION
|
||
self._selected_mode: str = ""
|
||
self._config_snapshot: dict[str, Any] = {}
|
||
|
||
def get_available_modes(self) -> list[dict[str, Any]]:
|
||
return [
|
||
{
|
||
"id": "wecom",
|
||
"label": "企业微信 (WeCom)",
|
||
"description": "使用企业微信应用消息 API,需要 corp_id、corp_secret、agent_id",
|
||
"required_fields": ["corp_id", "corp_secret", "agent_id"],
|
||
},
|
||
{
|
||
"id": "mp",
|
||
"label": "公众号 (MP)",
|
||
"description": "使用微信公众号客服消息 API,需要 app_id、app_secret",
|
||
"required_fields": ["app_id", "app_secret"],
|
||
},
|
||
{
|
||
"id": "personal",
|
||
"label": "个人微信 (Bridge)",
|
||
"description": "通过桥接服务连接个人微信,需要 bridge_url",
|
||
"required_fields": ["bridge_url"],
|
||
},
|
||
]
|
||
|
||
async def select_mode(self, mode: str) -> dict[str, Any]:
|
||
if mode not in ("wecom", "mp", "personal"):
|
||
return {
|
||
"success": False,
|
||
"error": f"Invalid mode: {mode}",
|
||
"available": [m["id"] for m in self.get_available_modes()],
|
||
}
|
||
self._selected_mode = mode
|
||
self._current_step = WizardStep.CREDENTIALS
|
||
return {"success": True, "mode": mode, "next_step": "credentials"}
|
||
|
||
async def set_credentials(self, credentials: dict[str, Any]) -> dict[str, Any]:
|
||
if not self._selected_mode:
|
||
return {"success": False, "error": "Please select a mode first"}
|
||
|
||
required = {
|
||
"wecom": ["corp_id", "corp_secret", "agent_id"],
|
||
"mp": ["app_id", "app_secret"],
|
||
"personal": ["bridge_url"],
|
||
}.get(self._selected_mode, [])
|
||
|
||
missing = [k for k in required if not credentials.get(k)]
|
||
if missing:
|
||
return {"success": False, "error": f"Missing required fields: {missing}"}
|
||
|
||
self._config_snapshot = {}
|
||
for k in required:
|
||
self._config_snapshot[k] = credentials[k]
|
||
self._current_step = WizardStep.VALIDATION
|
||
return {"success": True, "config": self._config_snapshot, "next_step": "validation"}
|
||
|
||
async def validate_connection(self, http_client_factory=None) -> dict[str, Any]:
|
||
if not self._config_snapshot:
|
||
return {"success": False, "error": "No credentials configured"}
|
||
|
||
if self._selected_mode == "wecom":
|
||
valid = all(self._config_snapshot.get(k) for k in ("corp_id", "corp_secret"))
|
||
elif self._selected_mode == "mp":
|
||
valid = all(self._config_snapshot.get(k) for k in ("app_id", "app_secret"))
|
||
elif self._selected_mode == "personal":
|
||
valid = bool(self._config_snapshot.get("bridge_url"))
|
||
else:
|
||
valid = False
|
||
|
||
self._current_step = WizardStep.WEBHOOK if valid else WizardStep.CREDENTIALS
|
||
return {
|
||
"success": valid,
|
||
"mode": self._selected_mode,
|
||
"message": "Credentials validated" if valid else "Invalid credentials",
|
||
"next_step": "webhook" if valid else "credentials",
|
||
}
|
||
|
||
async def configure_webhook(self, webhook_url: str) -> dict[str, Any]:
|
||
if not self._config_snapshot:
|
||
return {"success": False, "error": "No credentials configured"}
|
||
|
||
self._config_snapshot["webhook_url"] = webhook_url
|
||
self._config_snapshot["token"] = self._config_snapshot.get("token", "")
|
||
self._current_step = WizardStep.CONFIRMATION
|
||
return {
|
||
"success": True,
|
||
"webhook_url": webhook_url,
|
||
"next_step": "confirmation",
|
||
"note": "请在企业微信后台/公众号配置服务器地址为以上 webhook_url,并配置相同的 Token / EncodingAESKey"
|
||
if self._selected_mode == "wecom"
|
||
else "",
|
||
}
|
||
|
||
async def confirm_and_apply(self) -> dict[str, Any]:
|
||
if self._current_step != WizardStep.CONFIRMATION:
|
||
return {"success": False, "error": "Please complete all steps before confirming"}
|
||
|
||
config = {
|
||
"enabled": True,
|
||
"mode": self._selected_mode,
|
||
**self._config_snapshot,
|
||
}
|
||
|
||
logger.info(f"[WeChat/SetupWizard] Configuration confirmed for mode={self._selected_mode}")
|
||
return {
|
||
"success": True,
|
||
"mode": self._selected_mode,
|
||
"config": config,
|
||
"message": f"微信 {self._selected_mode} 模式配置完成,请启动适配器以验证连接",
|
||
}
|
||
|
||
def get_current_step_info(self) -> dict[str, Any]:
|
||
return {
|
||
"step": self._current_step.value,
|
||
"mode": self._selected_mode,
|
||
"config_snapshot": dict(self._config_snapshot),
|
||
}
|
||
|
||
def reset(self) -> None:
|
||
self._current_step = WizardStep.MODE_SELECTION
|
||
self._selected_mode = ""
|
||
self._config_snapshot = {}
|
||
|
||
|
||
class WeChatSetupAdapter:
|
||
ROBUST_READ_COMMANDS = [
|
||
"wechat config validate",
|
||
"wechat config show",
|
||
"wechat status",
|
||
]
|
||
|
||
@staticmethod
|
||
def get_wizard_summary(config: dict[str, Any]) -> dict[str, Any]:
|
||
has_corp = all(config.get(k) for k in ("corp_id", "corp_secret", "agent_id"))
|
||
has_mp = all(config.get(k) for k in ("app_id", "app_secret"))
|
||
has_bridge = bool(config.get("bridge_url"))
|
||
mode = "wecom" if has_corp else "mp" if has_mp else "personal" if has_bridge else "unconfigured"
|
||
return {
|
||
"mode": mode,
|
||
"configured": mode != "unconfigured",
|
||
"webhook_configured": bool(config.get("webhook_url")),
|
||
"enabled": config.get("enabled", False),
|
||
}
|