"""飞书渠道插件入口。 实现 ``CHANNEL_ENTRY`` 契约,注册 18 个适配器与生命周期钩子,返回 ``PluginManifest``。由宿主在 loaded 阶段通过 ``importlib`` 导入并调用。 调用顺序(严格): 1. ``getXxxPort`` 获取被驱动端口实例(宿主已从 manifest.accessible_ports 加载权限,无需运行时 declareXxx)。 2. 实例化 ``FeishuLifecycleHandler`` 与 ``FeishuClient``。 3. 实例化并注册 18 个适配器(17 个列表追加 + 1 个 identity_resolver 单实例)。 4. ``registerLifecycleHandler`` 注册生命周期钩子(鸭子类型调用)。 5. 返回 ``PluginManifest``。 per-account 适配器处理:``DirectoryAdapter`` / ``WhitelistAdapter`` / ``WizardAdapter`` 的 ``__init__`` 需 ``account_id``,但 ``CHANNEL_ENTRY`` 在插件加载时不知具体账户,传入空字符串占位(``_PLACEHOLDER_ACCOUNT_ID``)。 实际 account_id 由 ``LifecycleAdapter.afterAccountConfigWritten`` 在账户配置 写入后按账户重建对应适配器,或由调用方通过 ``ConfigPort`` 动态读取。 WebSocket 长连接管理:飞书 WS 连接由 ``StreamWorker`` 通过 ``FeishuStreamConnectorAdapter`` 统一管理(账号启用/禁用由 ``ChannelAccountOnline``/``ChannelAccountOffline`` 领域事件触发), ``LifecycleAdapter`` 与 ``LifecycleHandler`` 不再自管理 WS 连接。 依赖方向:仅 import ``yuxi.channels.contract.*`` + 标准库 + 同插件内部模块, 不污染框架层。 """ from __future__ import annotations from yuxi.channels.contract.dtos.capability import ChannelCapabilities from yuxi.channels.contract.dtos.channel import ChannelType from yuxi.channels.contract.dtos.config import ConfigField from yuxi.channels.contract.dtos.plugin import EnvVar from yuxi.channels.contract.plugin.entry import PluginHost from yuxi.channels.contract.plugin.manifest import ( ChannelManifest, FailurePolicy, PluginManifest, ResourceQuota, ) from .adapters.command_adapter import FeishuCommandAdapter from .adapters.directory_adapter import FeishuDirectoryAdapter from .adapters.doctor_adapter import FeishuDoctorAdapter from .adapters.identity_resolver_adapter import FeishuIdentityResolverAdapter from .adapters.inbound_adapter import FeishuInboundAdapter from .adapters.lifecycle_adapter import FeishuLifecycleAdapter from .adapters.login_adapter import FeishuLoginAdapter from .adapters.mention_adapter import FeishuMentionAdapter from .adapters.message_ops_adapter import FeishuMessageOpsAdapter from .adapters.outbound_adapter import FeishuOutboundAdapter from .adapters.probeable_adapter import FeishuProbeableAdapter from .adapters.rich_message_adapter import FeishuRichMessageAdapter from .adapters.session_adapter import FeishuSessionAdapter from .adapters.status_adapter import FeishuStatusAdapter from .adapters.stream_connector_adapter import FeishuStreamConnectorAdapter from .adapters.streaming_adapter import FeishuStreamingAdapter from .adapters.whitelist_adapter import FeishuWhitelistAdapter from .adapters.wizard_adapter import FeishuWizardAdapter from .feishu_client import FeishuClient from .lifecycle import FeishuLifecycleHandler # per-account 适配器占位 account_id:CHANNEL_ENTRY 在插件加载时不知具体账户, # DirectoryAdapter / WhitelistAdapter / WizardAdapter 传入空字符串占位。 # 实际 account_id 由 LifecycleAdapter 在账户配置写入后按账户重建适配器实例, # 或由适配器在方法调用时通过 ConfigPort 动态读取。 _PLACEHOLDER_ACCOUNT_ID = "" # 可访问端口(与 manifest.json accessible_ports 一致) _ACCESSIBLE_PORTS: tuple[str, ...] = ( "ConfigPort", "LoggerPort", "PersistencePort", "CachePort", "TracerPort", "ConversationPort", "IdentityResolverPort", ) # 可注入管道 _INJECTABLE_PIPELINES: tuple[str, ...] = ("inbound", "outbound") # 已注册适配器类型列表(与 registerAdapter 调用顺序一致) _ADAPTER_TYPES: tuple[str, ...] = ( "inbound", "outbound", "session", "status", "rich_message", "streaming", "mention", "message_ops", "directory", "command", "whitelist", "wizard", "doctor", "lifecycle", "probeable", "login", "identity_resolver", "stream_connector", ) def channel_entry(host: PluginHost) -> PluginManifest: """飞书渠道插件入口。 由宿主在 loaded 阶段调用,完成适配器注册、生命周期钩子注册, 返回 ``PluginManifest``。 能力边界(accessible_ports / injectable_pipelines / resource_quota) 由 ``ChannelManifest`` 静态字段声明,宿主在加载期通过 ``PluginCapabilityChecker.check`` 校验,运行时通过 ``PluginHostImpl._checkPortAccess`` 从 manifest 读取并强制约束。 """ # 1. 获取端口实例(宿主已从 manifest.accessible_ports 加载权限) config_port = host.getConfigPort() logger_port = host.getLoggerPort() cache_port = host.getCachePort() persistence_port = host.getPersistencePort() # 2. 构造配置 schema(供 lifecycle handler 派生不可热更新键 + 复用至 manifest) config_schema = _build_config_schema() # 3. 实例化生命周期处理器(管理插件级 httpx 连接池) handler = FeishuLifecycleHandler( config_port, logger_port, cache_port, config_schema, ) # 4. 实例化 FeishuClient client = FeishuClient(config_port, cache_port, logger_port) # 5. 实例化并注册 18 个适配器 # 17 个列表追加 + 1 个 identity_resolver 单实例赋值 host.registerAdapter( "inbound", FeishuInboundAdapter(client, config_port, logger_port), ) host.registerAdapter( "outbound", FeishuOutboundAdapter(client, logger_port, cache_port), ) host.registerAdapter("session", FeishuSessionAdapter()) host.registerAdapter("status", FeishuStatusAdapter(logger_port)) host.registerAdapter("rich_message", FeishuRichMessageAdapter(logger_port)) host.registerAdapter( "streaming", FeishuStreamingAdapter(client, logger_port, cache_port), ) host.registerAdapter("mention", FeishuMentionAdapter(config_port)) host.registerAdapter( "message_ops", FeishuMessageOpsAdapter(client, logger_port, cache_port), ) host.registerAdapter( "directory", FeishuDirectoryAdapter(client, cache_port, _PLACEHOLDER_ACCOUNT_ID), ) host.registerAdapter("command", FeishuCommandAdapter(config_port)) host.registerAdapter( "whitelist", FeishuWhitelistAdapter( persistence_port, logger_port, _PLACEHOLDER_ACCOUNT_ID, ), ) host.registerAdapter( "wizard", FeishuWizardAdapter(config_port, client, _PLACEHOLDER_ACCOUNT_ID), ) host.registerAdapter("doctor", FeishuDoctorAdapter(client, config_port)) host.registerAdapter( "lifecycle", FeishuLifecycleAdapter(client, config_port, logger_port), ) host.registerAdapter( "probeable", FeishuProbeableAdapter(client, config_port, persistence_port), ) host.registerAdapter( "login", FeishuLoginAdapter(client, config_port, logger_port), ) # identity_resolver 为单实例注册(非列表追加) host.registerAdapter( "identity_resolver", FeishuIdentityResolverAdapter(client, cache_port), ) # stream_connector:飞书 WS 长连接适配器,由 StreamWorker 管理连接生命周期。 # 需 ConfigPort 读取 ws_endpoint_path / api_base_url,支持按部署环境定制 # WS 端点 API 路径(私有化部署场景)。 host.registerAdapter( "stream_connector", FeishuStreamConnectorAdapter(persistence_port, logger_port, config_port), ) # 6. 注册生命周期钩子 # PluginHost Protocol 未声明 registerLifecycleHandler,但 PluginHostImpl # 已实现,运行时通过鸭子类型调用。 host.registerLifecycleHandler(handler) # 7. 返回 PluginManifest return PluginManifest( manifest=ChannelManifest( id="com.yuxi.channels.feishu", name="飞书", version="1.0.0", channel_type=ChannelType("feishu"), provides=("feishu",), entry_module="yuxi.channels.plugins.feishu.entry", capabilities=ChannelCapabilities( rich_message=True, streaming=True, typing_indicator=True, message_edit=True, message_recall=True, supports_reaction=True, supports_pin=True, supports_card_update=True, supports_card_update_streaming=True, supports_image_inbound=True, supports_video_inbound=False, supports_image_outbound=True, supports_video_outbound=True, mention=True, command=True, directory=True, doctor=True, whitelist=True, wizard=False, tools=False, status=False, probeable=False, identity_resolver=True, lifecycle=True, supports_qr_login=True, ), config_schema=config_schema, lifecycle=("init", "start", "stop", "unload"), compatibility={ "min_host_version": "1.0.0", "feishu_api_version": "v2", }, failure_policy=FailurePolicy.DEGRADE, resource_quota=ResourceQuota( max_cpu="20.0", max_memory="256MB", max_connections=50, max_calls_per_sec=50, ), accessible_ports=_ACCESSIBLE_PORTS, injectable_pipelines=_INJECTABLE_PIPELINES, env_vars=_build_env_vars(), critical=False, requires_dm_pairing=True, requires_outbound_delivery=True, max_message_length=30720, supports_credential_cloning=False, ), adapters=_ADAPTER_TYPES, ) def _build_config_schema() -> tuple[ConfigField, ...]: """构造 18 个 ConfigField(与 manifest.json config_schema 一致)。""" return ( ConfigField( key="app_id", type="string", required=True, hot_reloadable=False, constraints={"pattern": "^cli_[a-zA-Z0-9]+$"}, ), ConfigField( key="app_secret", type="string", required=True, hot_reloadable=False, constraints={"sensitive": True}, ), ConfigField( key="encrypt_key", type="string", required=False, hot_reloadable=True, constraints={"sensitive": True}, ), ConfigField( key="verification_token", type="string", required=False, hot_reloadable=True, constraints={"sensitive": True}, ), ConfigField( key="event_mode", type="enum", required=True, default="websocket", hot_reloadable=False, constraints={"values": ["websocket", "webhook"]}, ), ConfigField( key="webhook_url", type="string", required=False, hot_reloadable=False, constraints={"pattern": "^https://"}, ), ConfigField( key="bot_name", type="string", required=True, hot_reloadable=True, ), ConfigField( key="bot_open_id", type="string", required=True, hot_reloadable=True, constraints={"pattern": "^ou_[a-zA-Z0-9]+$"}, ), ConfigField( key="enable_qr_login", type="boolean", required=False, default=False, hot_reloadable=True, ), ConfigField( key="qr_redirect_uri", type="string", required=False, hot_reloadable=True, constraints={"pattern": "^https://"}, ), ConfigField( key="qr_timeout_ms", type="integer", required=False, default=120000, hot_reloadable=True, constraints={"min": 30000, "max": 300000}, ), ConfigField( key="api_base_url", type="string", required=False, default="https://open.feishu.cn", hot_reloadable=True, ), ConfigField( key="ws_endpoint", type="string", required=False, default="wss://open.feishu.cn", hot_reloadable=True, ), ConfigField( key="request_timeout_ms", type="integer", required=False, default=30000, hot_reloadable=True, constraints={"min": 5000, "max": 60000}, ), ConfigField( key="max_retries", type="integer", required=False, default=3, hot_reloadable=True, constraints={"min": 0, "max": 5}, ), ConfigField( key="rate_limit_per_sec", type="integer", required=False, default=50, hot_reloadable=True, constraints={"min": 1, "max": 100}, ), ConfigField( key="user_cache_ttl_sec", type="integer", required=False, default=1800, hot_reloadable=True, constraints={"min": 60, "max": 7200}, ), ConfigField( key="identity_cache_ttl_sec", type="integer", required=False, default=60, hot_reloadable=True, constraints={"min": 30, "max": 600}, ), ) def _build_env_vars() -> tuple[EnvVar, ...]: """构造 7 个 EnvVar(与 manifest.json env_vars 一致)。""" return ( EnvVar( name="FEISHU_APP_ID", description="飞书应用 App ID", required=True, ), EnvVar( name="FEISHU_APP_SECRET", description="飞书应用 App Secret", required=True, sensitive=True, ), EnvVar( name="FEISHU_ENCRYPT_KEY", description="事件加密密钥", required=False, sensitive=True, ), EnvVar( name="FEISHU_VERIFICATION_TOKEN", description="Webhook 验证 Token", required=False, sensitive=True, ), EnvVar( name="FEISHU_BOT_NAME", description="飞书机器人名称", required=True, ), EnvVar( name="FEISHU_QR_REDIRECT_URI", description="扫码登录回调 URI", required=False, ), EnvVar( name="FEISHU_QR_TIMEOUT_MS", description="扫码登录超时时间(毫秒)", required=False, default="120000", ), ) CHANNEL_ENTRY = channel_entry