新增完整的 channels 限界上下文模块,包含契约层、领域核心层、应用服务、管道编排、插件体系、基础设施组合根等全层级代码,新增飞书与微信 iLink 渠道插件基础结构,补充各类 DTO、端口协议与领域服务实现。
455 lines
35 KiB
Markdown
455 lines
35 KiB
Markdown
# 渠道插件包
|
||
|
||
本目录存放各渠道插件实现。每个渠道插件以独立子目录形式装配,通过 `CHANNEL_ENTRY` 契约注册到宿主。
|
||
|
||
**最高原则**:渠道开发 **不得** 污染框架层。框架层包括 `yuxi.channels` 包内除 `plugins/` 之外的所有层(契约层、领域核心、应用服务、组合根、被驱动适配器)。若渠道开发需要框架层变更支持,**必须** 走 §2 的变更申请流程,**不得** 自行修改。
|
||
|
||
---
|
||
|
||
## 1. 边界红线:渠道开发不得污染框架层
|
||
|
||
### 1.1 框架层范围
|
||
|
||
| 层 | 路径 | 插件可执行操作 |
|
||
| --- | --- | --- |
|
||
| 契约层 | `yuxi.channels.contract.*` | **只读 import**(不得新增/修改文件) |
|
||
| 领域核心层 | `yuxi.channels.core.*` | **禁止** import |
|
||
| 应用服务层 | `yuxi.channels.application.*` | **禁止** import |
|
||
| 组合根层 | `yuxi.channels.infrastructure.*` | **禁止** import |
|
||
| 被驱动适配器层 | `yuxi.channels.adapters.*` | **禁止** import |
|
||
| 插件层 | `yuxi.channels.plugins.<channel>.*` | 可自由编写 |
|
||
|
||
> 契约层文件的增删改属于框架层变更,**必须** 由架构角色评审后实施,渠道开发者 **不得** 直接修改(见 §2)。
|
||
|
||
### 1.2 依赖方向铁律(INV-1 / INV-6)
|
||
|
||
依据 [演进式六边形-管道-插件架构规范.md](file:///d:/ForcePilot-v1.1/docs/vibe/v1.0/规范文档/演进式六边形-管道-插件架构规范.md) §5.1:
|
||
|
||
- 所有依赖 **必须** 指向契约方向(外 → 内),内圈 **不得** 引用外圈类型。
|
||
- 插件 **不得** 直接引用宿主内部实现,**仅** 依赖 `yuxi.channels.contract.*`。
|
||
- 跨边界共享的数据结构 **必须** 定义在契约层,由内外圈共同引用。
|
||
- 依赖方向 **不得** 因"便利"而绕过,任何绕过都 **必须** 在评审中记录理由。
|
||
|
||
### 1.3 插件可依赖的契约入口
|
||
|
||
插件 **只能** import 以下契约层符号(适配器协议导出见 [contract/plugin/adapters/\_\_init\_\_.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/__init__.py),其余符号见 [contract/plugin/\_\_init\_\_.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/__init__.py)):
|
||
|
||
| 契约模块 | 内容 |
|
||
| --- | --- |
|
||
| `yuxi.channels.contract.plugin.entry` | `CHANNEL_ENTRY` / `PluginHost` |
|
||
| `yuxi.channels.contract.plugin.manifest` | `PluginManifest` / `ChannelManifest` / `ResourceQuota` / `PluginDependency` / `ConfigField` / `FailurePolicy` |
|
||
| `yuxi.channels.contract.plugin.adapters` | 23 个适配器 `Protocol`(`InboundAdapter` / `OutboundAdapter` / `SessionAdapter` / `RichMessageAdapter` / `StreamingAdapter` / `DirectoryAdapter` / `CommandAdapter` / `WizardAdapter` / `DoctorAdapter` / `WhitelistAdapter` / `ToolsAdapter` / `MessageOpsAdapter` / `StatusAdapter` / `MentionAdapter` / `IdentityResolverAdapter` / `LifecycleAdapter` / `LoginAdapter` / `ProbeableAdapter` / `ContentModerationAdapter` / `PullerAdapter` / `StreamConnectorAdapter` / `AttachmentUploadable` / `WebhookTestable`) |
|
||
| `yuxi.channels.contract.plugin.capability` | `CapabilityDeclaration` / `CapabilityProof` / `CapabilityProver` |
|
||
| `yuxi.channels.contract.plugin.lifecycle` | `LifecycleState` / `LifecycleHook` / `LifecycleHookHandler` |
|
||
| `yuxi.channels.contract.plugin.extension_point` | `StageSlot` / `EventSubscription` / `ConfigSource` / `Stage` / `ConflictStrategy` / `FailureStrategy` |
|
||
| `yuxi.channels.contract.dtos.*` | 跨边界共享 DTO |
|
||
| `yuxi.channels.contract.errors.*` | 统一错误类型层级 |
|
||
| `yuxi.channels.contract.ports.driven.*` / `yuxi.channels.contract.ports.driving.*` | 端口定义(仅供类型标注,实现由宿主注入) |
|
||
|
||
> **适配器协议方法风格**:所有适配器 `Protocol` 使用 `@runtime_checkable` 装饰,方法签名统一为 `async def`。方法体区分两种风格:必选方法为 `raise NotImplementedError`(实现类必须重写),可选方法为 `return None` / `return False`(实现类按需重写,默认不操作)。例如 `InboundAdapter.normalizeInbound` 为必选,`InboundAdapter.downloadAttachment`(FR-49 媒体下载,可选)默认 `return None` 表示未实现,由 [MediaFetchStage](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/pipeline/inbound/media_fetch_stage.py) 据此剔除附件并降级。`LifecycleAdapter` 与 `LoginAdapter` 的全部方法均为必选(`raise NotImplementedError`),声明对应能力的插件 **必须** 实现全部方法。
|
||
|
||
### 1.4 资源访问约束(INV-I7 / INV-6 / FR-32)
|
||
|
||
插件 **不得** 直接访问以下宿主资源,**必须** 通过 [PluginHost](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/entry.py) 提供的端口获取方法与扩展点注册方法获取依赖:
|
||
|
||
| 禁止直接访问 | 替代方式 |
|
||
| --- | --- |
|
||
| 宿主 `settings` | `host.getConfigPort()` |
|
||
| 宿主 `logger` | `host.getLoggerPort()` |
|
||
| 数据库连接池 | `host.getPersistencePort()` |
|
||
| Redis 客户端 | `host.getCachePort()` |
|
||
| 追踪器 | `host.getTracerPort()` |
|
||
| 队列 | `host.getQueuePort()` |
|
||
| 会话存储 | `host.getConversationPort()` |
|
||
| Agent 运行时 | `host.getAgentRunPort()` |
|
||
| 身份解析器 | `host.getIdentityResolverPort()` |
|
||
| 宿主中间件链 / 路由表 / 事件订阅器 | `host.registerStageSlot()` / `host.registerEventSubscription()` / `host.registerConfigSource()` / `host.registerMatchTier()` / `host.registerMatcher()` |
|
||
|
||
**端口访问前置声明(关键)**:插件在 `CHANNEL_ENTRY` 中调用任何 `getXxxPort()` **之前**,**必须** 先调用 `host.declareAccessiblePorts(...)` 声明将访问的端口名称元组。宿主在 `PluginHostImpl._checkPortAccess` 中校验每次端口获取是否在已声明集合内,未声明时抛 `PermissionDeniedError`。
|
||
|
||
可声明的端口名称(17 个,为 [PluginCapabilityChecker.ALLOWED_PORTS](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_capability_checker.py) 的子集):
|
||
|
||
| 类别 | 端口名称 |
|
||
| --- | --- |
|
||
| 基础被驱动端口(9 个) | `ConfigPort` / `LoggerPort` / `PersistencePort` / `CachePort` / `TracerPort` / `QueuePort` / `ConversationPort` / `AgentRunPort` / `IdentityResolverPort` |
|
||
| 仓储子端口(8 个,均由 `PersistencePort` 聚合实现) | `ChannelAccountRepositoryPort` / `ChannelSessionRepositoryPort` / `PairingRepositoryPort` / `AuditLogRepositoryPort` / `OutboxRepositoryPort` / `UserIdentityRepositoryPort` / `IdempotencyRepositoryPort` / `PersistenceHealthPort` |
|
||
|
||
> 仓储子端口在运行时返回 `DrivenAdapters.persistence` 聚合别名,该聚合同时实现全部 8 个细分子端口。`IdentityResolverPort` 默认为 `None`,由插件通过 `registerAdapter("identity_resolver", ...)` 注入后才可获取。
|
||
|
||
### 1.5 适配器注册(FR-32)
|
||
|
||
插件通过 `host.registerAdapter(adapter_type, adapter)` 注入渠道适配器。宿主 [PluginHostImpl.\_ADAPTER\_SETTERS](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_host_impl.py) 支持 24 种 `adapter_type`(1 个单实例 + 23 个列表追加,含 23 个适配器 Protocol + 1 个渠道上下文提供者 Port):
|
||
|
||
| adapter_type | 写入方式 | 对应 Protocol / Port |
|
||
| --- | --- | --- |
|
||
| `identity_resolver` | 单实例直接赋值 | `IdentityResolverAdapter` |
|
||
| `inbound` | 列表追加 | `InboundAdapter` |
|
||
| `outbound` | 列表追加 | `OutboundAdapter` |
|
||
| `status` | 列表追加 | `StatusAdapter` |
|
||
| `rich_message` | 列表追加 | `RichMessageAdapter` |
|
||
| `tools` | 列表追加 | `ToolsAdapter` |
|
||
| `message_ops` | 列表追加 | `MessageOpsAdapter` |
|
||
| `directory` | 列表追加 | `DirectoryAdapter` |
|
||
| `whitelist` | 列表追加 | `WhitelistAdapter` |
|
||
| `wizard` | 列表追加 | `WizardAdapter` |
|
||
| `doctor` | 列表追加 | `DoctorAdapter` |
|
||
| `command` | 列表追加 | `CommandAdapter` |
|
||
| `mention` | 列表追加 | `MentionAdapter` |
|
||
| `streaming` | 列表追加 | `StreamingAdapter` |
|
||
| `session` | 列表追加 | `SessionAdapter` |
|
||
| `channel_context_provider` | 列表追加 | `ChannelContextProviderPort`(见 [contract/ports/driven/channel_context_provider_port.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/ports/driven/channel_context_provider_port.py),**为 Port 而非适配器 Protocol**) |
|
||
| `lifecycle` | 列表追加 | `LifecycleAdapter`(见 [contract/plugin/adapters/lifecycle_adapter.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/lifecycle_adapter.py),AL-01 账户生命周期介入) |
|
||
| `login` | 列表追加 | `LoginAdapter`(见 [contract/plugin/adapters/login_adapter.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/login_adapter.py),QR-01 扫码登录) |
|
||
| `probeable` | 列表追加 | `ProbeableAdapter`(见 [contract/plugin/adapters/probeable_adapter.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/probeable_adapter.py),FR-35 主动探测,可选实现) |
|
||
| `content_moderation` | 列表追加 | `ContentModerationAdapter`(见 [contract/plugin/adapters/content_moderation_adapter.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/content_moderation_adapter.py),CR-01 内容预审核,可选实现) |
|
||
| `puller` | 列表追加 | `PullerAdapter`(见 [contract/plugin/adapters/puller_adapter.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/puller_adapter.py),传输引擎客户端型渠道轮询接入,可选实现) |
|
||
| `stream_connector` | 列表追加 | `StreamConnectorAdapter`(见 [contract/plugin/adapters/stream_connector_adapter.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/stream_connector_adapter.py),传输引擎客户端型渠道长连接接入,可选实现) |
|
||
| `attachment_upload` | 列表追加 | `AttachmentUploadable`(见 [contract/plugin/adapters/attachment_upload_adapter.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/attachment_upload_adapter.py),MSG-ATTACH-UPLOAD 附件上传,可选实现) |
|
||
| `webhook_test` | 列表追加 | `WebhookTestable`(见 [contract/plugin/adapters/webhook_test_adapter.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/adapters/webhook_test_adapter.py),WHK-TEST Webhook 测试事件发起,可选实现) |
|
||
|
||
未列出的 `adapter_type` 抛 `RuleViolationError`。
|
||
|
||
> **传输引擎适配器说明**:`puller` / `stream_connector` 由 [TransportManager](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/transport/manager.py) 在宿主启动时通过 `PluginRegistry.listPluginAdapters()` 收集,驱动 per-account 传输任务生命周期。插件注册这两类适配器后,账号启用/禁用由 `ChannelAccountOnline` / `ChannelAccountOffline` 领域事件自动触发,插件 **不得** 在 `LifecycleAdapter` 中自管理 WS 连接或轮询任务(详见 [channels-transport-engine-设计方案-v1.0.md](file:///d:/ForcePilot-v1.1/docs/vibe/v1.0/设计方案/channels-transport-engine-设计方案-v1.0.md))。
|
||
|
||
### 1.6 能力声明与校验
|
||
|
||
能力校验分两层,插件开发者需同时满足:
|
||
|
||
**加载期静态校验**(由 [PluginCapabilityChecker](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_capability_checker.py) 执行):
|
||
|
||
1. `check(manifest)`:校验 `accessible_ports` / `injectable_pipelines` / `resource_quota.max_cpu`(≤ 50.0%)是否在允许范围内,违规抛 `PermissionDeniedError` / `ValidationError`。
|
||
2. `verifyCapabilityConsistency(manifest, adapters)`:清单 `capabilities` 中声明为 `True` 的能力 **必须** 有对应的运行时适配器注册(FR-21 契约一致性)。映射关系:
|
||
|
||
| 能力字段 | 要求非空的适配器列表 |
|
||
| --- | --- |
|
||
| `rich_message` | `rich_message_adapters` |
|
||
| `streaming` | `streaming_adapters` |
|
||
| `mention` | `mention_adapters` |
|
||
| `command` | `command_adapters` |
|
||
| `directory` | `directory_adapters` |
|
||
| `doctor` | `doctor_adapters` |
|
||
| `whitelist` | `whitelist_adapters` |
|
||
| `lifecycle` | `lifecycle_adapters` |
|
||
| `supports_qr_login` | `login_adapters` |
|
||
| `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` 无专用适配器(由出站适配器承载),不在校验范围。`lifecycle`(AL-01)与 `supports_qr_login`(QR-02)由 [PluginLifecycleManager.\_parse\_capabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_lifecycle_manager.py) 从 `manifest.json` 解析,声明为 `True` 时触发对应适配器列表非空校验。
|
||
|
||
**运行时能力证明**(由插件实现 `CapabilityProver`,出站中间件调用):
|
||
|
||
- `proveCapability(capability_name)` 返回 `CapabilityProof`,结果可缓存(默认 TTL 300s)。
|
||
- 静态声明支持但运行时证明不支持时,**必须** 记录告警日志并走降级路径(FR-08 增强 / FR-21)。
|
||
|
||
### 1.7 适配器职责约束(INV-8)
|
||
|
||
插件提供的适配器 **只做协议转换与错误翻译**,**不得** 承载业务规则、事务编排、跨请求状态。业务规则归属领域核心,事务边界归属应用服务层。
|
||
|
||
### 1.8 错误显式化(INV-7)
|
||
|
||
- 契约违反、配置错误、插件故障 **必须** 显式抛出 `yuxi.channels.contract.errors.*` 中的统一异常,**禁止** 用防御性回退掩盖。
|
||
- **禁止** 静默吞错(如 `except Exception: pass`);观测层失败允许降级,但 **必须** 通过 `LoggerPort` 记录 warning。
|
||
- 原生异常(网络库、协议库异常)**必须** 在适配器内捕获并转换为统一异常,**禁止** 直接向外抛出。
|
||
|
||
---
|
||
|
||
## 2. 框架层变更申请流程
|
||
|
||
当渠道开发遇到以下情况时,**不得** 自行修改框架层,**必须** 提出变更申请:
|
||
|
||
- 契约层缺少必要端口 / DTO / Protocol 方法。
|
||
- 组合根层缺少必要能力(如新的扩展点类型、阶段插槽锚点)。
|
||
- 管道缺少必要的阶段插槽。
|
||
- 端口签名需要扩展(须走版本化流程,对应 INV-4 端口契约稳定性)。
|
||
- `PluginHost` 缺少必要的端口获取方法或扩展点注册方法。
|
||
|
||
### 2.1 申请方式
|
||
|
||
1. 在 `docs/vibe/v1.x/问题文档/渠道开发/` 下新建问题清单文档(命名 `YYYY-MM-DD-<channel>-前置问题清单.md`),内容包括:
|
||
- **缺口描述**:缺少什么、为什么需要、当前无法实现的用例。
|
||
- **影响范围**:契约层 / 组合根层 / 应用层,涉及的文件路径。
|
||
- **期望方案**:建议的变更方案与替代方案。
|
||
- **紧急程度**:P0(阻断渠道开发)/ P1(影响核心能力)/ P2(增强能力)。
|
||
2. 提交架构评审,由架构角色确认方案后,**由框架开发者实施** 框架层变更。
|
||
3. 变更完成后,渠道开发者同步切换到新契约,**不得** 在插件内保留对旧实现的绕过逻辑。
|
||
|
||
### 2.2 严禁的行为
|
||
|
||
- 在 `plugins/<channel>/` 之外的目录直接修改代码以"临时"支持某渠道。
|
||
- 在插件内通过反射、monkey-patch、import private symbol(`_` 前缀)、`TYPE_CHECKING` 之外的方式绕过契约边界。
|
||
- 在 `yuxi.channels.core.*` 中 import 任何框架类型(违反 INV-2 核心纯净性)。
|
||
- 自行在契约层新增 DTO / Protocol 方法以"补全"渠道所需能力。
|
||
|
||
---
|
||
|
||
## 3. 标准布局
|
||
|
||
每个渠道插件遵循以下目录结构:
|
||
|
||
```
|
||
plugins/
|
||
└── <channel_name>/ # 如 feishu/、dingtalk/、wecom/
|
||
├── __init__.py
|
||
├── manifest.json # 插件清单(元数据、能力声明、配置 schema)
|
||
├── entry.py # CHANNEL_ENTRY 入口函数
|
||
├── lifecycle.py # LifecycleHookHandler 实现(如需响应生命周期)
|
||
├── adapters/ # 适配器实现
|
||
│ ├── __init__.py
|
||
│ ├── outbound_adapter.py
|
||
│ ├── streaming_adapter.py
|
||
│ └── ...
|
||
└── dtos/ # 渠道特有 DTO(可选,仅插件内部使用)
|
||
└── __init__.py
|
||
```
|
||
|
||
**布局约束**:
|
||
|
||
- 插件 **只能** 在自身 `plugins/<channel>/` 子目录内创建文件。
|
||
- 跨渠道共享的 DTO **必须** 通过契约层变更申请提升到 `yuxi.channels.contract.dtos.*`,**不得** 在插件间互相 import。
|
||
- 渠道特有 DTO **不得** 泄漏到契约层,留在插件自身的 `dtos/` 目录内。
|
||
|
||
### 3.1 manifest.json schema
|
||
|
||
`manifest.json` 由宿主在 discovered 阶段解析为 [ChannelManifest](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/manifest.py)。字段如下:
|
||
|
||
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | string | 是 | — | 全局唯一标识(如 `com.yuxi.channels.feishu`) |
|
||
| `name` | string | 是 | — | 人类可读名称 |
|
||
| `version` | string | 是 | — | 语义化版本 |
|
||
| `channel_type` | string | 是 | — | 渠道类型枚举值(`feishu` / `dingtalk` / `wecom` / `webchat` / `telegram` / `discord` / `whatsapp` / `custom`) |
|
||
| `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) 字段,未声明默认 `false`;`max_message_length` 默认 `4096`) |
|
||
| `config_schema` | array | 是 | — | 配置项 schema(每项含 `key` / `type` / `required` / `default` / `hot_reloadable` / `constraints`) |
|
||
| `provides` | array | 是 | — | 提供的能力列表 |
|
||
| `lifecycle` | array | 是 | — | 支持的生命周期钩子(如 `["init","start","stop","unload"]`) |
|
||
| `compatibility` | object | 是 | — | 兼容性信息(自由结构 dict,由插件自行约定语义,框架仅透传不解释) |
|
||
| `failure_policy` | string | 是 | — | 失败策略(`degrade` / `circuit_break` / `isolate`) |
|
||
| `depends` | array | 否 | `[]` | 依赖的其他插件(每项含 `plugin_id` / `version_range`) |
|
||
| `resource_quota` | object | 否 | `null` | 资源配额(`max_cpu` ≤ 50.0、`max_memory`、`max_connections`、`max_calls_per_sec`) |
|
||
| `accessible_ports` | array | 否 | `[]` | 可访问的端口名称列表(必须为 §1.4 中 17 个端口的子集) |
|
||
| `injectable_pipelines` | array | 否 | `[]` | 可注入的管道列表(`inbound` / `outbound` / `control-plane` 的子集) |
|
||
| `skills` | array | 否 | `[]` | 提供的技能列表 |
|
||
| `env_vars` | array | 否 | `[]` | 环境变量声明(每项含 `name` / `description` / `required` / `default` / `sensitive`) |
|
||
| `critical` | bool | 否 | `false` | 是否为关键渠道(失败时宿主标记 `unhealthy`,否则标记 `degraded`,FR-31 / FR-35) |
|
||
| `requires_dm_pairing` | bool | 否 | `true` | 是否要求 DM 安全配对审批(声明 `false` 跳过配对审批,FR-31;插件未注册时框架回退为 `true`,fail-closed) |
|
||
| `requires_outbound_delivery` | bool | 否 | `true` | 是否要求出站投递(声明 `false` 无需实现 `OutboundAdapter`,框架跳过出站投递阶段,FR-31) |
|
||
|
||
> **必填字段说明**:`provides` / `lifecycle` / `compatibility` / `failure_policy` 在 `ChannelManifest` dataclass 中虽有默认值,但 [PluginLifecycleManager._parse_manifest](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_lifecycle_manager.py) 的 `required_fields` 校验将它们标记为 JSON 必填,缺失时抛 `ValidationError`。
|
||
|
||
> **`entry_module` 说明**:宿主通过 `importlib.import_module(manifest.entry_module)` 动态导入,必须为可 import 的完整模块路径。返回的 `PluginManifest.manifest.id` **必须** 与 `manifest.json` 的 `id` 一致,否则抛 `ValidationError`。
|
||
|
||
> **`capabilities` 解析范围**:[PluginLifecycleManager.\_parse\_capabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_lifecycle_manager.py) 完整解析 [ChannelCapabilities](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/dtos/capability.py) 的 20 个字段——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)、2 个新增能力(`lifecycle` AL-01 / `supports_qr_login` QR-02)。未声明的字段默认 `False` 或 `4096`。能力字段声明为 `True` 时,加载期由 [PluginCapabilityChecker.verifyCapabilityConsistency](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_capability_checker.py) 校验对应适配器列表非空(见 §1.6)。
|
||
|
||
---
|
||
|
||
## 4. 插件加载时序
|
||
|
||
依据 [演进式六边形-管道-插件架构规范.md](file:///d:/ForcePilot-v1.1/docs/vibe/v1.0/规范文档/演进式六边形-管道-插件架构规范.md) §9,插件生命周期状态机(见 [PluginStateMachine](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_state_machine.py)):
|
||
|
||
```
|
||
discovered → resolved → loaded → initialized → started ⇄ paused
|
||
↓ ↓
|
||
failed ←─────── (任一阶段失败)
|
||
↓
|
||
resolved (重载,FR-36)
|
||
|
||
started/paused → stopped → unloaded (终态)
|
||
```
|
||
|
||
**完整加载流程**(由 [PluginLifecycleManager](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_lifecycle_manager.py) 编排):
|
||
|
||
1. **discovered**:宿主扫描 `plugins/` 目录下每个子目录的 `manifest.json`,解析为 `ChannelManifest` 并注册到 `PluginRegistry`。无 `manifest.json` 的子目录被跳过;解析或注册冲突记录错误日志并跳过**该插件**,不阻塞其他插件(FR-21)。
|
||
2. **resolved**:[PluginCapabilityChecker.check](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_capability_checker.py) 校验能力边界(`accessible_ports` / `injectable_pipelines` / `resource_quota`)。
|
||
3. **loaded**:`importlib` 导入 `entry_module`,调用 `CHANNEL_ENTRY(host)` 获取 `PluginManifest`,校验清单 ID 一致性。插件在 `CHANNEL_ENTRY` 内通过 `host.registerAdapter` 注入适配器,通过 `host.registerStageSlot` / `registerEventSubscription` / `registerConfigSource` / `registerMatchTier` / `registerMatcher` 注入扩展点,通过 `host.registerLifecycleHandler` 注册生命周期钩子。随后 `verifyCapabilityConsistency` 校验声明能力与运行时适配器一致性(FR-21)。
|
||
4. **initialized**:调用 `LifecycleHookHandler.onInit`(超时 60s)。
|
||
5. **started**:调用 `onStart`(超时 60s),插件开始处理请求。成功后将 `manifest.version` 追加写入 `applied_migrations` 配置键(ACCOUNT 作用域,FR17-P0-3),标记配置已加载;写入失败抛异常触发降级。
|
||
6. **paused / resumed**:调用 `onPause` / `onResume`(超时 30s)。
|
||
7. **stopped**:调用 `onStop`(超时 30s,必须等待在途请求完成)。
|
||
8. **unloaded**:调用 `onUnload`(超时 30s,必须释放所有资源),注销扩展点注册,从 `PluginRegistry` 注销。
|
||
9. **failed**(任一阶段失败):标记 `FAILED` 状态,发布 `PluginFailed` 事件,调用 `onFail` 钩子(超时 30s),触发 [DegradationManager](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/core/service/degradation_manager.py) 优雅降级(FR-36),发布 `ChannelDegraded` 事件,释放插件占用的扩展点注册与 host 实例。失败插件由后台任务(`reloadLoop`,默认 60s 间隔)按退避时间自动重载(`FAILED → RESOLVED → … → STARTED`)。
|
||
|
||
**生命周期钩子完整列表**(见 [LifecycleHookHandler](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/lifecycle.py)):
|
||
|
||
| 钩子方法 | 触发阶段 | 超时 | 用途 |
|
||
| --- | --- | --- | --- |
|
||
| `onInit` | initialized | 60s | 初始化资源(连接、缓存预热) |
|
||
| `onStart` | started | 60s | 开始处理请求 |
|
||
| `onPause` | paused | 30s | 暂停接收新请求 |
|
||
| `onResume` | started(从 paused) | 30s | 恢复接收请求 |
|
||
| `onStop` | stopped | 30s | 等待在途请求完成 |
|
||
| `onUnload` | unloaded | 30s | 释放所有资源 |
|
||
| `onReconfigure` | 配置热更新(FR-37) | — | 接收新配置字典,**必须** 支持回滚 |
|
||
| `onFail` | failed | 30s | 失败清理(由宿主在 `_handle_failure` 中调用) |
|
||
|
||
**`reloadLoop` 多 Worker 保护**:后台扫描任务通过 `CachePort.acquireAdvisoryLock("plugin_reload_scanner", ttl_seconds=interval)` 获取分布式咨询锁,锁未获取时跳过本轮,避免多 Worker 重复重载失败插件(§10.2 并发控制)。正常路径在 `finally` 中释放锁,锁 TTL 与扫描间隔一致作为崩溃恢复安全网。
|
||
|
||
**生命周期约束**:
|
||
|
||
- 插件 **不得** 在 `started` 之前对外提供服务。
|
||
- 插件 **必须** 支持 `stopped` → `unloaded` 的干净退出,**不得** 遗留线程、连接、临时资源。
|
||
- 插件失败 **不得** 拖垮宿主:宿主负责按扩展点策略降级或熔断(FR-36)。
|
||
- 生命周期钩子超时后标记插件 `FAILED` 并触发降级。
|
||
- `onReconfigure` **必须** 支持配置回滚(FR-37),配置应用失败时恢复至上一次有效配置。
|
||
|
||
---
|
||
|
||
## 5. CHANNEL_ENTRY 契约
|
||
|
||
每个插件 **必须** 通过 `CHANNEL_ENTRY` 契约注册,**不得** 隐式全局注入(FR-32):
|
||
|
||
```python
|
||
# entry.py
|
||
from yuxi.channels.contract.plugin.entry import CHANNEL_ENTRY, 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
|
||
|
||
|
||
def channel_entry(host: PluginHost) -> PluginManifest:
|
||
"""飞书渠道插件入口。"""
|
||
|
||
# 1. 声明能力边界(必须在 getXxxPort 之前)
|
||
host.declareAccessiblePorts((
|
||
"ConfigPort",
|
||
"LoggerPort",
|
||
"PersistencePort",
|
||
"CachePort",
|
||
"ConversationPort",
|
||
))
|
||
host.declareInjectablePipelines(("inbound", "outbound"))
|
||
host.declareResourceQuota(ResourceQuota(max_cpu="20.0", max_connections=50))
|
||
|
||
# 2. 注册生命周期钩子(onInit/onStart/onStop/onUnload)
|
||
# registerLifecycleHandler 当前仅在 PluginHostImpl 实现,
|
||
# 尚未声明到 PluginHost Protocol,运行时通过鸭子类型调用。
|
||
host.registerLifecycleHandler(FeishuLifecycleHandler())
|
||
|
||
# 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=...))
|
||
|
||
# 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,
|
||
),
|
||
adapters=("outbound", "streaming"),
|
||
)
|
||
|
||
|
||
CHANNEL_ENTRY = channel_entry
|
||
```
|
||
|
||
> **registerLifecycleHandler 说明**:`PluginHost` Protocol([entry.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/entry.py))当前未声明 `registerLifecycleHandler` / `getLifecycleHandler` 方法,但 [PluginHostImpl](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_host_impl.py) 已实现。插件运行时可通过鸭子类型调用;如需静态类型检查支持,走 §2 变更申请流程补充 Protocol 声明。
|
||
|
||
---
|
||
|
||
## 6. 禁止事项清单(Code Review Checklist)
|
||
|
||
提交渠道代码前,逐项确认:
|
||
|
||
### 6.1 边界
|
||
|
||
- [ ] 未修改 `plugins/<channel>/` 之外的任何文件。
|
||
- [ ] 未 import `yuxi.channels.core.*` / `yuxi.channels.application.*` / `yuxi.channels.infrastructure.*` / `yuxi.channels.adapters.*`。
|
||
- [ ] 未新增 / 修改 `yuxi.channels.contract.*` 中的任何文件(契约层变更走 §2 流程)。
|
||
- [ ] 未在插件间互相 import 内部实现(跨渠道共享走契约层提升)。
|
||
- [ ] 需要框架层支持时已走变更申请流程,未自行突破边界。
|
||
|
||
### 6.2 资源访问
|
||
|
||
- [ ] 未直接访问 `settings` / `logger` / DB pool / Redis client。
|
||
- [ ] 在调用任何 `getXxxPort()` 之前已通过 `declareAccessiblePorts(...)` 声明端口。
|
||
- [ ] 所有被驱动依赖通过 `PluginHost.getXxxPort()` 获取。
|
||
- [ ] 未修改宿主中间件链、路由表、事件订阅器(仅通过 `registerXxx()` 注入)。
|
||
- [ ] `accessible_ports` 为 §1.4 中 17 个端口的子集,`injectable_pipelines` 为 `inbound` / `outbound` / `control-plane` 的子集。
|
||
- [ ] `resource_quota.max_cpu` 不超过 50.0%。
|
||
|
||
### 6.3 契约与异常
|
||
|
||
- [ ] 通过 `CHANNEL_ENTRY` 显式注册,未隐式全局注入。
|
||
- [ ] `manifest.json` 的 `entry_module` 为可 import 的完整模块路径,返回清单 ID 与声明 ID 一致。
|
||
- [ ] `manifest.json` 包含全部必填字段(`id` / `name` / `version` / `channel_type` / `entry_module` / `capabilities` / `config_schema` / `provides` / `lifecycle` / `compatibility` / `failure_policy`)。
|
||
- [ ] 清单 `capabilities` 声明为 `True` 的能力均有对应运行时适配器注册(FR-21)。
|
||
- [ ] 声明 `lifecycle: true` 时已注册 `LifecycleAdapter`,声明 `supports_qr_login: true` 时已注册 `LoginAdapter`(见 §1.6)。
|
||
- [ ] 注册 `ProbeableAdapter` 时已实现 `async def probe(self) -> AdapterProbeOutcome`(从 `yuxi.channels.contract.plugin.adapters` 导入,FR-35)。
|
||
- [ ] 适配器只做协议转换与错误翻译,不承载业务规则(INV-8)。
|
||
- [ ] 原生异常已转换为 `yuxi.channels.contract.errors.*` 统一异常。
|
||
- [ ] 未用 try/except 静默吞错(INV-7);观测层降级必须有 `LoggerPort` warning。
|
||
|
||
### 6.4 生命周期
|
||
|
||
- [ ] 通过 `host.registerLifecycleHandler(handler)` 注册了 `LifecycleHookHandler`(如需响应生命周期)。
|
||
- [ ] 未在 `started` 之前对外提供服务。
|
||
- [ ] `stopped` → `unloaded` 干净退出,无遗留线程 / 连接 / 临时资源。
|
||
- [ ] `onInit` / `onStart` 在 60s 内完成,`onPause` / `onResume` / `onStop` / `onUnload` / `onFail` 在 30s 内完成。
|
||
- [ ] `onReconfigure`(如实现)支持配置回滚(FR-37)。
|
||
- [ ] `onFail`(如实现)不抛异常,失败清理逻辑不得阻断降级流程。
|
||
|
||
---
|
||
|
||
## 7. 参考
|
||
|
||
**契约层源码**:
|
||
|
||
- [entry.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/entry.py) — `CHANNEL_ENTRY` / `PluginHost`
|
||
- [manifest.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/manifest.py) — `PluginManifest` / `ChannelManifest` / `ResourceQuota` / `FailurePolicy`
|
||
- [capability.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/capability.py) — `CapabilityDeclaration` / `CapabilityProof` / `CapabilityProver`
|
||
- [lifecycle.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/lifecycle.py) — `LifecycleState` / `LifecycleHook` / `LifecycleHookHandler`
|
||
- [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` + `AttachmentUploadable` + `WebhookTestable`;已删除 `MediaAdapter` / `OAuthAdapter` 孤儿协议)
|
||
- [contract/plugin/\_\_init\_\_.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/contract/plugin/__init__.py) — 契约层导出清单(entry/manifest/capability/lifecycle/extension_point 全量 + 适配器 23 个全量导出)
|
||
|
||
**应用层实现**(仅供理解宿主行为,插件 **不得** import):
|
||
|
||
- [plugin_host_impl.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_host_impl.py) — `PluginHost` 实现,含 `_ADAPTER_SETTERS` 分发表与 `_checkPortAccess` 校验
|
||
- [plugin_lifecycle_manager.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_lifecycle_manager.py) — 生命周期编排,含 `discover` / `load` / `reload` / `reloadLoop`
|
||
- [plugin_state_machine.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_state_machine.py) — 状态转移表与钩子超时约束
|
||
- [plugin_capability_checker.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_capability_checker.py) — 加载期能力边界校验与契约一致性校验
|
||
- [plugin_loader.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/lifecycle/plugin_loader.py) — `importlib` 动态导入与 `CHANNEL_ENTRY` 调用
|
||
- [channel_probe.py](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/application/health/channel_probe.py) — `ChannelProbe` 主动探测器实现(FR-35;`ProbeableAdapter` Protocol 已提升到契约层)
|
||
|
||
**架构规范**:
|
||
|
||
- [演进式六边形-管道-插件架构规范.md](file:///d:/ForcePilot-v1.1/docs/vibe/v1.0/规范文档/演进式六边形-管道-插件架构规范.md) — §5 不变量(INV-1 / INV-6 / INV-7 / INV-8)、§9 插件规范
|
||
- [外部系统渠道开发规范.md](file:///d:/ForcePilot-v1.1/docs/vibe/v1.0/规范文档/外部系统渠道开发规范.md) — 同源参考实现(协议适配器 / 认证插件 / 厂商集成包)
|