ForcePilot/backend/package/yuxi/channels/plugins/wechat_ilink/entry.py
Kris 00092c818e chore: 批量代码优化与规范完善
本次提交包含多项代码优化与规范修正:
1. 文档与注释优化:修正注释术语、补充注解与FR编号
2. 代码格式调整:统一空格、换行与缩进规范
3. 类型与接口完善:补充__all__导出、修正返回类型注解
4. 错误处理增强:新增领域错误类与校验逻辑
5. 依赖与导入调整:修复路径引用、统一时区导入
6. 协议与契约更新:完善接口文档与一致性注解
2026-07-03 19:18:13 +08:00

344 lines
12 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. ``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
# handler 需 client 引用以注入连接池;先实例化 handlerclient=None
# 再实例化 client最后通过 attach_client 建立引用,解决循环依赖。
handler = WeChatILinkLifecycleHandler(config_port, logger_port, cache_port)
client = ILinkClient(config_port, cache_port, logger_port)
handler.attach_client(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.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,
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=False,
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