ForcePilot/backend/package/yuxi/channels/plugins/wechat_ilink/entry.py
Kris b88c0ae29e feat(channels): 批量新增多渠道网关限界上下文基础代码与契约
新增完整的 channels 限界上下文模块,包含契约层、领域核心层、应用服务、管道编排、插件体系、基础设施组合根等全层级代码,新增飞书与微信 iLink 渠道插件基础结构,补充各类 DTO、端口协议与领域服务实现。
2026-07-02 03:22:12 +08:00

282 lines
11 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.

"""微信 iLinkClawBot渠道插件入口。
实现 ``CHANNEL_ENTRY`` 契约,注册 11 个适配器与生命周期钩子,返回
``PluginManifest``。由宿主在 loaded 阶段通过 ``importlib`` 导入并调用。
调用顺序(严格):
1. ``declareAccessiblePorts`` / ``declareInjectablePipelines`` /
``declareResourceQuota`` 声明能力边界(必须在 ``getXxxPort`` 之前)。
2. ``getXxxPort`` 获取被驱动端口实例。
3. 实例化 ``WeChatILinkLifecycleHandler`` 与 ``ILinkClient``。
4. 实例化并注册 11 个适配器puller / inbound / outbound / session /
status / login / lifecycle / probeable / doctor / wizard / whitelist
5. ``registerLifecycleHandler`` 注册生命周期钩子(鸭子类型调用)。
6. 返回 ``PluginManifest``。
适配器 DI 说明:
- 8 个适配器puller / inbound / outbound / login / lifecycle / probeable /
doctor / wizard接收 ``(client, logger_port)``。
- ``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 CHANNEL_ENTRY, 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``。
"""
# 1. 声明能力边界(必须在 getXxxPort 之前,宿主 _checkPortAccess 校验)
host.declareAccessiblePorts(_ACCESSIBLE_PORTS)
host.declareInjectablePipelines(_INJECTABLE_PIPELINES)
host.declareResourceQuota(ResourceQuota(
max_cpu="15.0",
max_memory="128MB",
max_connections=10,
max_calls_per_sec=20,
))
# 2. 获取端口实例(声明之后)
config_port = host.getConfigPort()
logger_port = host.getLoggerPort()
cache_port = host.getCachePort()
persistence_port = host.getPersistencePort()
# 3. 实例化生命周期处理器与 ILinkClient
# handler 需 client 引用以注入连接池;先实例化 handlerclient=None
# 再实例化 client最后通过 attach_client 建立引用,解决循环依赖。
handler = WeChatILinkLifecycleHandler(config_port, logger_port, cache_port)
client = ILinkClient(config_port, cache_port, persistence_port, logger_port)
handler.attach_client(client)
# 4. 实例化并注册 11 个适配器
# 9 个适配器接收 (client, 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, logger_port))
host.registerAdapter("doctor", WeChatILinkDoctorAdapter(client, logger_port))
host.registerAdapter("wizard", WeChatILinkWizardAdapter(client, logger_port))
host.registerAdapter("whitelist", WeChatILinkWhitelistAdapter(cache_port, logger_port))
# 5. 注册生命周期钩子
# PluginHost Protocol 未声明 registerLifecycleHandler但 PluginHostImpl
# 已实现,运行时通过鸭子类型调用。
host.registerLifecycleHandler(handler)
# 6. 返回 PluginManifest
return PluginManifest(
manifest=ChannelManifest(
id="com.yuxi.channels.wechat_ilink",
name="微信iLink",
version="1.0.0",
channel_type=ChannelType.CUSTOM,
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,
mention=False,
command=False,
directory=False,
doctor=True,
whitelist=True,
supports_reaction=False,
supports_pin=False,
supports_card_update=False,
supports_image_inbound=True,
supports_video_inbound=True,
supports_image_outbound=True,
supports_video_outbound=True,
max_message_length=4000,
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,
),
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