ADR 0044:适配器能力画像——规范在 zhin,能力由适配器注入,功能按能力组合
状态:Proposed
关联:ADR 0041(段组装器、platform 段、场景事实)、ADR 0042(工具分档)、ADR 0038(liveDanmaku 能力,本 ADR 的先例)、ADR 0035(评论回复适配器契约)。
背景
理想的分工是:zhin 提供规范;适配器按规范注入能力;插件根据组合出来的能力完成实际功能。 平台差异应由适配器声明,而不是在 zhin 内部写 if (platform === 'xxx');这些差异最终决定 prompt 怎么写、agent 能做什么。
2026-10-29 扫描 main,核心层仍有按平台名写死的分支:
| 位置 | 硬编码 | 后果 |
|---|---|---|
basic/cli/.../agent/turn/request.ts | adapter !== 'icqq' 则不查群 / 成员资料 | 其他平台的场景行、消息头为空 |
agent/typing-indicator/adapter-integration.ts | 按 icqq / telegram / discord / kook / slack / qq / napcat / sandbox 列「正在输入」做法 | 新平台要改 agent 包 |
core/side-event/normalize.ts、types.ts | kook、slack、icqq、onebot、napcat 各自的通知映射 | 事件标准化在核心里 |
agent/media/media-publisher.ts | platform !== 'icqq' 才落盘大文件 | 投递约束写死在 agent |
ai/agent/tool-filter.ts | 查询含 "icqq" / "qq 点赞" 时筛特定工具 | 工具选择与平台名耦合 |
agent/security/exec-policy.ts | 命令名 icqq 特判 | 安全策略混入平台逻辑 |
cli/.../sandbox-policy.ts、console/page-renderer.ts | platform === 'sandbox' / localName === 'sandbox' | 沙箱平台权限与页面写死 |
已有的正确样板:AdapterDefinition.segments(消息段能力,含表情回应)、liveDanmaku(合流只看能力不看平台名)、endpoint-management 的可选方法(实现了才有)、插件 platform 段注入(ADR 0041 D)。
另外,平台能力随会话场景变化:同一平台在群聊和私聊能做的事不同;Bot 是管理员还是普通成员,能否禁言 / 踢人不同;被禁言、处在被动回复窗口外时能否主动发消息也不同。能力不能只是平台级的一张静态表。
Fin 2026-10-29 拍板:
- 能力分组按本 ADR §1。
- prompt 里的平台说明 自动生成 + 适配器自定义都支持。
- 协议相近的适配器(OneBot / NapCat / icqq)共享一个事件映射包。
- 能力要考虑不同会话场景下的差异。
决策
0. 原则
- 核心只认能力,不认平台名:zhin 的 core / agent / ai / cli 运行时代码里不出现平台名字面量;需要差异的地方一律读能力。
- 没声明 = 不支持:未声明的能力按「不支持」处理,功能自动降级(不发、不显示、不提供工具),而不是报错或猜。
- 能力分两层:静态画像(按会话类型声明,构建时可校验)+ 动态解析(按当前会话与事实收窄),动态层只能收窄、不能放宽静态层。
- 能力是事实,不是权限:能力描述平台能做什么;产品授权(谁可以让 Bot 做)仍由角色、硬闸、审批决定,二者取交集。
1. 能力目录(规范,zhin 提供类型)
AdapterCapabilityProfile,分七组。每组按会话类型(private / group / channel / thread,以及平台声明的其他类型)分别声明,缺省继承 default。
| 组 | 字段(示例) |
|---|---|
消息 messaging | 支持的消息段与降级(沿用 segments)、单条长度上限、mention(@ 人 / @ 全体)、quote(引用回复)、reaction(表情集合)、recall(可撤回时限、能否撤回他人)、edit、forward、单轮最大条数 |
会话场景 scene | 能否查群信息、能否查成员资料、平台角色词汇及到 owner / admin / member 的映射、禁言语义(时长 / 全员)、成员上限、是否有名片 / 头衔 |
动作 actions | Bot 能主动执行的操作,每项声明可用的会话类型、所需 Bot 角色、参数范围(见下表) |
事件 events | 会发出的标准事件名集合(member.joined、member.left、member.role_changed、member.muted、member.card_changed、message.recalled、message.reaction(收到表情回应)、poke(被戳 / 别人互戳)…);原始事件 → 标准事件的映射由适配器(或共享映射包,§3)负责 |
交互反馈 feedback | 「正在输入」/ 执行状态的方式(typing / reaction / status-message / none)及其参数 |
投递约束 delivery | 被动回复窗口(时长 / 条数)、主动发送是否允许、频率限制、媒体大小上限、是否需落盘后发送(spool)、需要的消息序号等 |
prompt 贡献 prompt | 自动生成规则的开关与适配器自定义文本(§4) |
动作清单(actions,每项形如 { scopes, requiresRole?, params? };未列出 = 不支持):
| 动作 | 说明 | 典型条件 / 参数 |
|---|---|---|
reaction | 对消息做表情回应 | 可用表情集合;能否回应自己 / 他人的消息;是否可取消 |
poke | 戳一戳 | 私聊 / 群可用性;能否戳 Bot 自己;频率 |
mute | 禁言成员 | 需 admin / owner;时长范围(最短 / 最长);不能禁言更高角色 |
unmute | 解除禁言 | 同上 |
muteAll | 全员禁言 / 解除 | 需 admin / owner |
kick | 踢出成员 | 需 admin / owner;能否同时拉黑 |
setCard / setTitle | 改群名片 / 头衔 | 头衔常需 owner;长度上限 |
recall | 撤回消息 | 自己的消息时限;撤回他人需 admin |
essence | 设 / 取消精华 | 需 admin |
announce | 发群公告 | 需 admin;长度上限 |
invite | 邀请入群 | 是否需要好友关系 |
quoteReply / forward | 引用回复、合并转发 | 转发条数上限 |
- 动作与产品授权取交集:能力说「平台允许 Bot 这么做」,硬闸 / 审批 / 角色说「谁可以让 Bot 这么做」(ADR 0036、0039 不变)。禁言、踢人等仍是危险动作,照旧过硬闸。
resolveCapabilities按当前事实收窄动作:Bot 不是管理员 →mute/kick/essence/announce关闭;目标角色更高 → 该目标不可禁言(在执行前复核);被禁言 → 仅保留不发消息的动作(如reaction视平台而定)。- 动作对应的执行端口沿用
endpoint-management/operations,声明了动作而未实现端口 → 装配期报错。 - 自动生成的平台说明只写对决策有用的动作(「可以戳一戳」「你是管理员,可禁言 1 分钟–30 天」「不能踢人」),不罗列全部。
liveDanmaku、评论回复(ADR 0035)等既有能力并入对应分组,保留原字段名作为别名一个版本。
2. 注入方式(适配器按规范实现)
defineAdapter({ ..., profile }):静态画像,模块加载时校验(未知字段、类型错误即拒绝装配,fail closed)。resolveCapabilities?(context):可选的动态解析,入参为当前会话(ConversationRef)与可信事实(ADR 0041 §6 的TurnContextState:Bot 角色、禁言、发送者角色等)及投递状态(被动窗口剩余等),返回对静态画像的收窄补丁(例:Bot 不是管理员 →actions.mute/actions.kick等关闭;被禁言 →delivery.send关闭;被动窗口外 →delivery.proactive关闭)。运行时校验返回值只收窄,放宽字段被丢弃并记警告。- 实际操作仍走现有端口:
operations、endpoint-management方法、send。画像声明「能做」,端口负责「去做」;声明了却未实现端口 → 装配期报错。 - 结果按回合计算一次,作为
TurnCapabilities挂到 Turn 上(与 ADR 0042 W2 的工具分档、ADR 0041 的场景事实同一处),随会话事实缓存(ADR 0041 二级缓存)一起失效。
3. 共享事件映射包
- 新包
@zhin.js/adapter-onebot-events(名称实现时定):OneBot v11 / NapCat / icqq 共用的原始通知 → 标准事件映射,从core/side-event/normalize.ts/types.ts迁出;icqq 的差异以小的覆盖表表达。 - KOOK、Slack 等各自的映射迁入各自适配器包。
core只保留标准事件名的定义与校验,不再出现任何平台映射表。
4. prompt 里的平台说明
platform 文本段(ADR 0041 §3)由两部分组成,按会话场景渲染:
- 自动生成:由本回合的
TurnCapabilities生成固定模板的短句,只写对行为有影响的项,例如「支持表情回应(👍 ❤️ 😂…)」「单条消息最多 2000 字」「不能 @ 全体」「当前不能撤回他人消息」「被动回复窗口剩 2 条」。模板在 zhin 内,措辞统一,可测试、可缓存(按场景类型 + 收窄结果取值,组合有限)。 - 适配器自定义:
profile.prompt.text(或按会话类型分别提供),作为同一platform段中的附加小节。适配器是已安装的代码,按插件同等信任处理;但第三方适配器的文本沿用 #726 的external_content标注,不能授予权限、不能覆盖安全规则。长度上限 600 字符,超出截断并告警。
- 缓存(ADR 0042 §1):两部分都只依赖「适配器 + 会话类型 + 收窄结果」,同类会话跨会话稳定;动态部分(窗口剩余条数等)放进
scene段,不进稳定前缀。
5. agent 与插件按能力组合
- 工具可见性:工具(及 skill)可声明
requiresCapabilities(如actions.mute、actions.poke、actions.recall.others);回合内能力不满足即不出现在工具列表(与 ADR 0042 W2 分档同一过滤点),不再靠平台名或关键字筛选(替换tool-filter.ts的 icqq 关键字)。 - 出站:渲染与投递按
messaging/delivery决定拆分、降级、落盘与被动窗口处理(替换media-publisher.ts的 icqq 判断)。 - 交互反馈:
feedback声明驱动「正在输入」(替换typing-indicator的平台表)。 - 场景事实:
scene声明决定是否查询群 / 成员资料(替换request.ts的adapter !== 'icqq')。 - 沙箱:
sandbox平台改为声明一个能力(如runtime.sandbox),策略读能力。 - 插件:通过
TurnCapabilities/ 端点能力查询 API 判断能否做某事,同样不写平台名。
6. 约束检查
新增 pnpm check:no-platform-branches(并入 check:all):在 packages/im/{core,agent,ai}/src、basic/cli/src/plugin-runtime 中禁止平台名字面量('icqq'、'qq'、'napcat'、'onebot'、'kook'、'slack'、'discord'、'telegram'、'sandbox'、'bilibili' 等,列表可扩展)作为比较对象;测试、类型声明中的示例、文档字符串除外;确需例外须写明理由并登记白名单。
7. 迁移
| 现状 | 迁移目标 |
|---|---|
request.ts icqq 场景事实 | scene.groupInfo / scene.memberProfile + 端口 |
| typing indicator 平台表 | feedback |
core/side-event 映射表 | §3 共享包 / 各适配器 |
media-publisher icqq 落盘 | delivery.spool |
tool-filter icqq 关键字 | requiresCapabilities |
exec-policy icqq 命令特判 | 移入 icqq 适配器的工具 / 命令声明 |
| sandbox 策略与页面 | runtime.sandbox 能力 |
icqq 先完整声明(它是当前主力),其余适配器按能力补齐;未补齐的能力按「不支持」降级。
8. 实现分工(多 agent 并行,最后一个 PR)
| 工作项 | 内容 |
|---|---|
| C1 规范 | AdapterCapabilityProfile 类型、校验、resolveCapabilities 收窄语义、TurnCapabilities 计算与缓存 |
| C2 prompt | §4 自动生成模板 + 自定义文本、platform / scene 段接线 |
| C3 消费方迁移 | §5:工具 requiresCapabilities、出站、交互反馈、场景事实、沙箱 |
| C4 事件映射包 | §3:共享包 + 各适配器映射迁出 core |
| C5 适配器声明 | icqq 完整画像(含全部动作:回应、戳一戳、禁言、踢人、名片 / 头衔、撤回、精华、公告、邀请);其余适配器的基础画像 |
| C6 守卫 | §6 检查脚本 + 白名单 |
C1 先行;C2–C5 依赖 C1 的类型;C6 最后(迁移完成后才能全绿)。
验收标准
- core / agent / ai / cli 运行时代码中无平台名分支,
check:no-platform-branches通过。 - icqq 群聊中,Bot 被撤管理员后,踢人 / 禁言 / 精华 / 公告工具从工具列表消失,prompt 平台说明同步变化;恢复后重新出现。戳一戳、表情回应在群聊与私聊按画像各自可用。
- 同一 icqq 实例,私聊与群聊的平台说明不同;同类会话跨会话逐字节相同。
- 一个只声明了
messaging基础能力的新适配器,不改 zhin 任何代码即可接入,不支持的功能自动降级。 - OneBot / NapCat / icqq 的进出群、改名片等事件经共享包得到相同的标准事件。
- 适配器自定义 prompt 文本出现在
platform段;第三方适配器的文本带external_content标注;超长被截断。
待 Fin 确认的默认值
- 共享映射包名:
@zhin.js/adapter-onebot-events。 - 适配器自定义 prompt 文本上限 600 字符。
resolveCapabilities只能收窄;放宽字段丢弃并告警。- 平台名白名单初始为空,例外须逐条登记。
非目标
- 跨适配器的统一账号 / 身份体系。
- 能力的远程动态下发(只在代码中声明)。
- 改变产品授权模型(角色、硬闸、审批不变,仅与能力取交集)。
结果
- 新平台只需实现适配器与能力画像即可接入,zhin 核心零改动。
- prompt 的平台说明与 agent 的可用工具随真实能力与会话场景自动变化。
- 平台差异集中在适配器,核心代码不再增长平台分支。