ForcePilot/backend/package/yuxi/channels/plugins/wechat_ilink/entry.py

349 lines
13 KiB
Python
Raw Normal View History

"""微信 iLinkClawBot渠道插件入口。
实现 ``CHANNEL_ENTRY`` 契约注册 11 个适配器与生命周期钩子返回
``PluginManifest``由宿主在 loaded 阶段通过 ``importlib`` 导入并调用
调用顺序严格
1. ``getXxxPort`` 获取被驱动端口实例宿主已从 manifest.accessible_ports
加载权限无需运行时 declareXxx
2. 实例化 ``WeChatILinkLifecycleHandler`` ``ILinkClient``
3. 实例化并注册 11 个适配器puller / inbound / outbound / session /
status / login / lifecycle / probeable / doctor / wizard / whitelist
4. ``registerLifecycleHandler`` 注册生命周期钩子鸭子类型调用
5. 返回 ``PluginManifest``
适配器 DI 说明
- 7 个适配器puller / inbound / outbound / login / lifecycle / doctor /
wizard接收 ``(client, logger_port)``
- ``probeable`` 适配器接收 ``(client, persistence_port, logger_port)``
需动态发现 account_id 调用 getconfig 探活
- ``whitelist`` 适配器接收 ``(cache_port, logger_port)``白名单数据本地
存储于 CachePort不调用渠道侧 API无需 ILinkClient
- ``session`` / ``status`` 适配器无状态INV-5 ``__init__`` 参数
仅做协议转换所有数据从 raw_event.payload 提取
长轮询传输管理iLink 通过 ``PullerAdapter`` 长轮询 ``getupdates`` 拉取
消息``PullerWorker`` 由传输引擎通过 ``ChannelAccountOnline`` /
``ChannelAccountOffline`` 领域事件触发启停``LifecycleAdapter``
``LifecycleHandler`` 不自管理轮询任务
依赖方向 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.doctor_adapter import WeChatILinkDoctorAdapter
from .adapters.inbound_adapter import WeChatILinkInboundAdapter
from .adapters.lifecycle_adapter import WeChatILinkLifecycleAdapter
from .adapters.login_adapter import WeChatILinkLoginAdapter
from .adapters.outbound_adapter import WeChatILinkOutboundAdapter
from .adapters.probeable_adapter import WeChatILinkProbeableAdapter
from .adapters.puller_adapter import WeChatILinkPullerAdapter
from .adapters.session_adapter import WeChatILinkSessionAdapter
from .adapters.status_adapter import WeChatILinkStatusAdapter
from .adapters.whitelist_adapter import WeChatILinkWhitelistAdapter
from .adapters.wizard_adapter import WeChatILinkWizardAdapter
from .ilink_client import ILinkClient
from .lifecycle import WeChatILinkLifecycleHandler
# 可访问端口(与 manifest.json accessible_ports 一致)
_ACCESSIBLE_PORTS: tuple[str, ...] = (
"ConfigPort",
"LoggerPort",
"PersistencePort",
"CachePort",
"TracerPort",
"ConversationPort",
)
# 可注入管道(与 manifest.json injectable_pipelines 一致)
_INJECTABLE_PIPELINES: tuple[str, ...] = ("inbound", "outbound")
# 已注册适配器类型列表(与 registerAdapter 调用顺序一致)
_ADAPTER_TYPES: tuple[str, ...] = (
"puller",
"inbound",
"outbound",
"session",
"status",
"login",
"lifecycle",
"probeable",
"doctor",
"wizard",
"whitelist",
)
def channel_entry(host: PluginHost) -> PluginManifest:
"""微信 iLink 渠道插件入口。
由宿主在 loaded 阶段调用完成适配器注册生命周期钩子注册
返回 ``PluginManifest``
能力边界accessible_ports / injectable_pipelines / resource_quota
``ChannelManifest`` 静态字段声明宿主在加载期通过
``PluginCapabilityChecker.check`` 校验运行时通过
``PluginHostImpl._checkPortAccess`` manifest 读取并强制约束
"""
# 1. 获取端口实例(宿主已从 manifest.accessible_ports 加载权限)
# ILinkClient 仅依赖 ConfigPort/CachePort/LoggerPort凭证由
# CredentialService 通过 ConfigPort→CachePort 管理);
# PersistencePort 供 ProbeableAdapter 动态发现 account_id。
config_port = host.getConfigPort()
logger_port = host.getLoggerPort()
cache_port = host.getCachePort()
persistence_port = host.getPersistencePort()
# 2. 实例化 ILinkClient 与 LifecycleHandler
# 依赖关系为单向handler 在 onInit 调 client.attach_http_client 注入连接池,
# client 不引用 handler。先创建 client再创建 handler 并通过构造器注入。
client = ILinkClient(config_port, cache_port, logger_port)
handler = WeChatILinkLifecycleHandler(
config_port,
logger_port,
cache_port,
_build_config_schema(),
client,
)
# 3. 实例化并注册 11 个适配器
# 8 个适配器接收 (client, logger_port)probeable 接收
# (client, persistence_port, logger_port)whitelist 接收
# (cache_port, logger_port)session / status 无状态。
host.registerAdapter("puller", WeChatILinkPullerAdapter(client, logger_port))
host.registerAdapter("inbound", WeChatILinkInboundAdapter(client, logger_port))
host.registerAdapter("outbound", WeChatILinkOutboundAdapter(client, logger_port))
host.registerAdapter("session", WeChatILinkSessionAdapter())
host.registerAdapter("status", WeChatILinkStatusAdapter())
host.registerAdapter("login", WeChatILinkLoginAdapter(client, logger_port))
host.registerAdapter("lifecycle", WeChatILinkLifecycleAdapter(client, logger_port))
host.registerAdapter("probeable", WeChatILinkProbeableAdapter(client, persistence_port, logger_port))
host.registerAdapter("doctor", WeChatILinkDoctorAdapter(client, logger_port))
host.registerAdapter("wizard", WeChatILinkWizardAdapter(client, logger_port))
host.registerAdapter("whitelist", WeChatILinkWhitelistAdapter(cache_port, logger_port))
# 4. 注册生命周期钩子
# PluginHost Protocol 未声明 registerLifecycleHandler但 PluginHostImpl
# 已实现,运行时通过鸭子类型调用。
host.registerLifecycleHandler(handler)
# 5. 返回 PluginManifest
return PluginManifest(
manifest=ChannelManifest(
id="com.yuxi.channels.wechat_ilink",
name="微信iLink",
version="1.0.0",
channel_type=ChannelType("wechat_ilink"),
provides=("wechat_ilink",),
entry_module="yuxi.channels.plugins.wechat_ilink.entry",
capabilities=ChannelCapabilities(
rich_message=False,
streaming=False,
typing_indicator=False,
message_edit=False,
message_recall=False,
supports_reaction=False,
supports_pin=False,
supports_card_update=False,
supports_card_update_streaming=False,
supports_image_inbound=True,
supports_video_inbound=True,
supports_image_outbound=True,
supports_video_outbound=True,
mention=False,
command=False,
directory=False,
doctor=True,
whitelist=True,
wizard=True,
tools=False,
status=False,
probeable=True,
identity_resolver=False,
lifecycle=True,
supports_qr_login=True,
),
config_schema=_build_config_schema(),
lifecycle=("init", "start", "stop", "pause", "resume", "unload"),
compatibility={
"min_host_version": "1.0.0",
"ilink_api_version": "2.0.0",
},
failure_policy=FailurePolicy.DEGRADE,
resource_quota=ResourceQuota(
max_cpu="15.0",
max_memory="128MB",
max_connections=10,
max_calls_per_sec=20,
),
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=4000,
supports_credential_cloning=False,
),
adapters=_ADAPTER_TYPES,
)
def _build_config_schema() -> tuple[ConfigField, ...]:
"""构造 12 个 ConfigField与 manifest.json config_schema 一致)。"""
return (
ConfigField(
key="bot_token",
type="string",
required=True,
hot_reloadable=False,
constraints={"sensitive": True},
),
ConfigField(
key="ilink_bot_id",
type="string",
required=True,
hot_reloadable=False,
constraints={},
),
ConfigField(
key="ilink_user_id",
type="string",
required=True,
hot_reloadable=False,
constraints={},
),
ConfigField(
key="baseurl",
type="string",
required=False,
default="https://ilinkai.weixin.qq.com",
hot_reloadable=False,
constraints={"pattern": "^https?://"},
),
ConfigField(
key="channel_version",
type="string",
required=False,
default="2.0.0",
hot_reloadable=True,
constraints={},
),
ConfigField(
key="cdn_url",
type="string",
required=False,
default="https://novac2c.cdn.weixin.qq.com/c2c",
hot_reloadable=True,
constraints={"pattern": "^https?://"},
),
ConfigField(
key="long_poll_timeout_ms",
type="integer",
required=False,
default=35000,
hot_reloadable=True,
constraints={"min": 10000, "max": 60000},
),
ConfigField(
key="qr_poll_interval_ms",
type="integer",
required=False,
default=2000,
hot_reloadable=True,
constraints={"min": 1000, "max": 5000},
),
ConfigField(
key="qr_timeout_ms",
type="integer",
required=False,
default=120000,
hot_reloadable=True,
constraints={"min": 30000, "max": 300000},
),
ConfigField(
key="max_message_length",
type="integer",
required=False,
default=4000,
hot_reloadable=True,
constraints={"min": 500, "max": 10000},
),
ConfigField(
key="enable_media",
type="boolean",
required=False,
default=True,
hot_reloadable=True,
constraints={},
),
ConfigField(
key="enable_voice",
type="boolean",
required=False,
default=False,
hot_reloadable=True,
constraints={},
),
)
def _build_env_vars() -> tuple[EnvVar, ...]:
"""构造 6 个 EnvVar与 manifest.json env_vars 一致)。"""
return (
EnvVar(
name="ILINK_BASE_URL",
description="iLink API 基础地址",
required=False,
default="https://ilinkai.weixin.qq.com",
),
EnvVar(
name="ILINK_CDN_URL",
description="iLink CDN 地址",
required=False,
default="https://novac2c.cdn.weixin.qq.com/c2c",
),
EnvVar(
name="ILINK_CHANNEL_VERSION",
description="iLink 渠道协议版本",
required=False,
default="2.0.0",
),
EnvVar(
name="ILINK_QR_TIMEOUT_MS",
description="扫码登录超时时间(毫秒)",
required=False,
default="120000",
),
EnvVar(
name="ILINK_LONGPOLL_TIMEOUT_MS",
description="长轮询超时时间(毫秒)",
required=False,
default="35000",
),
EnvVar(
name="ILINK_QR_POLL_INTERVAL_MS",
description="扫码状态轮询间隔(毫秒)",
required=False,
default="2000",
),
)
CHANNEL_ENTRY = channel_entry