246 lines
14 KiB
Python
246 lines
14 KiB
Python
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", {}) |