ForcePilot/backend/package/yuxi/channels/plugins/feishu/entry.py
Kris 8eead29de0 refactor: 批量清理冗余空行,优化部分枚举使用方式
1.  移除所有适配器文件中多余的空导入行
2.  调整ValidationError继承,移除不必要的ValueError继承
3.  修正多处ChannelType使用方式,从.value改为直接使用枚举实例
4.  优化飞书插件部分硬编码渠道类型为枚举实例
5.  更新wechat_ilink插件清单与适配器配置
6.  新增飞书目录适配器缓存清理支持判断与iLink生命周期适配器凭据轮换支持判断
7.  优化配置处理器历史查询逻辑,区分键不存在与无历史记录场景
2026-07-04 00:14:56 +08:00

455 lines
15 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.

"""飞书渠道插件入口。
实现 ``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_idCHANNEL_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