ForcePilot/backend/package/yuxi/channel/extensions/flock/config.py
Kris 0bad70ec19 feat(flock): 新增Flock团队协作IM渠道插件
实现了完整的Flock渠道接入能力,包含消息收发、Webhook事件监听、账号配置、安全校验、媒体文件处理等功能,支持私聊和群组聊天,适配ForcePilot插件规范。
2026-05-21 10:47:01 +08:00

208 lines
8.4 KiB
Python

from __future__ import annotations
import logging
import os
from .types import FlockAccount
logger = logging.getLogger(__name__)
ENV_MAP = {
"FLOCK_BOT_TOKEN": "bot_token",
"FLOCK_APP_TOKEN": "app_token",
"FLOCK_APP_SECRET": "app_secret",
"FLOCK_OUTGOING_WEBHOOK_TOKEN": "outgoing_webhook_token",
"FLOCK_EVENT_LISTENER_TOKEN": "event_listener_token",
"FLOCK_INCOMING_WEBHOOK_URL": "incoming_webhook_url",
"FLOCK_DM_POLICY": "dm_policy",
"FLOCK_GROUP_POLICY": "group_policy",
"FLOCK_TEXT_CHUNK_LIMIT": "text_chunk_limit",
}
def _apply_env_overrides(account: FlockAccount) -> FlockAccount:
for env_key, attr_name in ENV_MAP.items():
env_val = os.environ.get(env_key)
if env_val is not None:
setattr(account, attr_name, env_val)
return account
def _dict_to_account(data: dict) -> FlockAccount:
return FlockAccount(
account_id=data.get("account_id", "default"),
enabled=data.get("enabled", True),
bot_token=data.get("bot_token", ""),
app_token=data.get("app_token", ""),
app_secret=data.get("app_secret", ""),
outgoing_webhook_token=data.get("outgoing_webhook_token", ""),
event_listener_token=data.get("event_listener_token", ""),
incoming_webhook_url=data.get("incoming_webhook_url", ""),
dm_policy=data.get("dm_policy", "open"),
dm_allow_from=data.get("dm_allow_from", []),
group_policy=data.get("group_policy", "open"),
group_allow_from=data.get("group_allow_from", []),
require_mention=data.get("require_mention", False),
text_chunk_limit=data.get("text_chunk_limit", 7000),
media_max_mb=data.get("media_max_mb", 100),
thread_replies=data.get("thread_replies", "inbound"),
block_streaming=data.get("block_streaming", True),
)
def _resolve_account_data(raw_config: dict, account_id: str) -> FlockAccount:
accounts = raw_config.get("accounts", {})
data = accounts.get(account_id, {})
if "account_id" not in data:
data = {**data, "account_id": account_id}
account = _dict_to_account(data)
return _apply_env_overrides(account)
class FlockConfigAdapter:
def __init__(self, raw_config: dict | None = None):
self._raw_config: dict = raw_config or {}
def set_config(self, config: dict) -> None:
self._raw_config = config
def list_account_ids(self, config: dict | None = None) -> list[str]:
cfg = config if config is not None else self._raw_config
accounts = cfg.get("accounts", {})
if not accounts:
return ["default"]
return list(accounts.keys())
def resolve_account(self, account_id: str) -> dict:
accounts = self._raw_config.get("accounts", {})
data = accounts.get(account_id, {})
if not data:
return {"account_id": account_id}
if "account_id" not in data:
return {**data, "account_id": account_id}
return data
def resolve_account_object(self, account_id: str) -> FlockAccount:
return _resolve_account_data(self._raw_config, account_id)
def is_configured(self, account: dict) -> bool:
acct = _dict_to_account(account)
acct = _apply_env_overrides(acct)
return bool(acct.bot_token or acct.incoming_webhook_url)
def is_enabled(self, account: dict, config: dict | None = None) -> bool:
return account.get("enabled", True)
def describe_account(self, account: dict, config: dict | None = None) -> dict:
acct = _dict_to_account(account)
acct = _apply_env_overrides(acct)
return {
"account_id": acct.account_id,
"enabled": acct.enabled,
"configured": bool(acct.bot_token or acct.incoming_webhook_url),
"dm_policy": acct.dm_policy,
"group_policy": acct.group_policy,
}
def disabled_reason(self, account: dict, config: dict | None = None) -> str:
if account.get("enabled", True):
return ""
return "Account is disabled"
def unconfigured_reason(self, account: dict, config: dict | None = None) -> str:
acct = _dict_to_account(account)
acct = _apply_env_overrides(acct)
if not acct.bot_token and not acct.incoming_webhook_url:
return "bot_token and incoming_webhook_url not set"
return ""
def default_account_id(self, config: dict) -> str:
return "default"
def resolve_allow_from(self, config: dict, account_id: str | None = None) -> list[str] | None:
aid = account_id or "default"
accounts = config.get("accounts", {})
data = accounts.get(aid, {})
acct = _dict_to_account(data)
acct = _apply_env_overrides(acct)
return list(acct.dm_allow_from)
def format_allow_from(self, config: dict, account_id: str | None, allow_from: list[str]) -> list[str]:
return allow_from
def has_configured_state(self, config: dict) -> bool:
accounts = config.get("accounts", {})
for data in accounts.values():
if self.is_configured(data):
return True
return False
def has_persisted_auth_state(self, config: dict) -> bool:
return self.has_configured_state(config)
def config_schema(self) -> dict:
return {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"accounts": {
"type": "object",
"default": {"default": {}},
"additionalProperties": {
"type": "object",
"properties": {
"account_id": {"type": "string", "default": "default"},
"enabled": {"type": "boolean", "default": True},
"bot_token": {"type": "string", "description": "Flock Bot Token"},
"app_token": {"type": "string", "description": "Flock App Token (备选)"},
"app_secret": {
"type": "string",
"description": "Flock App Secret (Event Token HMAC 签名验证)",
},
"outgoing_webhook_token": {
"type": "string",
"description": "Outgoing Webhook 验证 Token",
},
"event_listener_token": {
"type": "string",
"description": "Event Listener 验证 Token",
},
"incoming_webhook_url": {
"type": "string",
"description": "Incoming Webhook URL (备选发送方式)",
},
"dm_policy": {
"type": "string",
"enum": ["open", "pairing", "allowlist", "disabled"],
"default": "open",
},
"dm_allow_from": {
"type": "array",
"items": {"type": "string"},
"default": [],
},
"group_policy": {
"type": "string",
"enum": ["open", "allowlist", "disabled"],
"default": "open",
},
"group_allow_from": {
"type": "array",
"items": {"type": "string"},
"default": [],
},
"require_mention": {"type": "boolean", "default": False},
"text_chunk_limit": {"type": "integer", "default": 7000},
"media_max_mb": {"type": "integer", "default": 100},
"thread_replies": {
"type": "string",
"enum": ["off", "inbound", "always"],
"default": "inbound",
},
"block_streaming": {"type": "boolean", "default": True},
},
},
}
},
}