新增小红书、XMPP、元宝、Zalo 四个渠道扩展。 小红书渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, media, status, window XMPP 渠道扩展主要模块:plugin, config, gateway, outbound, streaming, pairing, security, dedupe, accounts, commands, muc, rate_limiter, stanza_utils, status, monitor 元宝渠道扩展主要模块:plugin, client, config_schema, gateway, outbound(chunk/queue/transport), inbound(dispatcher), streaming, pairing, security, accounts, actions, commands, codec(biz/conn), session, shared, utils Zalo 渠道扩展主要模块:api, config, gateway, webhook, outbound, pairing, security, session, polling, monitor, status
170 lines
6.3 KiB
Python
170 lines
6.3 KiB
Python
import os
|
||
import logging
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class ZaloConfigAdapter:
|
||
def list_account_ids(self, config: dict) -> list[str]:
|
||
accounts = config.get("accounts", {})
|
||
if not accounts:
|
||
return ["default"] if self._env_token_exists("default") else []
|
||
return list(accounts.keys())
|
||
|
||
async def resolve_account(self, account_id: str) -> dict:
|
||
raw = self._load_raw_config(account_id)
|
||
return self.build_account(account_id, raw)
|
||
|
||
def is_configured(self, account: dict) -> bool:
|
||
return bool(account.get("bot_token"))
|
||
|
||
def is_enabled(self, account: dict) -> bool:
|
||
return account.get("enabled", True)
|
||
|
||
def describe_account(self, account: dict) -> dict:
|
||
token_source = account.get("token_source", "none")
|
||
return {
|
||
"account_id": account.get("account_id", ""),
|
||
"name": account.get("name", ""),
|
||
"token_source": token_source,
|
||
"dm_policy": account.get("dm_policy", "pairing"),
|
||
"configured": self.is_configured(account),
|
||
}
|
||
|
||
def default_account_id(self, config: dict) -> str:
|
||
return config.get("default_account", "default")
|
||
|
||
def config_schema(self) -> dict:
|
||
return {
|
||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||
"type": "object",
|
||
"title": "Zalo 渠道配置",
|
||
"properties": {
|
||
"bot_token": {
|
||
"type": "string",
|
||
"title": "Bot Token",
|
||
"description": "Zalo Bot API Token(也可通过环境变量 ZALO_BOT_TOKEN 注入)",
|
||
"x-ui-password": True,
|
||
},
|
||
"dm_policy": {
|
||
"type": "string",
|
||
"title": "DM 策略",
|
||
"enum": ["pairing", "allowlist", "open", "disabled"],
|
||
"default": "pairing",
|
||
},
|
||
"group_policy": {
|
||
"type": "string",
|
||
"title": "群组策略",
|
||
"enum": ["open", "allowlist", "disabled"],
|
||
"default": "allowlist",
|
||
},
|
||
"webhook_url": {
|
||
"type": "string",
|
||
"title": "Webhook URL",
|
||
"description": "HTTPS Webhook URL(生产推荐)",
|
||
},
|
||
"webhook_secret": {
|
||
"type": "string",
|
||
"title": "Webhook Secret",
|
||
"description": "Webhook 签名密钥(8-256 字符)",
|
||
"x-ui-password": True,
|
||
},
|
||
"media_max_mb": {
|
||
"type": "integer",
|
||
"title": "媒体大小上限 (MB)",
|
||
"default": 5,
|
||
"minimum": 1,
|
||
"maximum": 10,
|
||
},
|
||
"proxy": {
|
||
"type": "string",
|
||
"title": "HTTP 代理",
|
||
"description": "HTTP 代理 URL(如 http://proxy:8080)",
|
||
},
|
||
},
|
||
"required": ["bot_token"],
|
||
}
|
||
|
||
def _load_raw_config(self, account_id: str) -> dict:
|
||
return {}
|
||
|
||
def build_account(self, account_id: str, raw: dict) -> dict:
|
||
token, token_source = self._resolve_token(account_id, raw)
|
||
env_secret = os.environ.get("ZALO_WEBHOOK_SECRET", "")
|
||
|
||
allow_from = raw.get("allow_from", [])
|
||
group_allow_from = raw.get("group_allow_from", allow_from)
|
||
|
||
return {
|
||
"account_id": account_id,
|
||
"name": raw.get("name", account_id),
|
||
"bot_token": token,
|
||
"token_source": token_source,
|
||
"dm_policy": raw.get("dm_policy", "pairing"),
|
||
"group_policy": raw.get("group_policy", "allowlist"),
|
||
"allow_from": allow_from,
|
||
"group_allow_from": group_allow_from,
|
||
"webhook_url": raw.get("webhook_url", ""),
|
||
"webhook_secret": env_secret or raw.get("webhook_secret", ""),
|
||
"webhook_path": raw.get("webhook_path", ""),
|
||
"media_max_mb": raw.get("media_max_mb", 5),
|
||
"proxy": raw.get("proxy", ""),
|
||
"response_prefix": raw.get("response_prefix", ""),
|
||
"enabled": raw.get("enabled", True),
|
||
}
|
||
|
||
def _resolve_token(self, account_id: str, raw: dict) -> tuple[str, str]:
|
||
bot_token = raw.get("bot_token", "")
|
||
|
||
if isinstance(bot_token, dict):
|
||
env_key = bot_token.get("env", "")
|
||
if env_key and os.environ.get(env_key):
|
||
return os.environ[env_key], "env"
|
||
file_path = bot_token.get("file", "")
|
||
if file_path:
|
||
try:
|
||
resolved = os.path.realpath(file_path)
|
||
except (OSError, ValueError):
|
||
pass
|
||
else:
|
||
try:
|
||
with open(resolved, encoding="utf-8") as f:
|
||
return f.read().strip(), "configFile"
|
||
except OSError:
|
||
logger.warning("Zalo tokenFile not readable: %s", file_path)
|
||
exec_val = bot_token.get("exec", "")
|
||
if exec_val:
|
||
return exec_val, "config"
|
||
return str(bot_token.get("value", "")), "config" if bot_token.get("value") else "none"
|
||
|
||
if bot_token and not bot_token.startswith("${"):
|
||
return bot_token, "config"
|
||
|
||
env_token = self._env_bot_token(account_id)
|
||
if env_token:
|
||
return env_token, "env"
|
||
|
||
token_file = raw.get("tokenFile", "")
|
||
if token_file:
|
||
try:
|
||
resolved = os.path.realpath(token_file)
|
||
except (OSError, ValueError):
|
||
return "", "none"
|
||
try:
|
||
with open(resolved, encoding="utf-8") as f:
|
||
return f.read().strip(), "configFile"
|
||
except OSError:
|
||
logger.warning("Zalo tokenFile not readable: %s", token_file)
|
||
|
||
return "", "none"
|
||
|
||
@staticmethod
|
||
def _env_bot_token(account_id: str) -> str:
|
||
key = f"ZALO_BOT_TOKEN_{account_id.upper()}"
|
||
return os.environ.get(key, "") or os.environ.get("ZALO_BOT_TOKEN", "")
|
||
|
||
@staticmethod
|
||
def _env_token_exists(account_id: str) -> bool:
|
||
return bool(ZaloConfigAdapter._env_bot_token(account_id))
|