ForcePilot/backend/package/yuxi/channels/adapters/wechat/setup_wizard.py
Kris a1d9ba9683 refactor(wechat): 整理代码风格与导入顺序,新增多项微信适配功能
本次提交包含多项优化与新增功能:
1. 清理多个文件中多余的空行与导入顺序
2. 修复voice.py中的多行字符串格式化问题
3. 新增微信公众号被动回复构建函数与配置项
4. 新增企业微信markdown消息发送支持
5. 新增消息去重TTL与最大条目配置
6. 新增markdown文本截断工具函数
7. 新增微信授权与OAuth相关工具方法
8. 重构消息去重逻辑,使用DedupPolicy替代本地字典实现
9. 新增子账号多租户支持功能
10. 新增消息动作处理适配器,支持send/reply等操作
11. 修复token持久化逻辑,新增状态存储支持
2026-05-13 16:16:52 +08:00

222 lines
8.5 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
import httpx
from yuxi.channels.adapters.wechat.probe import probe_bridge, probe_mp, probe_wecom
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
if not valid:
self._current_step = WizardStep.CREDENTIALS
return {
"success": False,
"mode": self._selected_mode,
"error": "Invalid credentials",
"next_step": "credentials",
}
if http_client_factory is None:
proxy = self._config_snapshot.get("proxy")
timeout_s = self._config_snapshot.get("timeout_seconds", 15.0)
async with httpx.AsyncClient(proxy=proxy, timeout=httpx.Timeout(timeout_s)) as http_client:
return await self._probe_connection(http_client)
else:
http_client = http_client_factory()
return await self._probe_connection(http_client)
async def _probe_connection(self, http_client: httpx.AsyncClient) -> dict[str, Any]:
try:
status = None
if self._selected_mode == "wecom":
status = await probe_wecom(http_client, self._config_snapshot)
elif self._selected_mode == "mp":
status = await probe_mp(http_client, self._config_snapshot)
elif self._selected_mode == "personal":
bridge_url = self._config_snapshot.get("bridge_url", "")
status = await probe_bridge(http_client, bridge_url)
if status is None:
self._current_step = WizardStep.CREDENTIALS
return {
"success": False,
"error": "Unknown probe mode",
"next_step": "credentials",
}
if status.status == "healthy":
self._current_step = WizardStep.WEBHOOK
return {
"success": True,
"mode": self._selected_mode,
"message": f"{self._selected_mode} API 连接验证成功",
"metadata": status.metadata,
"next_step": "webhook",
}
self._current_step = WizardStep.CREDENTIALS
return {
"success": False,
"mode": self._selected_mode,
"error": status.last_error or f"{self._selected_mode} API 连接失败",
"next_step": "credentials",
}
except Exception as e:
logger.error(f"[WeChat/SetupWizard] Probe failed for mode={self._selected_mode}: {e}")
self._current_step = WizardStep.CREDENTIALS
return {
"success": False,
"mode": self._selected_mode,
"error": str(e),
"next_step": "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),
}