ForcePilot/backend/package/yuxi/channels/adapters/wechat/security.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

161 lines
5.6 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 typing import Any
def check_dm_policy(config: dict[str, Any], open_id: str) -> bool:
dm_policy = config.get("dm_policy", "open")
if dm_policy == "open":
return True
elif dm_policy == "disabled":
return False
elif dm_policy == "allowlist":
allow_from = config.get("allow_from", [])
return f"wx:{open_id}" in allow_from
return False
def check_group_policy(config: dict[str, Any], chat_id: str, open_id: str) -> bool:
group_policy = config.get("group_policy", "allowlist")
if group_policy == "open":
return True
elif group_policy == "disabled":
return False
elif group_policy == "allowlist":
global_allow = config.get("group_allow_from", [])
if f"wx:{open_id}" in global_allow:
return True
groups_config = config.get("groups", {})
chat_config = groups_config.get(chat_id, {})
per_group_allow = chat_config.get("allow_from", [])
return f"wx:{open_id}" in per_group_allow
return False
def is_bot_mentioned(config: dict[str, Any], payload: dict, mode: str) -> bool:
if mode == "wecom":
content = payload.get("Content", "")
agent_name = config.get("agent_name", "")
if agent_name and ("@" in content and agent_name in content):
return True
return "@" in content
if mode == "mp":
content = payload.get("Content", "")
return "@" in content
if mode == "personal":
at_list = payload.get("at_list", [])
return len(at_list) > 0
return False
def should_require_mention(config: dict[str, Any], chat_id: str) -> bool:
groups_config = config.get("groups", {})
chat_config = groups_config.get(chat_id, {})
require_mention = chat_config.get("require_mention", True)
return require_mention
WECHAT_SAFETY_STRATEGIES: dict[str, dict[str, Any]] = {
"rateLimit": {
"description": "API 调用频率控制",
"implementation": "TokenBucketRateLimiter(rate=20, per=60)",
},
"banned": {
"description": "封禁检测与处理",
"implementation": "检测微信错误码 48001API未授权/ 违规记录 → 自动禁用适配器",
},
"autoJoin": {
"description": "自动加入群组策略",
"implementation": "config.auto_join_groups 白名单 → connect() 时自动加入",
},
"autoDerive": {
"description": "自动派生会话策略",
"implementation": "群聊消息按 sender 自动派生子会话(可配置开关)",
},
}
class WeChatSecurityAdapter:
@staticmethod
def resolve_dm_policy(config: dict[str, Any], account_id: str = "default") -> str:
accounts = config.get("accounts", {})
if account_id in accounts:
return accounts[account_id].get("dm_policy", "pairing")
return config.get("dm_policy", "pairing")
@staticmethod
def resolve_dm_allow_from(config: dict[str, Any], account_id: str = "default") -> list[str]:
accounts = config.get("accounts", {})
if account_id in accounts:
return accounts[account_id].get("allow_from", [])
return config.get("allow_from", [])
@staticmethod
def resolve_group_policy(config: dict[str, Any], account_id: str = "default") -> str:
accounts = config.get("accounts", {})
if account_id in accounts:
return accounts[account_id].get("group_policy", "allowlist")
return config.get("group_policy", "allowlist")
@staticmethod
def resolve_group_allow_from(config: dict[str, Any], chat_id: str) -> list[str]:
global_allow = config.get("group_allow_from", [])
groups_config = config.get("groups", {})
chat_config = groups_config.get(chat_id, {})
per_group_allow = chat_config.get("allow_from", [])
result = list(global_allow)
for entry in per_group_allow:
if entry not in result:
result.append(entry)
return result
@staticmethod
def collect_warnings(config: dict[str, Any]) -> list[str]:
warnings: list[str] = []
dm_policy = config.get("dm_policy", "pairing")
if dm_policy == "open":
warnings.append("dm_policy='open' 存在高危安全风险,建议启用配对机制")
allow_from = config.get("allow_from", [])
group_allow = config.get("group_allow_from", [])
if dm_policy == "allowlist" and not allow_from:
warnings.append("allowlist 为空,无人可访问")
if not group_allow:
groups_config = config.get("groups", {})
has_per_group = any(chat_config.get("allow_from") for chat_config in groups_config.values())
if not has_per_group and config.get("group_policy") == "allowlist":
warnings.append("群组 allowlist 为空,所有群组将被拒绝访问")
has_pairing = any(acc.get("dm_policy") == "pairing" for acc in config.get("accounts", {}).values())
if dm_policy != "pairing" and not has_pairing:
warnings.append("未启用配对机制 (pairing),建议开启以增强安全性")
return warnings
@staticmethod
def collect_audit_findings(config: dict[str, Any]) -> list[dict[str, Any]]:
findings: list[dict[str, Any]] = []
allow_from = config.get("allow_from", [])
if len(allow_from) > 1000:
findings.append(
{
"severity": "warning",
"category": "allowlist_size",
"message": f"allowlist 规模异常({len(allow_from)} 条目 > 1000",
}
)
return findings