ForcePilot/backend/package/yuxi/channel/extensions/telegram/config.py
Kris 8a3601250e feat(channel): 添加 Telegram 渠道扩展
新增 Telegram 渠道扩展,支持在 Yuxi 平台中集成 Telegram 即时通讯渠道。

包含以下功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- polling: 长轮询模式
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- actions: 动作处理
- inline_keyboard: 内联键盘
- native_commands: 原生指令
- chat: 聊天管理
- delivery: 消息送达确认
- media: 媒体资源处理
- profile: 用户资料
- reactions: 表情反应
- sticker: 贴纸处理
- types: 类型定义
2026-05-21 11:48:09 +08:00

198 lines
7.4 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
import hashlib
import logging
import os
logger = logging.getLogger(__name__)
class TelegramConfigAdapter:
def __init__(self):
self._config: dict = {}
@property
def _tg_cfg(self) -> dict:
return self._config.get("channels", {}).get("telegram", {})
def list_account_ids(self, config: dict) -> list[str]:
self._config = config
accounts = self._tg_cfg.get("accounts", {})
if accounts:
return list(accounts.keys())
if self._env_token_exists():
return ["default"]
return []
async def resolve_account(self, account_id: str) -> dict:
return self._build_account(account_id)
def is_configured(self, account: dict) -> bool:
return bool(account.get("token"))
def is_enabled(self, account: dict) -> bool:
return account.get("enabled", True)
def describe_account(self, account: dict) -> dict:
return {
"account_id": account.get("account_id", ""),
"name": account.get("name", ""),
"token_source": account.get("token_source", ""),
"dm_policy": account.get("dm_policy", "pairing"),
"configured": self.is_configured(account),
}
def default_account_id(self) -> str:
return self._tg_cfg.get("defaultAccount", "default")
def config_schema(self) -> dict:
return {
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Telegram 渠道配置",
"properties": {
"botToken": {
"type": "string",
"title": "Bot Token",
"description": "Telegram Bot Token从 @BotFather 获取,也可通过环境变量 TELEGRAM_BOT_TOKEN 注入)",
"x-ui-password": True,
},
"tokenFile": {
"type": "string",
"title": "Token 文件路径",
"description": "存放 Bot Token 的文件路径(与 botToken 互斥,优先级低于 botToken",
},
"dm_policy": {
"type": "string",
"title": "DM 策略",
"enum": ["pairing", "allowlist", "open", "disabled"],
"default": "pairing",
},
"group_policy": {
"type": "string",
"title": "群组策略",
"enum": ["open", "disabled", "allowlist"],
"default": "open",
},
"text_chunk_limit": {
"type": "integer",
"title": "文本分块上限",
"default": 4096,
"minimum": 100,
"maximum": 4096,
},
"webhook_url": {
"type": "string",
"title": "Webhook URL",
"description": "Telegram Webhook 回调地址(公网可访问),留空则使用 Long Polling",
},
"webhook_secret": {
"type": "string",
"title": "Webhook Secret",
"description": "X-Telegram-Bot-Api-Secret-Token 验证令牌",
"x-ui-password": True,
},
"reaction_level": {
"type": "string",
"title": "Reaction 级别",
"enum": ["off", "ack", "minimal"],
"default": "minimal",
},
"streaming_mode": {
"type": "string",
"title": "流式模式",
"enum": ["off", "partial", "block", "progress"],
"default": "partial",
},
},
}
@staticmethod
def token_fingerprint(token: str) -> str:
return hashlib.sha256(token.encode()).hexdigest()[:8]
@staticmethod
def _env_bot_token(account_id: str) -> str:
if account_id != "default":
key = f"TELEGRAM_BOT_TOKEN_{account_id.upper()}"
val = os.environ.get(key, "")
if val:
return val
return os.environ.get("TELEGRAM_BOT_TOKEN", "")
@staticmethod
def _env_token_exists() -> bool:
return bool(os.environ.get("TELEGRAM_BOT_TOKEN", ""))
@staticmethod
def _read_token_file(file_path: str) -> str:
try:
return open(file_path).read().strip()
except OSError:
logger.warning("Telegram tokenFile read failed: %s", file_path)
return ""
def _build_account(self, account_id: str) -> dict:
tg_cfg = self._tg_cfg
accounts = tg_cfg.get("accounts", {})
account_raw = accounts.get(account_id, {}) if account_id != "default" else tg_cfg
token = ""
token_source = "none"
env_token = self._env_bot_token(account_id)
if env_token:
token = env_token
token_source = "env"
elif account_raw.get("botToken"):
token = account_raw["botToken"]
token_source = "config"
elif account_raw.get("tokenFile") or tg_cfg.get("tokenFile"):
file_path = account_raw.get("tokenFile", tg_cfg.get("tokenFile", ""))
token = self._read_token_file(file_path)
if token:
token_source = "tokenFile"
def _get(key: str, default=None):
return account_raw.get(key, tg_cfg.get(key, default))
def _get_nested(*keys, default=None):
acct_val = account_raw
top_val = tg_cfg
for key in keys[:-1]:
acct_val = acct_val.get(key, {}) if isinstance(acct_val, dict) else {}
top_val = top_val.get(key, {}) if isinstance(top_val, dict) else {}
last = keys[-1]
return (
acct_val.get(last, top_val.get(last, default))
if isinstance(acct_val, dict)
else top_val.get(last, default)
)
return {
"account_id": account_id,
"token": token,
"token_source": token_source,
"name": account_raw.get("name", account_id),
"enabled": account_raw.get("enabled", True),
"dm_policy": _get("dmPolicy", "pairing"),
"group_policy": _get("groupPolicy", "open"),
"allow_from": _get("allowFrom", []),
"group_allow_from": _get("groupAllowFrom", []),
"text_chunk_limit": _get("textChunkLimit", 4096),
"webhook_url": _get("webhookUrl", ""),
"webhook_secret": _get("webhookSecret", ""),
"reaction_level": _get("reactionLevel", "minimal"),
"reaction_notifications": _get("reactionNotifications", "own"),
"streaming_mode": _get_nested("streaming", "mode", default="partial"),
"streaming_min_chars": _get_nested("streaming", "minChars", default=18),
"streaming_idle_ms": _get_nested("streaming", "idleMs", default=500),
"groups": _get("groups", {}),
"dms": _get("dms", {}),
"custom_commands": _get("customCommands", []),
"actions_reactions": _get_nested("actions", "reactions", default=True),
"actions_send_message": _get_nested("actions", "sendMessage", default=True),
"actions_poll": _get_nested("actions", "poll", default=True),
}