from __future__ import annotations import logging import os from pathlib import Path from yuxi.channel.extensions.feishu.types import FeishuAccount logger = logging.getLogger(__name__) class FeishuConfigAdapter: def list_account_ids(self, config: dict) -> list[str]: feishu_cfg = _get_feishu_config(config) accounts = feishu_cfg.get("accounts", {}) if accounts: return list(accounts.keys()) if feishu_cfg.get("appId") or feishu_cfg.get("app_id"): return ["default"] if os.environ.get("FEISHU_APP_ID"): return ["default"] return [] def default_account_id(self, config: dict) -> str: feishu_cfg = _get_feishu_config(config) return feishu_cfg.get("defaultAccount") or feishu_cfg.get("default_account", "default") async def resolve_account(self, account_id: str, config: dict | None = None) -> FeishuAccount: feishu_cfg = _get_feishu_config(config) if config else {} accounts = feishu_cfg.get("accounts", {}) account = accounts.get(account_id, {}) if accounts else {} app_id = account.get("appId") or account.get("app_id") or feishu_cfg.get("appId") or feishu_cfg.get("app_id") or os.environ.get("FEISHU_APP_ID", "") app_secret = account.get("appSecret") or account.get("app_secret") if not app_secret: secret_file = account.get("appSecretFile") or account.get("app_secret_file") or feishu_cfg.get("appSecretFile") if secret_file: try: app_secret = Path(secret_file).read_text(encoding="utf-8").strip() except OSError: logger.warning("Failed to read app secret file: %s", secret_file) if not app_secret: app_secret = feishu_cfg.get("appSecret") or feishu_cfg.get("app_secret", "") if not app_secret: app_secret = os.environ.get("FEISHU_APP_SECRET", "") encrypt_key = account.get("encryptKey") or account.get("encrypt_key") or feishu_cfg.get("encryptKey") or feishu_cfg.get("encrypt_key") or os.environ.get("FEISHU_ENCRYPT_KEY") verification_token = account.get("verificationToken") or account.get("verification_token") or feishu_cfg.get("verificationToken") or feishu_cfg.get("verification_token") or os.environ.get("FEISHU_VERIFICATION_TOKEN") domain = account.get("domain") or feishu_cfg.get("domain", "feishu") connection_mode = account.get("connectionMode") or account.get("connection_mode") or feishu_cfg.get("connectionMode") or feishu_cfg.get("connection_mode", "websocket") dm_policy = account.get("dmPolicy") or account.get("dm_policy") or feishu_cfg.get("dmPolicy") or feishu_cfg.get("dm_policy", "pairing") group_policy = account.get("groupPolicy") or account.get("group_policy") or feishu_cfg.get("groupPolicy") or feishu_cfg.get("group_policy", "allowlist") if group_policy == "allowall": group_policy = "open" allow_from = account.get("allowFrom") or account.get("allow_from") or feishu_cfg.get("allowFrom") or feishu_cfg.get("allow_from", []) group_allow_from = account.get("groupAllowFrom") or account.get("group_allow_from") or feishu_cfg.get("groupAllowFrom") or feishu_cfg.get("group_allow_from", []) return FeishuAccount( account_id=account_id, app_id=app_id, app_secret=app_secret, name=account.get("name", account_id), domain=domain, connection_mode=connection_mode, encrypt_key=encrypt_key, verification_token=verification_token, enabled=account.get("enabled", True), dm_policy=dm_policy, group_policy=group_policy, allow_from=list(allow_from) if allow_from else [], group_allow_from=list(group_allow_from) if group_allow_from else [], require_mention=account.get("requireMention") if "requireMention" in account else feishu_cfg.get("requireMention", True), group_session_scope=account.get("groupSessionScope") or feishu_cfg.get("groupSessionScope") or feishu_cfg.get("group_session_scope", "group"), reply_in_thread=account.get("replyInThread") or feishu_cfg.get("replyInThread") or feishu_cfg.get("reply_in_thread", "disabled"), render_mode=account.get("renderMode") or feishu_cfg.get("renderMode") or feishu_cfg.get("render_mode", "auto"), streaming=account.get("streaming") if "streaming" in account else feishu_cfg.get("streaming", True), typing_indicator=account.get("typingIndicator") if "typingIndicator" in account else feishu_cfg.get("typingIndicator", True), reaction_notifications=account.get("reactionNotifications") or feishu_cfg.get("reactionNotifications") or feishu_cfg.get("reaction_notifications", "own"), resolve_sender_names=account.get("resolveSenderNames") if "resolveSenderNames" in account else feishu_cfg.get("resolveSenderNames", True), text_chunk_limit=account.get("textChunkLimit") or feishu_cfg.get("textChunkLimit") or feishu_cfg.get("text_chunk_limit", 4000), media_max_mb=account.get("mediaMaxMb") or feishu_cfg.get("mediaMaxMb") or feishu_cfg.get("media_max_mb", 30), http_timeout_ms=account.get("httpTimeoutMs") or feishu_cfg.get("httpTimeoutMs") or feishu_cfg.get("http_timeout_ms", 30000), webhook_path=account.get("webhookPath") or feishu_cfg.get("webhookPath") or feishu_cfg.get("webhook_path", "/feishu/events"), default_account=feishu_cfg.get("defaultAccount") or feishu_cfg.get("default_account", ""), tools=account.get("tools") or feishu_cfg.get("tools", {}), groups=account.get("groups") or feishu_cfg.get("groups", {}), dms=account.get("dms") or feishu_cfg.get("dms", {}), ) def is_configured(self, account: FeishuAccount | dict) -> bool: if isinstance(account, FeishuAccount): return account.is_configured() return bool(account.get("app_id") or account.get("appId")) and bool(account.get("app_secret") or account.get("appSecret")) def is_enabled(self, account: FeishuAccount | dict, config: dict | None = None) -> bool: if isinstance(account, FeishuAccount): return account.enabled return account.get("enabled", True) def disabled_reason(self, account: FeishuAccount | dict, config: dict | None = None) -> str: if not self.is_configured(account): return "App ID or App Secret not configured" return "" def describe_account(self, account: FeishuAccount | dict, config: dict | None = None) -> dict: if isinstance(account, FeishuAccount): return { "account_id": account.account_id, "name": account.name, "configured": account.is_configured(), "domain": account.domain, "connection_mode": account.connection_mode, } return { "account_id": account.get("account_id", ""), "name": account.get("name", ""), "configured": self.is_configured(account), "domain": account.get("domain", "feishu"), "connection_mode": account.get("connection_mode", "websocket"), } def resolve_allow_from(self, config: dict, account_id: str | None = None) -> list[str] | None: import asyncio async def _resolve(): account = await self.resolve_account(account_id or "default", config) return account.allow_from try: loop = asyncio.get_running_loop() except RuntimeError: loop = asyncio.new_event_loop() result = loop.run_until_complete(_resolve()) loop.close() return result return None def inspect_account(self, config: dict, account_id: str | None = None) -> dict: feishu_cfg = _get_feishu_config(config) accounts = feishu_cfg.get("accounts", {}) account = accounts.get(account_id or "default", {}) if accounts else {} return { "account_id": account_id or "default", "configured": bool(account.get("appId") or account.get("app_id") or feishu_cfg.get("appId")), "app_id": account.get("appId") or account.get("app_id") or feishu_cfg.get("appId", ""), "dm_policy": account.get("dmPolicy") or account.get("dm_policy", "pairing"), } def set_account_enabled(self, config: dict, account_id: str, enabled: bool) -> dict: config.setdefault("channels", {}).setdefault("feishu", {}).setdefault("accounts", {}).setdefault(account_id, {}) config["channels"]["feishu"]["accounts"][account_id]["enabled"] = enabled return config def delete_account(self, config: dict, account_id: str) -> dict: accounts = config.get("channels", {}).get("feishu", {}).get("accounts", {}) if account_id in accounts: del accounts[account_id] return config def has_configured_state(self, config: dict) -> bool: feishu_cfg = _get_feishu_config(config) accounts = feishu_cfg.get("accounts", {}) if accounts: return any(bool(a.get("appId") or a.get("app_id")) for a in accounts.values()) return bool(feishu_cfg.get("appId") or feishu_cfg.get("app_id") or os.environ.get("FEISHU_APP_ID")) def has_persisted_auth_state(self, config: dict) -> bool: return self.has_configured_state(config) def config_schema(self) -> dict: return { "type": "object", "title": "飞书渠道配置", "properties": { "enabled": {"type": "boolean", "default": True}, "domain": {"type": "string", "enum": ["feishu", "lark"], "default": "feishu"}, "connectionMode": {"type": "string", "enum": ["websocket", "webhook"], "default": "websocket"}, "dmPolicy": {"type": "string", "enum": ["pairing", "allowlist", "open", "disabled"], "default": "pairing"}, "groupPolicy": {"type": "string", "enum": ["open", "allowlist", "disabled"], "default": "allowlist"}, "allowFrom": {"type": "array", "items": {"type": "string"}}, "groupAllowFrom": {"type": "array", "items": {"type": "string"}}, "requireMention": {"type": "boolean", "default": True}, "groupSessionScope": {"type": "string", "enum": ["group", "group_sender", "group_topic", "group_topic_sender"], "default": "group"}, "replyInThread": {"type": "string", "enum": ["disabled", "enabled"], "default": "disabled"}, "renderMode": {"type": "string", "enum": ["auto", "raw", "card"], "default": "auto"}, "streaming": {"type": "boolean", "default": True}, "typingIndicator": {"type": "boolean", "default": True}, "reactionNotifications": {"type": "string", "enum": ["off", "own", "all"], "default": "own"}, "resolveSenderNames": {"type": "boolean", "default": True}, "textChunkLimit": {"type": "integer", "default": 4000}, "mediaMaxMb": {"type": "integer", "default": 30}, "httpTimeoutMs": {"type": "integer", "default": 30000}, "streamingThrottleMs": {"type": "integer", "default": 160, "description": "流式卡片更新节流间隔(毫秒)"}, "streamingSignificantDeltaChars": {"type": "integer", "default": 18, "description": "触发更新的最小新增字符数"}, "streamingPrintFrequencyMs": {"type": "integer", "default": 50, "description": "打字机打印频率(毫秒)"}, "accounts": { "type": "object", "additionalProperties": { "type": "object", "properties": { "appId": {"type": "string"}, "appSecret": {"type": "string"}, "name": {"type": "string"}, "domain": {"type": "string", "enum": ["feishu", "lark"]}, "enabled": {"type": "boolean"}, }, }, }, "tools": { "type": "object", "properties": { "doc": {"type": "boolean", "default": True}, "chat": {"type": "boolean", "default": True}, "wiki": {"type": "boolean", "default": True}, "drive": {"type": "boolean", "default": True}, "perm": {"type": "boolean", "default": False}, "scopes": {"type": "boolean", "default": True}, "bitable": {"type": "boolean", "default": True}, }, }, }, } def get_channel_config(self, config: dict) -> dict: return _get_feishu_config(config) def get_group_config(self, config: dict, chat_id: str) -> dict | None: groups = _get_feishu_config(config).get("groups", {}) return groups.get(chat_id) def collect_warnings(self, config: dict, account_id: str | None = None, account: FeishuAccount | None = None) -> list[str]: warnings = [] group_policy = account.group_policy if account else _get_feishu_config(config).get("groupPolicy", "allowlist") if group_policy == "open" and not _get_feishu_config(config).get("groups"): warnings.append("groupPolicy is 'open' without groups allowlist") return warnings def _get_feishu_config(config: dict | None) -> dict: if not config: return {} return config.get("channels", {}).get("feishu", {})