diff --git a/backend/package/yuxi/channels/plugins/README.md b/backend/package/yuxi/channels/plugins/README.md index 112e1e38..19e18336 100644 --- a/backend/package/yuxi/channels/plugins/README.md +++ b/backend/package/yuxi/channels/plugins/README.md @@ -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`(复用 StreamingAdapter,FR-12 流式卡片更新) | | `message_edit` / `message_recall` / `supports_reaction` / `supports_pin` / `supports_card_update`(任一为 True) | `message_ops_adapters` | | `supports_image_inbound` / `supports_video_inbound`(任一为 True,FR-43) | `inbound_adapters` | | `supports_image_outbound` / `supports_video_outbound`(任一为 True,FR-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/ -└── / # 如 feishu/、dingtalk/、wecom/ +└── / # 如 wechat_woc/、feishu/、wechat_ilink/ ├── __init__.py - ├── manifest.json # 插件清单(元数据、能力声明、配置 schema) - ├── entry.py # CHANNEL_ENTRY 入口函数 - ├── lifecycle.py # LifecycleHookHandler 实现(如需响应生命周期) - ├── adapters/ # 适配器实现 + ├── manifest.json # 插件清单(元数据、能力声明、配置 schema,F-03 单真相源) + ├── entry.py # CHANNEL_ENTRY 入口函数(host, manifest)-> PluginManifest + ├── lifecycle.py # LifecycleHookHandler 实现(onInit/onStart/.../onFail,管理 httpx 连接池等插件级资源) + ├── _constants.py # 渠道共享常量(配置默认值/依赖标识,与 manifest.json default 对齐) + ├── _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//` 子目录内创建文件。 - 跨渠道共享的 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 取自 manifest(F-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. 返回 PluginManifest(manifest 直接复用入参,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 个内置 matcher(session_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 个适配器 Protocol(15 个基础 + `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 字段)