docs(channel-plugin-docs): 更新渠道插件开发文档

1. 重构端口权限校验逻辑,改为manifest静态声明而非运行时声明
2. 新增wizard/tools/status/probeable等4个能力项与对应适配器
3. 补充标准插件目录结构与更多示例代码细节
4. 更新manifest.json字段说明与capabilities解析范围
5. 修正CHANNEL_ENTRY契约示例,移除运行时端口声明逻辑
6. 更新契约层文件与能力字段数量说明
This commit is contained in:
Kris 2026-07-08 23:00:28 +08:00
parent 532e7c59ec
commit fd885e5323

View File

@ -68,7 +68,7 @@
| 仓储子端口8 个) | `host.getChannelAccountRepositoryPort()` / `getChannelSessionRepositoryPort()` / `getPairingRepositoryPort()` / `getAuditLogRepositoryPort()` / `getOutboxRepositoryPort()` / `getUserIdentityRepositoryPort()` / `getIdempotencyRepositoryPort()` / `getPersistenceHealthPort()`(运行时均返回 `DrivenAdapters.persistence` 聚合别名) |
| 宿主中间件链 / 路由表 / 事件订阅器 | `host.registerStageSlot()` / `host.registerEventSubscription()` / `host.registerConfigSource()` / `host.registerMatchTier()` / `host.registerMatcher()` |
**端口访问前置声明(关键)**:插件在 `CHANNEL_ENTRY` 中调用任何 `getXxxPort()` **之前****必须** 先调用 `host.declareAccessiblePorts(...)` 声明将访问的端口名称元组。宿主在 `PluginHostImpl._checkPortAccess` 中校验每次端口获取是否在已声明集合内,未声明时抛 `PermissionDeniedError`
**端口访问权限manifest 驱动,关键)**:插件可访问的端口由 `manifest.json``accessible_ports` 字段静态声明,宿主在 discover 阶段解析为 `ChannelManifest.accessible_ports`,运行时由 [PluginHostImpl.\_checkPortAccess](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_host_impl.py) 在每次 `getXxxPort()` 调用中校验:请求的端口名 **必须**`manifest.accessible_ports` 集合内,否则抛 `PermissionDeniedError`。插件 **无需**(也无法)在 `CHANNEL_ENTRY` 内运行时声明端口权限——`PluginHost` Protocol 不提供 `declareAccessiblePorts` / `declareInjectablePipelines` / `declareResourceQuota` 方法,能力边界全部以 `manifest.json` 为唯一真相源F-03。`accessible_ports` 同时由 [PluginCapabilityChecker.check](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_capability_checker.py) 在 resolved 阶段校验是否为下表 17 个端口名称的子集
可声明的端口名称17 个,为 [PluginCapabilityChecker.ALLOWED_PORTS](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_capability_checker.py) 的子集):
@ -132,14 +132,19 @@
| `directory` | `directory_adapters` |
| `doctor` | `doctor_adapters` |
| `whitelist` | `whitelist_adapters` |
| `wizard` | `wizard_adapters` |
| `tools` | `tools_adapters` |
| `status` | `status_adapters` |
| `probeable` | `probeable_adapters` |
| `lifecycle` | `lifecycle_adapters` |
| `supports_qr_login` | `login_adapters` |
| `agent_collaboration` | `mention_adapters`(声明时 `MentionAdapter.classifyAgentMention` 必须可触发) |
| `supports_card_update_streaming` | `streaming_adapters`(复用 StreamingAdapterFR-12 流式卡片更新) |
| `message_edit` / `message_recall` / `supports_reaction` / `supports_pin` / `supports_card_update`(任一为 True | `message_ops_adapters` |
| `supports_image_inbound` / `supports_video_inbound`(任一为 TrueFR-43 | `inbound_adapters` |
| `supports_image_outbound` / `supports_video_outbound`(任一为 TrueFR-43 | `outbound_adapters` |
`typing_indicator` 无专用适配器(由出站适配器承载),不在校验范围。`supports_credential_cloning` 无专用适配器,不在校验范围。`lifecycle`AL-01`supports_qr_login`QR-02由 [manifest\_loader.\_parse\_capabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/manifest_loader.py) 从 `manifest.json` 解析,声明为 `True` 时触发对应适配器列表非空校验。
`typing_indicator`(由出站适配器承载)/ `identity_resolver`(由 `IdentityResolverPort` 承载)/ `supports_credential_cloning`(顶层安全策略字段,非能力声明)无专用适配器,不在校验范围。`lifecycle`AL-01`supports_qr_login`QR-02由 [manifest\_loader.\_parse\_capabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/manifest_loader.py) 从 `manifest.json` 解析,声明为 `True` 时触发对应适配器列表非空校验。
**运行时能力证明**(由插件实现 `CapabilityProver`,出站中间件调用):
@ -190,21 +195,36 @@
## 3. 标准布局
每个渠道插件遵循以下目录结构:
每个渠道插件遵循以下目录结构(以 wechat_woc 为参照,含客户端渠道的典型文件)
```
plugins/
└── <channel_name>/ # 如 feishu/、dingtalk/、wecom/
└── <channel_name>/ # 如 wechat_woc/、feishu/、wechat_ilink/
├── __init__.py
├── manifest.json # 插件清单(元数据、能力声明、配置 schema
├── entry.py # CHANNEL_ENTRY 入口函数
├── lifecycle.py # LifecycleHookHandler 实现(如需响应生命周期)
├── adapters/ # 适配器实现
├── manifest.json # 插件清单(元数据、能力声明、配置 schemaF-03 单真相源)
├── entry.py # CHANNEL_ENTRY 入口函数host, manifest-> PluginManifest
├── lifecycle.py # LifecycleHookHandler 实现onInit/onStart/.../onFail管理 httpx 连接池等插件级资源)
├── _constants.py # 渠道共享常量(配置默认值/依赖标识,与 manifest.json default 对齐)
├── <channel>_client.py # 渠道 HTTP/WS 客户端(如 woc_bridge_client.py / feishu_client.py封装 API 调用与错误翻译)
├── error_translator.py # 渠道原生错误码 → 契约层 Error 子类 / TransportError 翻译INV-7
├── adapters/ # 适配器实现(无 mutable 跨请求状态INV-5 / INV-8
│ ├── __init__.py
│ ├── inbound_adapter.py
│ ├── outbound_adapter.py
│ ├── streaming_adapter.py
│ └── ...
└── dtos/ # 渠道特有 DTO可选仅插件内部使用
│ ├── puller_adapter.py # 仅 pull/stream/both 传输模式需要
│ ├── stream_connector_adapter.py # 仅 stream/both 传输模式需要
│ ├── session_adapter.py # 通常无状态
│ ├── status_adapter.py # 通常无状态
│ ├── lifecycle_adapter.py # 账户生命周期account_id 派生、配置规范化、缓存清理)
│ ├── login_adapter.py # 扫码登录supports_qr_login=true 时必选)
│ ├── probeable_adapter.py # 连接探活probeable=true 时必选)
│ ├── doctor_adapter.py # 配置诊断
│ ├── wizard_adapter.py # 配置向导
│ ├── whitelist_adapter.py # 白名单管理
│ ├── directory_adapter.py # 通讯录查询
│ ├── capability_prover.py # CapabilityProver 实现(如需运行时能力证明)
│ └── ... # 按需rich_message / streaming / mention / command / message_ops 等
└── dtos/ # 渠道特有 DTO可选仅插件内部使用不泄漏到契约层
└── __init__.py
```
@ -213,6 +233,7 @@ plugins/
- 插件 **只能** 在自身 `plugins/<channel>/` 子目录内创建文件。
- 跨渠道共享的 DTO **必须** 通过契约层变更申请提升到 `yuxi.channels.contract.dtos.*`**不得** 在插件间互相 import。
- 渠道特有 DTO **不得** 泄漏到契约层,留在插件自身的 `dtos/` 目录内。
- `error_translator.py``_constants.py` 为可选但推荐集中管理渠道错误码翻译与跨模块共享常量避免重复定义wechat_woc / feishu / wechat_ilink 均采用此模式)。
### 3.1 manifest.json schema
@ -225,8 +246,8 @@ plugins/
| `version` | string | 是 | — | 语义化版本 |
| `channel_type` | string | 是 | — | 渠道类型枚举值(`feishu` / `dingtalk` / `wecom` / `webchat` / `telegram` / `discord` / `whatsapp` / `custom`,共 8 个;框架层禁止分支到具体枚举值,必须通过 `ChannelManifest` 声明字段驱动) |
| `entry_module` | string | 是 | — | 插件入口模块路径Python import 路径,如 `yuxi.channels.plugins.feishu.entry` |
| `capabilities` | object | 是 | — | 能力集合(见 [ChannelCapabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/dtos/capability.py) 字段,共 22 个字段,未声明默认 `false``max_message_length` 默认 `4096` |
| `config_schema` | array | 是 | — | 配置项 schema每项含 `key` / `type` / `required` / `default` / `hot_reloadable` / `constraints` |
| `capabilities` | object | 是 | — | 能力集合(见 [ChannelCapabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/dtos/capability.py) 字段,共 26 个 bool 字段,未声明默认 `false`,详见下方 capabilities 解析范围说明 |
| `config_schema` | array | 是 | — | 配置项 schema每项含 `key` / `type` / `required` / `default` / `hot_reloadable` / `sensitive` / `scope` / `constraints` |
| `provides` | array | 是 | — | 提供的能力列表 |
| `lifecycle` | array | 是 | — | 支持的生命周期钩子(如 `["init","start","stop","unload"]` |
| `compatibility` | object | 是 | — | 兼容性信息(自由结构 dict由插件自行约定语义框架仅透传不解释 |
@ -241,12 +262,19 @@ plugins/
| `requires_dm_pairing` | bool | 否 | `true` | 是否要求 DM 安全配对审批(声明 `false` 跳过配对审批FR-31插件未注册时框架回退为 `true`fail-closed |
| `requires_outbound_delivery` | bool | 否 | `true` | 是否要求出站投递(声明 `false` 无需实现 `OutboundAdapter`框架跳过出站投递阶段FR-31 |
| `credential_strategy` | object | 否 | `null` | 凭据策略(`CredentialStrategy`,含 `type``static`/`dynamic`/`hybrid`、`acquire_method` ∈ `manual`/`qr_login`/`oauth`/`webhook_verify`、`required_fields` / `optional_fields` / `supports_rotation` / `supports_revocation` |
| `max_message_length` | integer | 否 | `4096` | 单条消息最大文本长度FR19-P0-5 渠道级内容长度校验)。出站管道据此对超长内容做截断或分片决策。**顶层字段,非 `capabilities` 内字段**(如 wechat_woc 声明 `2000`、feishu 声明 `30720` |
| `supports_credential_cloning` | bool | 否 | `false` | 是否允许账户克隆时复制凭据字段ACC-CLONE。声明 `false``include_credentials=True` 的克隆请求被 handler 拒绝(`RuleViolationError`fail-closed 防止凭据泄露。**顶层安全策略字段,非能力声明,无专用适配器** |
| `icon` | string | 否 | `null` | 渠道展示图标名lucide 图标库 kebab-case 名称,如 `"smartphone"` / `"message-square"`)。供前端卡片渲染,缺失时前端回退默认图标。纯展示元数据,框架不做取值校验 |
| `transport_mode` | string | 否 | `both` | 传输模式声明Task 11.3`pull` 仅轮询 / `stream` 仅长连接 / `both` 两者皆可(优先 Stream。`TransportManager` 据此与适配器能力选择启动 Puller 或 Stream如 wechat_woc 声明 `both` |
| `capability_requirements` | array | 否 | `[]` | 能力运行时依赖声明,每项为 `[能力字段名, [所需配置字段名清单]]` 二元组(如 wechat_woc 声明 `["supports_image_inbound", ["bridge_url","bridge_token"]]`)。用于能力查询页判断"声明支持"与"实际可用"的差距 |
> **必填字段说明**`provides` / `lifecycle` / `compatibility` / `failure_policy``ChannelManifest` dataclass 中虽有默认值,但 [manifest_loader.load_manifest_from_dir](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/manifest_loader.py) 的 `required_fields`(共 11 个:`id` / `name` / `version` / `channel_type` / `entry_module` / `capabilities` / `config_schema` / `provides` / `lifecycle` / `compatibility` / `failure_policy`)将它们标记为 JSON 必填,缺失时抛 `ValidationError`
> **`entry_module` 说明**:宿主通过 `importlib.import_module(manifest.entry_module)` 动态导入,必须为可 import 的完整模块路径。返回的 `PluginManifest.manifest.id` **必须**`manifest.json``id` 一致,否则抛 `ValidationError`。模块缺失抛 `DependencyError``CHANNEL_ENTRY` 不可调用抛 `ValidationError`,调用异常抛 `InternalError`
> **`capabilities` 解析范围**[manifest\_loader.\_parse\_capabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/manifest_loader.py) 解析 [ChannelCapabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/dtos/capability.py) 的 **22 个字段**——13 个基础能力(`rich_message` / `streaming` / `typing_indicator` / `message_edit` / `message_recall` / `mention` / `command` / `directory` / `doctor` / `whitelist` / `supports_reaction` / `supports_pin` / `supports_card_update`、4 个媒体能力(`supports_image_inbound` / `supports_video_inbound` / `supports_image_outbound` / `supports_video_outbound`FR-43、1 个消息长度上限(`max_message_length`FR19-P0-5、4 个能力字段(`lifecycle` AL-01 / `supports_qr_login` QR-02 / `agent_collaboration` / `supports_credential_cloning`)。未声明的字段默认 `False``4096`。能力字段声明为 `True` 时,加载期由 [PluginCapabilityChecker.verifyCapabilityConsistency](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_capability_checker.py) 校验对应适配器列表非空(见 §1.6`supports_credential_cloning` 无专用适配器,不在校验范围。
> **`capabilities` 解析范围**[manifest\_loader.\_parse\_capabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/manifest_loader.py) 解析 [ChannelCapabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/dtos/capability.py) 的 **26 个 bool 字段**——5 个基础消息能力(`rich_message` / `streaming` / `typing_indicator` / `message_edit` / `message_recall`、4 个消息操作子能力(`supports_reaction` / `supports_pin` / `supports_card_update` / `supports_card_update_streaming`、4 个媒体能力(`supports_image_inbound` / `supports_video_inbound` / `supports_image_outbound` / `supports_video_outbound`FR-43、10 个扩展能力(`mention` / `command` / `directory` / `doctor` / `whitelist` / `wizard` / `tools` / `status` / `probeable` / `identity_resolver`、3 个生命周期与协作能力(`lifecycle` AL-01 / `supports_qr_login` QR-02 / `agent_collaboration`)。未声明的字段默认 `False`。能力字段声明为 `True` 时,加载期由 [PluginCapabilityChecker.verifyCapabilityConsistency](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_capability_checker.py) 校验对应适配器列表非空(见 §1.6`typing_indicator` / `identity_resolver` / `supports_credential_cloning` 无专用适配器,不在校验范围。
>
> **注意**`max_message_length`(消息长度上限)与 `supports_credential_cloning`(凭据克隆策略)是 `ChannelManifest` 的**顶层字段****不在** `capabilities` 对象内,声明位置见上表对应行。
---
@ -319,102 +347,119 @@ started/paused → stopped → unloaded → NOT_INSTALLED (终态)
## 5. CHANNEL_ENTRY 契约
每个插件 **必须** 通过 `CHANNEL_ENTRY` 契约注册,**不得** 隐式全局注入FR-32
每个插件 **必须** 通过 `CHANNEL_ENTRY` 契约注册,**不得** 隐式全局注入FR-32。签名 `(host, manifest) -> PluginManifest`(见 [entry.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/entry.py) 末尾类型别名):宿主在 discover 阶段从 `manifest.json` 解析出 `ChannelManifest` 后,于 loaded 阶段连同 `host` 一并传入。插件 **直接复用** 入参 `manifest` 作为返回 `PluginManifest.manifest`**不得** 在 entry 内重新构造声明性字段(`capabilities` / `config_schema` / `accessible_ports` / `injectable_pipelines` / `resource_quota` / `env_vars` / `credential_strategy`避免双真相源F-03 单真相源。entry 仅负责:端口获取 → client/handler 实例化 → `registerAdapter` 调用 → `registerLifecycleHandler` 注册 → 返回 `PluginManifest`
以下示例取自 [wechat_woc/entry.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/plugins/wechat_woc/entry.py)(已精简),展示 13 适配器型客户端渠道插件的标准写法feishu / wechat_ilink 同模式):
```python
# entry.py
from yuxi.channels.contract.plugin.entry import CHANNEL_ENTRY, PluginHost
from yuxi.channels.contract.plugin.entry import PluginHost
from yuxi.channels.contract.plugin.manifest import (
ChannelManifest,
PluginManifest,
FailurePolicy,
ResourceQuota,
)
from yuxi.channels.contract.dtos.capability import ChannelCapabilities
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.plugin.lifecycle import LifecycleHookHandler
from .adapters.outbound_adapter import FeishuOutboundAdapter
from .adapters.streaming_adapter import FeishuStreamingAdapter
from .lifecycle import FeishuLifecycleHandler
from .adapters.capability_prover import WocCapabilityProver
from .adapters.directory_adapter import WeChatWocDirectoryAdapter
from .adapters.doctor_adapter import WeChatWocDoctorAdapter
from .adapters.inbound_adapter import WeChatWocInboundAdapter
from .adapters.lifecycle_adapter import WeChatWocLifecycleAdapter
from .adapters.login_adapter import WeChatWocLoginAdapter
from .adapters.outbound_adapter import WeChatWocOutboundAdapter
from .adapters.probeable_adapter import WeChatWocProbeableAdapter
from .adapters.puller_adapter import WeChatWocPullerAdapter
from .adapters.session_adapter import WeChatWocSessionAdapter
from .adapters.status_adapter import WeChatWocStatusAdapter
from .adapters.stream_connector_adapter import WeChatWocStreamConnectorAdapter
from .adapters.whitelist_adapter import WeChatWocWhitelistAdapter
from .adapters.wizard_adapter import WeChatWocWizardAdapter
from .lifecycle import WeChatWocLifecycleHandler
from .woc_bridge_client import WocBridgeClient
# 运行时适配器清单(与 registerAdapter 调用顺序一致,非声明性字段)
_ADAPTER_TYPES: tuple[str, ...] = (
"puller", "stream_connector", "inbound", "outbound", "session",
"status", "login", "lifecycle", "probeable", "doctor",
"wizard", "whitelist", "directory",
)
def channel_entry(host: PluginHost) -> PluginManifest:
"""飞书渠道插件入口。"""
def channel_entry(host: PluginHost, manifest: ChannelManifest) -> PluginManifest:
"""微信 wechat_woc 渠道插件入口。
# 1. 声明能力边界(必须在 getXxxPort 之前)
host.declareAccessiblePorts((
"ConfigPort",
"LoggerPort",
"PersistencePort",
"CachePort",
"ConversationPort",
))
host.declareInjectablePipelines(("inbound", "outbound"))
host.declareResourceQuota(ResourceQuota(max_cpu="20.0", max_connections=50))
能力边界由 manifest 静态字段承载,宿主在加载期通过
PluginCapabilityChecker.check 校验,运行时通过 _checkPortAccess
从 manifest.accessible_ports 强制约束。本函数不重复构造声明性字段。
"""
# 2. 注册生命周期钩子onInit/onStart/onStop/onUnload
# registerLifecycleHandler 当前仅在 PluginHostImpl 实现,
# 尚未声明到 PluginHost Protocol运行时通过鸭子类型调用。
host.registerLifecycleHandler(FeishuLifecycleHandler())
# 1. 获取端口实例(宿主已从 manifest.accessible_ports 加载权限,
# getXxxPort 调用经 _checkPortAccess 校验,未声明抛 PermissionDeniedError
config_port = host.getConfigPort()
logger_port = host.getLoggerPort()
cache_port = host.getCachePort()
persistence_port = host.getPersistencePort()
# 3. 注册渠道适配器adapter_type 见 §1.5
host.registerAdapter("outbound", FeishuOutboundAdapter())
host.registerAdapter("streaming", FeishuStreamingAdapter())
# 4. 注册扩展点(按需)
# host.registerStageSlot(StageSlot(pipeline="outbound", anchor="after:format", stage=...))
# host.registerEventSubscription(EventSubscription(event_type="ConfigChanged", handler=...))
# host.registerConfigSource(ConfigSource(source_id="feishu_env", loader=...))
# host.registerMatchTier(MatchTier(name="...", priority=..., match_method="exact"))
# host.registerMatcher("custom_matcher", matcher_fn)
# host.registerCapabilityProver(prover)
# 5. 返回 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,
),
config_schema=(),
lifecycle=("init", "start", "stop", "unload"),
compatibility={"min_host_version": "1.0.0"},
accessible_ports=(
"ConfigPort",
"LoggerPort",
"PersistencePort",
"CachePort",
"ConversationPort",
),
injectable_pipelines=("inbound", "outbound"),
failure_policy=FailurePolicy.DEGRADE,
# 2. 实例化 client 与 LifecycleHandler
# handler 在 onInit 调 client.attach_http_client 注入连接池,
# config_schema / resource_quota 取自 manifestF-03 单真相源)
client = WocBridgeClient(config_port, cache_port, logger_port, plugin_version=manifest.version)
handler = WeChatWocLifecycleHandler(
config_port, logger_port, cache_port, manifest.config_schema, client,
max_connections=(
manifest.resource_quota.max_connections
if manifest.resource_quota is not None else 10
),
adapters=("outbound", "streaming"),
)
# 3. 注册 13 个适配器adapter_type 见 §1.5
# puller/outbound 接收 (client, config_port, logger_port)
# stream_connector/inbound/doctor/wizard/directory/login 接收 (client, logger_port)
# lifecycle/whitelist 接收 (cache_port, logger_port)
# probeable 接收 (client, persistence_port, logger_port)session/status 无状态
host.registerAdapter("puller", WeChatWocPullerAdapter(client, config_port, logger_port))
host.registerAdapter("stream_connector", WeChatWocStreamConnectorAdapter(client, config_port, logger_port))
host.registerAdapter("inbound", WeChatWocInboundAdapter(client, logger_port))
host.registerAdapter("outbound", WeChatWocOutboundAdapter(client, config_port, logger_port))
host.registerAdapter("session", WeChatWocSessionAdapter())
host.registerAdapter("status", WeChatWocStatusAdapter())
host.registerAdapter("login", WeChatWocLoginAdapter(client, logger_port))
host.registerAdapter("lifecycle", WeChatWocLifecycleAdapter(cache_port, logger_port))
host.registerAdapter("probeable", WeChatWocProbeableAdapter(client, persistence_port, logger_port))
host.registerAdapter("doctor", WeChatWocDoctorAdapter(client, logger_port))
host.registerAdapter("wizard", WeChatWocWizardAdapter(client, logger_port))
host.registerAdapter("whitelist", WeChatWocWhitelistAdapter(cache_port, logger_port))
host.registerAdapter("directory", WeChatWocDirectoryAdapter(client, logger_port))
# 4. 注册能力证明者FR-08 增强,对 manifest 声明的媒体出站能力运行时证明)
host.registerCapabilityProver(WocCapabilityProver(client))
# 5. 注册生命周期钩子registerLifecycleHandler 仅 PluginHostImpl 实现,
# 未声明到 Protocol运行时通过鸭子类型调用
host.registerLifecycleHandler(handler)
# 6. 返回 PluginManifestmanifest 直接复用入参adapters 为运行时清单)
return PluginManifest(manifest=manifest, adapters=_ADAPTER_TYPES)
CHANNEL_ENTRY = channel_entry
```
> **扩展点注册(按需)**`registerStageSlot` / `registerEventSubscription` / `registerConfigSource` / `registerMatchTier` / `registerMatcher` 可在 entry 内按需调用wechat_woc / feishu / wechat_ilink 三个内置插件当前均未注入自定义阶段或匹配层级依赖宿主默认装配8 个默认 MatchTier 见 §5.1)。
> **适配器 DI 风格**:适配器构造参数由插件自行设计,仅约束"无 mutable 跨请求状态INV-5+ 仅做协议转换INV-8"。三种典型形态:①持有 `(client, config_port, logger_port)` 需读 ConfigPort 的适配器puller / outbound / stream_connector②仅持有 `(client, logger_port)` 的纯协议转换适配器inbound / doctor / wizard / directory / login③无状态适配器session / status`__init__` 参数,所有数据从 raw_event.payload 提取)。
### 5.1 PluginHost Protocol 完整方法清单
[PluginHost](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/entry.py) Protocol`@runtime_checkable`)声明的方法(实现见 [PluginHostImpl](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_host_impl.py)
| 类别 | 方法 |
| --- | --- |
| 能力边界声明3 | `declareAccessiblePorts(ports: tuple[str, ...])` / `declareInjectablePipelines(pipelines: tuple[str, ...])` / `declareResourceQuota(quota: ResourceQuota)` |
| 端口获取17 | `getConfigPort` / `getLoggerPort` / `getPersistencePort` / `getCachePort` / `getTracerPort` / `getQueuePort` / `getConversationPort` / `getAgentRunPort` / `getIdentityResolverPort` / `getChannelAccountRepositoryPort` / `getChannelSessionRepositoryPort` / `getPairingRepositoryPort` / `getAuditLogRepositoryPort` / `getOutboxRepositoryPort` / `getUserIdentityRepositoryPort` / `getIdempotencyRepositoryPort` / `getPersistenceHealthPort` |
| 扩展点注册7 | `registerAdapter(adapter_type: str, adapter: PluginAdapter)` / `registerStageSlot(slot: StageSlot)` / `registerEventSubscription(subscription: EventSubscription)` / `registerConfigSource(source: ConfigSource)` / `registerCapabilityProver(prover: CapabilityProver)` / `registerMatchTier(tier: MatchTier)` / `registerMatcher(name: str, matcher: Matcher)` |
| 事件发布(异步) | `async publishEvent(event: DomainEvent) -> None` |
> **能力边界声明不在 Protocol**`PluginHost` Protocol **不** 声明 `declareAccessiblePorts` / `declareInjectablePipelines` / `declareResourceQuota`——这三项能力边界由 `manifest.json``accessible_ports` / `injectable_pipelines` / `resource_quota` 字段静态承载,宿主在 discover 阶段解析resolved 阶段由 `PluginCapabilityChecker.check` 校验,运行时由 `_checkPortAccess``manifest.accessible_ports` 强制约束(见 §1.4)。
> `registerLifecycleHandler` / `getLifecycleHandler` 已在 `PluginHostImpl` 实现但未声明到 Protocol见 §4 末尾说明)。
> **`MatchTier`** 定义在 `contract/dtos/route.py``name: str` / `priority: int` / `match_method: str` / `enabled: bool = True`。宿主在 [factory.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/infrastructure/factory.py) 注册 8 个默认匹配层级(升序:`default`=100 / `account`=200 / `channel_type`=300 / `channel_session`=400 / `chat_type`=500 / `peer_id`=600 / `identity_id`=700 / `session_key`=800其中 5 个内置 matchersession_key/identity_id/peer_id/account/default其余 3 个层级chat_type/channel_session/channel_type暂未实现匹配逻辑可由插件注入。
@ -638,8 +683,8 @@ CHANNEL_ENTRY = channel_entry
### 10.2 资源访问
- [ ] 未直接访问 `settings` / `logger` / DB pool / Redis client。
- [ ] 在调用任何 `getXxxPort()` 之前已通过 `declareAccessiblePorts(...)` 声明端口。
- [ ] 所有被驱动依赖通过 `PluginHost.getXxxPort()` 获取。
- [ ] `manifest.json``accessible_ports` 声明了所有将调用的 `getXxxPort()` 对应端口(宿主运行时据 `_checkPortAccess` 校验,未声明抛 `PermissionDeniedError`)。
- [ ] 未修改宿主中间件链、路由表、事件订阅器(仅通过 `registerXxx()` 注入)。
- [ ] `accessible_ports` 为 §1.4 中 17 个端口的子集,`injectable_pipelines` 为 `inbound` / `outbound` / `control-plane` 的子集。
- [ ] `resource_quota.max_cpu` 不超过 50.0%。
@ -694,7 +739,7 @@ CHANNEL_ENTRY = channel_entry
- [extension_point.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/extension_point.py) — `StageSlot` / `EventSubscription` / `ConfigSource` / `Stage` / `ConflictStrategy` / `FailureStrategy`
- [adapters/](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/) — 23 个适配器 Protocol15 个基础 + `LifecycleAdapter` + `LoginAdapter` + `ProbeableAdapter` + `ContentModerationAdapter` + `PullerAdapter` + `StreamConnectorAdapter` + `AttachmentUploadAdapter` + `WebhookTestAdapter`;已删除 `MediaAdapter` / `OAuthAdapter` 孤儿协议)
- [contract/plugin/\_\_init\_\_.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/__init__.py) — 契约层导出清单38 个符号manifest 9 + entry 2 + capability 3 + lifecycle 3 + extension_point 6 + adapters 23未导出 `PluginAdapter` / `Matcher`
- [contract/dtos/capability.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/dtos/capability.py) — `ChannelCapabilities`22 个字段)/ `CapabilityResult`
- [contract/dtos/capability.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/dtos/capability.py) — `ChannelCapabilities`26 个 bool 字段)/ `CapabilityResult`
- [contract/dtos/channel.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/dtos/channel.py) — `ChannelType`8 个枚举值)/ `AccountStatus` / `OnboardingStatus` / `SessionStatus`
- [contract/dtos/route.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/dtos/route.py) — `MatchTier`
- [contract/ports/driven/aggregate.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/ports/driven/aggregate.py) — `DrivenAdapters` 聚合13 单实例 + 23 列表 = 36 字段)