ForcePilot/backend/package/yuxi/channel/extensions/tencent_im/config.py
Kris 331daa1216 feat(channel): 添加腾讯 IM 渠道扩展
新增腾讯 IM(Tencent IM)渠道扩展,支持在 Yuxi 平台中集成腾讯即时通讯 IM 渠道。

包含以下功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- pairing: 用户配对与绑定
- security: 安全校验
- signature: 请求签名验证
- usersig: UserSig 生成
- dedupe: 消息去重
- status: 会话状态管理
- group: 群组管理
- types: 类型定义
2026-05-21 11:49:29 +08:00

207 lines
8.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.

import os
from yuxi.channel.extensions.tencent_im.types import TencentIMAccount
ENV_MAP = {
"sdk_appid": "TENCENT_IM_SDK_APPID",
"secret_key": "TENCENT_IM_SECRET_KEY",
"public_key": "TENCENT_IM_PUBLIC_KEY",
"admin_userid": "TENCENT_IM_ADMIN_USERID",
"region": "TENCENT_IM_REGION",
"dm_policy": "TENCENT_IM_DM_POLICY",
"usersig_expire_days": "TENCENT_IM_USERSIG_EXPIRE_DAYS",
"callback_verify_enabled": "TENCENT_IM_CALLBACK_VERIFY",
"callback_token": "TENCENT_IM_CALLBACK_TOKEN",
"callback_signature_version": "TENCENT_IM_CALLBACK_SIG_VERSION",
}
FIELD_MAP = {
"sdk_appid": "sdkAppid",
"secret_key": "secretKey",
"public_key": "publicKey",
"admin_userid": "adminUserid",
"region": "region",
"dm_policy": "dmPolicy",
"usersig_expire_days": "usersigExpireDays",
"callback_verify_enabled": "callbackVerifyEnabled",
"callback_token": "callbackToken",
"callback_signature_version": "callbackSignatureVersion",
}
class TencentIMConfig:
def list_account_ids(self, config: dict | None = None) -> list[str]:
if config and "accounts" in config:
return list(config["accounts"].keys())
if config and config.get("sdkAppid"):
return ["default"]
if self._env("sdk_appid"):
return ["default"]
return []
def resolve_account(self, account_id: str = "default") -> TencentIMAccount:
return TencentIMAccount(
account_id=account_id,
sdk_appid=int(self._config_value(account_id, "sdkAppid", "sdk_appid", "0")),
secret_key=self._config_value(account_id, "secretKey", "secret_key", ""),
public_key=self._config_value(account_id, "publicKey", "public_key", ""),
admin_userid=self._config_value(account_id, "adminUserid", "admin_userid", "admin"),
region=self._config_value(account_id, "region", "region", "ap-guangzhou"),
dm_policy=self._config_value(account_id, "dmPolicy", "dm_policy", "open"),
callback_verify_enabled=self._config_value_bool(
account_id, "callbackVerifyEnabled", "callback_verify_enabled", True
),
callback_token=self._config_value(account_id, "callbackToken", "callback_token", ""),
callback_signature_version=self._config_value(
account_id, "callbackSignatureVersion", "callback_signature_version", "v1"
),
usersig_expire_days=int(
self._config_value(account_id, "usersigExpireDays", "usersig_expire_days", "180")
),
streaming=True,
)
def is_configured(self, account: dict | None = None) -> bool:
if isinstance(account, dict):
sdk_appid = account.get("sdkAppid") or account.get("sdk_appid") or account.get("sdk_appid", 0)
secret_key = account.get("secretKey") or account.get("secret_key") or account.get("secret_key", "")
return bool(sdk_appid and secret_key)
return bool(self._env("sdk_appid") and self._env("secret_key"))
def is_enabled(self, account: dict, config: dict | None = None) -> bool:
if isinstance(account, dict):
return account.get("enabled", True) and self.is_configured(account)
return self.is_configured()
def disabled_reason(self, account: dict, config: dict | None = None) -> str:
if not self.is_configured(account):
return "SDKAppID 或 SecretKey 未配置"
if not account.get("enabled", True):
return "渠道已禁用"
return ""
def unconfigured_reason(self, account: dict, config: dict | None = None) -> str:
sdk_appid = account.get("sdkAppid", 0) if isinstance(account, dict) else 0
secret_key = account.get("secretKey", "") if isinstance(account, dict) else ""
if not sdk_appid:
return "缺少 SDKAppID"
if not secret_key:
return "缺少 SecretKey"
return ""
def describe_account(self, account: dict, config: dict | None = None) -> dict:
info = dict(account) if isinstance(account, dict) else {}
info.pop("secretKey", None)
info.pop("secret_key", None)
info["account_id"] = info.get("account_id", "default")
return info
def default_account_id(self, config: dict | None = None) -> str:
ids = self.list_account_ids(config)
return ids[0] if ids else "default"
def has_configured_state(self, config: dict | None = None) -> bool:
return self.list_account_ids(config) != []
def config_schema(self) -> dict:
return {
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"title": "腾讯云 IM 渠道配置",
"properties": {
"sdkAppid": {
"type": "integer",
"title": "SDKAppID",
"description": "腾讯云 IM 控制台中应用的 SDKAppID",
},
"secretKey": {
"type": "string",
"title": "Secret Key",
"x-ui-password": True,
"description": "IM 控制台开发辅助工具中的密钥(key),用于生成 UserSig",
},
"publicKey": {
"type": "string",
"title": "Public Key",
"x-ui-password": True,
"description": "IM 控制台回调配置中的 public_key用于回调签名校验",
},
"adminUserid": {
"type": "string",
"title": "管理员账号",
"default": "admin",
"description": "App 管理员账号标识,用于调用 REST API",
},
"region": {
"type": "string",
"title": "数据中心地域",
"enum": [
"ap-guangzhou",
"ap-beijing",
"ap-shanghai",
"ap-chengdu",
"ap-singapore",
"ap-seoul",
"eu-frankfurt",
"na-siliconvalley",
"ap-tokyo",
"ap-jakarta",
],
"default": "ap-guangzhou",
},
"dmPolicy": {
"type": "string",
"title": "DM 安全策略",
"enum": ["open", "pairing", "allowlist", "disabled"],
"default": "open",
},
"callbackVerifyEnabled": {
"type": "boolean",
"title": "启用回调签名校验",
"default": True,
},
"callbackToken": {
"type": "string",
"title": "回调 Token",
"x-ui-password": True,
"description": "腾讯云 IM 回调配置中的 Token用于新签名方案 (v2)",
},
"callbackSignatureVersion": {
"type": "string",
"title": "回调签名版本",
"enum": ["v1", "v2"],
"default": "v1",
"description": "v1 使用 public_key 校验v2 使用 Token + RequestTime 校验",
},
"usersigExpireDays": {
"type": "integer",
"title": "UserSig 有效期(天)",
"default": 180,
"minimum": 1,
"maximum": 180,
},
},
"required": ["sdkAppid", "secretKey", "adminUserid"],
}
def _config_value(self, account_id: str, db_key: str, env_key: str, default: str) -> str:
env_val = os.getenv(ENV_MAP.get(env_key, "")) if env_key else None
return env_val or default
def _config_value_bool(self, account_id: str, db_key: str, env_key: str, default: bool) -> bool:
env_key_name = ENV_MAP.get(env_key, "")
if env_key_name:
env_val = os.getenv(env_key_name)
if env_val is not None:
return env_val.lower() != "false"
return default
def _env(self, key: str) -> str | None:
env_key = ENV_MAP.get(key, "")
if env_key:
return os.getenv(env_key)
return None