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:
parent
532e7c59ec
commit
fd885e5323
@ -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/
|
||||
└── <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 # 插件清单(元数据、能力声明、配置 schema,F-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 取自 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 字段)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user