ForcePilot/backend/package/yuxi/channels/adapters/wechat/setup_wizard.py
Kris 05abecc02b feat(wechat): 新增完整微信渠道适配器实现
该提交实现了支持企业微信、微信公众号、个人微信桥接三种模式的完整微信渠道适配器,包含以下核心模块:
1. 基础认证与配置相关:auth_adapter、config_reload、setup_contract等
2. 消息处理与格式转换:format、attachment_adapter、outbound_adapter等
3. 多模式客户端支持:wecom/mp子模块,包含加解密、消息收发能力
4. 辅助能力:限速器、防抖、会话绑定、事件映射、模板渲染等
5. 扩展能力:二维码登录、消息读取、特权用户、心跳监控等

实现了完整的微信生态对接能力,支持消息收发、事件处理、API调用限流、配置热重载等功能。
2026-05-12 00:51:04 +08:00

163 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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),
}