首页 产品 为什么选 OCTO 解决方案 文档 关于
文档中心 / API 与集成 / 卡片开发指南
← 返回文档中心

卡片开发指南

OCTO 文档中心 · API 与集成

  • 04-api-integration/消息类型与发送-v1.0(Q9源码确认)
  • 04-api-integration/Bot-API错误码与排障-v1.0(Q13+A类补证)
  • 客户案例2/5卡片模板实践

OCTO 卡片开发指南 v1.0

定位:OCTO 互动卡片(type=17)一站式开发者指南,合并《消息类型与发送 v1.0》(Q9)、《Bot-API错误码与排障-v1.0》(Q13+A类)、案例 2/5 卡片实践、《卡片能力正式口径》四源卡片知识。🔴 硬口径红线;⚠️ 待运行时确认。limits 以运行时服务端 manifest 为准(R8),本文数值为当前版本源码确认参考值。配套:《Bot-API快速入门 v1.0》《Webhook与事件订阅 v1.0》。


1. 卡片消息概述

1.1 type=17 是什么

能力 说明
富展示 文本块、富文本、图片、表格、键值对、分栏布局
本地动作 OpenUrl / 复制剪贴板 / 切换可见性(不回服务端)
交互输入 文本/下拉/开关/数字/日期/时间(octo/v2 专属)
服务端回调 Action.Submit → card_action 事件回传 Bot(v2 专属)
动态改卡 Bot 编辑已发卡片(状态推进、锁定防重复)
  1. 🔴 只有 Bot 可发送 type=17——普通用户 API 绝对拒绝卡片 payload(`IsCardPayload → ErrMessageCardSendForbidden`),无例外。
  2. 🔴 Token 前缀:User Bot `bf_`(卡片开发常规选择);App Bot `app_` 仅支持 DM(群端点 403 `app_bot_dm_only`)且须已发布(否则 403 `bot_unavailable`)。
  3. Bot 只能向已加入的会话发消息(未加群 → 403 `not_group_member`)。

1.2 能力协商与降级机制

GET /v1/bot/card/profile 拉取 manifest(该 Bot 可用 profiles / limits / card_version)
  → POST /v1/bot/sendMessage(payload.type=17,发送入口内联校验:白名单/大小/深度/版本)
  → 客户端按自身支持的 profile 渲染
      ├─ 支持 octo/v1 / octo/v2 → 渲染卡片;└─ 不支持 → 自动降级纯文本,不报错、不丢消息
  • 不支持时自动降级纯文本(G10)——老客户端看到文本是预期行为不是 bug;勿假设所有接收方都看到卡片效果。
  • 🔴 本地 OpenClaw 配置键(cardDisplay/cardInteraction/cardProgress/reasoningCardTemplateMode)已废弃/运行时忽略;能力只由服务端 per-Bot 策略决定。
  • 服务端改 profile 开关后通常下一轮生效(短期缓存),不承诺即时生效。

1.3 客户端渲染差异(务必实测)


2. 卡片规格硬限制

2.1 硬限制总表(✅ 源码确认)

限制项 阈值 超限错误/行为 说明
payload 整包大小 ≤ 512 KiB(524288 字节) 400 ErrCardTooLarge 卡片 JSON 序列化后总大小
body 大小 ≤ 2 MiB 校验失败拒绝 卡片 body 部分
节点数量 ≤ 200 400 ErrCardTooManyNodes 所有元素节点总数
嵌套深度 ≤ 16 校验失败拒绝 元素嵌套层数上限
card_version 必须为字符串 "1.5" 400 ErrCardProfileUnsupported payload 与 card 两层均须 "1.5"
元素/动作白名单 仅白名单内元素 校验失败拒绝 详见 §3.2
URL 格式 必须合法 校验失败拒绝 Action.OpenUrl 等携带的 URL
profile octo/v1 或 octo/v2 拒绝/降级 v1 卡片含 v2 专属元素会被拒

🔴 R8 红线:上表为当前版本(1.5.0,2026-09-15 插件)源码确认参考值;limits 以运行时 manifest 下发为准,不承诺固定数值——写入代码/合同/SLA 前以 `GET /v1/bot/card/profile` 实测为准。本文模板均远低于限额。

2.2 发送前自检

cat card.json | wc -c                 # 1. 整包大小 < 524288(512 KiB)
grep -n '"card_version"' card.json    # 2. 两处版本都要 "1.5" 字符串
grep -o '"type"' card.json | wc -l    # 3. 节点粗算 ≤ 200

2.3 无独立校验端点(发送即校验)

  • 无法"只校验不发送"——验证格式只能真实发送测试消息;
  • 调试:先发最简测试卡(§9.3)→ 逐步加元素定位问题;
  • 测试卡发 Bot 自己的测试 DM(channel_type=1),不扰业务群。

3. Profile 体系

3.1 两档 Profile

Profile 定位 可用元素 交互方式
octo/v1 纯展示型卡片 8 种展示元素 + 3 种本地动作 仅本地动作(OpenUrl/ToggleVisibility/CopyToClipboard),不产生服务端回调
octo/v2 交互型卡片 v1 全部 + 6 种输入元素 + Action.Submit 输入提交/按钮点击 → card_action 事件回传 Bot

3.2 元素白名单对照表(✅ 源码确认 whitelist.go)

元素 用途 常用属性
TextBlock 文本块 text / size / weight / isSubtle / wrap
RichTextBlock 富文本块(行内混排) paragraphs / inlines
Image 单张图片 url / altText / size
ImageSet 图片集 images / imageSize
Container 容器(纵向收纳+样式) items / style
ColumnSet 多列布局 columns(内含 Column/items)
FactSet 键值对事实列表(最高频元素) facts: [{title, value}]
Table 表格 rows / columns / firstRowAsHeader
元素 用途 常用属性
Input.Text 文本输入框 id / label / placeholder / isMultiline / maxLength
Input.Toggle 开关/勾选确认 id / title / value / valueOn / valueOff
Input.ChoiceSet 下拉(compact)/单选(expanded)/多选 id / label / style / value / choices: [{title, value}]
Input.Number 数字输入 id / label / min / max / value
Input.Date 日期选择 id / label / value
Input.Time 时间选择 id / label / value
动作 v1 v2 行为
Action.OpenUrl ✅ ✅ 本地打开 URL(不触发回调)
Action.ToggleVisibility ✅ ✅ 本地切换元素可见性(不触发回调)
Action.CopyToClipboard ✅ ✅ 本地复制文本(不触发回调)
Action.Submit ❌ ✅ 提交到服务端,产生 card_action 事件
  • Action.Execute——不支持;
  • auto-refresh(自动刷新)——不支持,需要刷新状态请用动态改卡(§6)。

注:对外统一口径「8 展示 + 6 输入 + 4 动作」;`ActionSet`(body 内动作按钮组,同属白名单)计入动作类,上表未重复列出。

⚠️ 属性级校验以运行时 manifest 为准;本文模板属性为案例 2/5 已验证组合,可直接复用。


4. 卡片 JSON 结构详解

4.1 发送请求信封

POST /v1/bot/sendMessage
{
  "channel_id":   "<目标会话ID>",          // 必填:群号 / DM UID / Thread ID
  "channel_type": ,                // 必填:1=DM(Person) 2=群 5=Thread
  "payload": {                            // 必填
    "type": 17,                           // 必填:固定 17
    "profile": "octo/v2",                 // 必填:octo/v1 或 octo/v2;服务端最终覆盖写入
    "card_version": "1.5",                // 必填:字符串 "1.5"(int 或其他值→拒绝)
    "card": { ...AdaptiveCard 内容... }    // 必填:卡片本体
  },
  "client_msg_no": "<唯一编号>"            // 强烈建议:幂等去重 / 业务对账
}

4.2 卡片本体层级(card 对象)

{
  "type": "AdaptiveCard",
  "version": "1.5",
  "body": [
    { "type": "TextBlock", "text": "标题", "size": "medium", "weight": "bolder" },
    { "type": "FactSet", "facts": [{ "title": "键", "value": "值" }] }
  ],
  "actions": [
    { "type": "Action.Submit", "title": "提交", "data": { "action": "xx" } }
  ]
}
层级 字段 说明
card type/version 固定 "AdaptiveCard";version 必须 "1.5"(与外层一致)
card body 元素数组:展示元素 +(v2)输入元素,自上而下渲染
card actions 动作数组:4 种动作,渲染为底部按钮区
输入元素 id 回调 inputs 的键名,必须唯一且有语义
Action.Submit data 回调时原样回传,放 action(路由键)+ 业务 ID
Action.OpenUrl url 合法 URL,需过格式校验
  1. 标题层:首元素 TextBlock(medium/bolder)+ 次元素 isSubtle 说明——全部案例模板统一模式。
  2. 信息层:结构化信息用 FactSet,比长段落更易读、移动端更稳。
  3. 输入层(v2):Input 必须有语义化唯一 `id`;ChoiceSet 用 `style: "expanded"`(单选竖排)或 `"compact"`(下拉)。
  4. 动作层:确认/取消用两个 Action.Submit 以 `data.action` 区分路由;本地动作(copy/link)不触发回调,业务逻辑勿挂其上。
  5. `client_msg_no` 放业务单号(工单号/会话号),落库后与 card_action 的 message_id 对账。

4.3 最小可发送卡片(连通性测试用)

{
  "channel_id": "",
  "channel_type": 1,
  "payload": {
    "type": 17,
    "profile": "octo/v1",
    "card_version": "1.5",
    "card": {
      "type": "AdaptiveCard",
      "version": "1.5",
      "body": [
        { "type": "TextBlock", "text": "测试卡片:连通性 OK", "size": "medium", "weight": "bolder" }
      ]
    }
  },
  "client_msg_no": "smoke-card-001"
}

5. 五个实战模板(完整 JSON 可直接用)

均为 `POST /v1/bot/sendMessage` 完整请求体,替换 `channel_id` 与占位数据即可发送(建议先开发环境验证)。来源:案例 2(金融)/案例 5(医疗)已验证实践。

5.1 通知公告卡(octo/v1 纯展示:TextBlock + FactSet + 本地动作)

{
  "channel_id": "<目标群会话ID>",
  "channel_type": 2,
  "payload": {
    "type": 17,
    "profile": "octo/v1",
    "card_version": "1.5",
    "card": {
      "type": "AdaptiveCard",
      "version": "1.5",
      "body": [
        {
          "type": "TextBlock",
          "text": "📢 系统维护通知",
          "size": "medium",
          "weight": "bolder"
        },
        {
          "type": "TextBlock",
          "text": "核心业务系统将于本周末升级维护,期间相关服务暂停访问。维护结束后另行通知。",
          "isSubtle": true,
          "wrap": true
        },
        {
          "type": "FactSet",
          "facts": [
            { "title": "维护窗口", "value": "2026-09-26 02:00 ~ 06:00" },
            { "title": "影响范围", "value": "报销系统、审批中心" },
            { "title": "发布单位", "value": "信息技术部" },
            { "title": "公告编号", "value": "NOTICE-2026-0926-01" }
          ]
        },
        {
          "type": "TextBlock",
          "text": "紧急事项请联系值班热线。详情以门户公告为准。",
          "isSubtle": true,
          "wrap": true
        }
      ],
      "actions": [
        {
          "type": "Action.OpenUrl",
          "title": "查看维护详情",
          "url": "https://portal.example.com/notice/NOTICE-2026-0926-01"
        },
        {
          "type": "Action.CopyToClipboard",
          "title": "复制公告编号",
          "text": "NOTICE-2026-0926-01"
        }
      ]
    }
  },
  "client_msg_no": "notice-2026-0926-01"
}

5.2 信息确认卡(octo/v2 交互:FactSet + 确认/取消 Action.Submit)

{
  "channel_id": "<用户DM会话ID>",
  "channel_type": 1,
  "payload": {
    "type": 17,
    "profile": "octo/v2",
    "card_version": "1.5",
    "card": {
      "type": "AdaptiveCard",
      "version": "1.5",
      "body": [
        {
          "type": "TextBlock",
          "text": "✅ 申请信息确认",
          "size": "medium",
          "weight": "bolder"
        },
        {
          "type": "TextBlock",
          "text": "请核对以下受理信息,确认无误后工单进入处理流程。",
          "isSubtle": true,
          "wrap": true
        },
        {
          "type": "FactSet",
          "facts": [
            { "title": "申请编号", "value": "GD-20260922-00315" },
            { "title": "业务类型", "value": "设备报修" },
            { "title": "受理组", "value": "运维二组" },
            { "title": "提交时间", "value": "2026-09-22 15:20" },
            { "title": "生效方式", "value": "确认后即时受理" }
          ]
        },
        {
          "type": "TextBlock",
          "text": "提示:确认后本卡片将锁定。如需变更,请联系在线客服。",
          "isSubtle": true,
          "wrap": true
        }
      ],
      "actions": [
        {
          "type": "Action.Submit",
          "title": "确认无误",
          "data": { "action": "confirm", "ref_id": "GD-20260922-00315" }
        },
        {
          "type": "Action.Submit",
          "title": "取消申请",
          "data": { "action": "cancel", "ref_id": "GD-20260922-00315" }
        }
      ]
    }
  },
  "client_msg_no": "confirm-GD-20260922-00315"
}

5.3 数据采集表单卡(octo/v2 交互:Input.ChoiceSet + Input.Text + Input.Toggle + Submit)

{
  "channel_id": "<用户DM会话ID>",
  "channel_type": 1,
  "payload": {
    "type": 17,
    "profile": "octo/v2",
    "card_version": "1.5",
    "card": {
      "type": "AdaptiveCard",
      "version": "1.5",
      "body": [
        {
          "type": "TextBlock",
          "text": "📋 设备报修登记",
          "size": "medium",
          "weight": "bolder"
        },
        {
          "type": "TextBlock",
          "text": "请填写以下信息提交报修申请。提交后由运维组跟进处理。",
          "isSubtle": true,
          "wrap": true
        },
        {
          "type": "FactSet",
          "facts": [
            { "title": "申请人", "value": "张*" },
            { "title": "所属部门", "value": "市场部" }
          ]
        },
        {
          "type": "Input.ChoiceSet",
          "id": "device_type",
          "label": "报修设备类型",
          "style": "compact",
          "value": "laptop",
          "choices": [
            { "title": "笔记本电脑", "value": "laptop" },
            { "title": "台式工作站", "value": "workstation" },
            { "title": "打印机/外设", "value": "peripheral" },
            { "title": "会议室设备", "value": "meeting_room" }
          ]
        },
        {
          "type": "Input.Text",
          "id": "device_sn",
          "label": "设备编号",
          "placeholder": "请输入设备资产编号",
          "maxLength": 32
        },
        {
          "type": "Input.Text",
          "id": "fault_desc",
          "label": "故障描述",
          "placeholder": "简述故障现象(200字以内)",
          "isMultiline": true,
          "maxLength": 200
        },
        {
          "type": "Input.Text",
          "id": "contact_phone",
          "label": "联系电话",
          "placeholder": "用于工程师联系",
          "maxLength": 20
        },
        {
          "type": "Input.Toggle",
          "id": "agree_notice",
          "title": "我已阅读并同意《报修服务须知》",
          "value": "false",
          "valueOn": "true",
          "valueOff": "false"
        }
      ],
      "actions": [
        {
          "type": "Action.Submit",
          "title": "提交申请",
          "data": { "action": "repair_submit", "form_version": "1.0" }
        },
        {
          "type": "Action.OpenUrl",
          "title": "查看服务须知",
          "url": "https://help.example.com/repair-notice"
        }
      ]
    }
  },
  "client_msg_no": "repair-apply-20260922-0007"
}

5.4 进度状态卡(动态改卡场景 + card_seq CAS)

{
  "channel_id": "<用户DM会话ID>",
  "channel_type": 1,
  "payload": {
    "type": 17,
    "profile": "octo/v2",
    "card_version": "1.5",
    "card": {
      "type": "AdaptiveCard",
      "version": "1.5",
      "body": [
        {
          "type": "TextBlock",
          "text": "⏳ 工单处理进度",
          "size": "medium",
          "weight": "bolder"
        },
        {
          "type": "FactSet",
          "facts": [
            { "title": "工单号", "value": "GD-20260922-00315" },
            { "title": "当前阶段", "value": "① 已提交" },
            { "title": "处理组", "value": "运维二组" },
            { "title": "预计完成", "value": "2026-09-23 18:00" }
          ]
        },
        {
          "type": "TextBlock",
          "text": "流程:已提交 → 处理中 → 已办结",
          "isSubtle": true,
          "wrap": true
        }
      ],
      "actions": [
        {
          "type": "Action.Submit",
          "title": "催办",
          "data": { "action": "urge", "ticket_id": "GD-20260922-00315" }
        }
      ]
    }
  },
  "client_msg_no": "progress-GD-20260922-00315"
}
  • 中间态:“当前阶段”改为 "② 处理中(运维二组接单)";
  • 终态:标题改 "✅ 工单已办结",“当前阶段”改 "③ 已办结(2026-09-23 14:05)",actions 置空,body 末尾追加锁定说明:
{
  "type": "TextBlock",
  "text": "本工单已办结,卡片已锁定。如有新需求请提交新申请。",
  "isSubtle": true,
  "wrap": true
}

5.5 满意度评价卡(octo/v2 交互:Input.ChoiceSet expanded + 提交)

{
  "channel_id": "<用户DM会话ID>",
  "channel_type": 1,
  "payload": {
    "type": 17,
    "profile": "octo/v2",
    "card_version": "1.5",
    "card": {
      "type": "AdaptiveCard",
      "version": "1.5",
      "body": [
        {
          "type": "TextBlock",
          "text": "📝 服务满意度评价",
          "size": "medium",
          "weight": "bolder"
        },
        {
          "type": "TextBlock",
          "text": "本次服务是否解决了您的问题?您的反馈将帮助我们改进服务。",
          "isSubtle": true,
          "wrap": true
        },
        {
          "type": "Input.ChoiceSet",
          "id": "csat_score",
          "label": "满意度评分",
          "style": "expanded",
          "value": "5",
          "choices": [
            { "title": "非常满意", "value": "5" },
            { "title": "满意", "value": "4" },
            { "title": "一般", "value": "3" },
            { "title": "不满意", "value": "2" }
          ]
        },
        {
          "type": "Input.Text",
          "id": "csat_comment",
          "label": "其他意见(选填)",
          "placeholder": "请输入您的建议(100字以内)",
          "isMultiline": true,
          "maxLength": 100
        }
      ],
      "actions": [
        {
          "type": "Action.Submit",
          "title": "提交评价",
          "data": {
            "action": "csat_submit",
            "session_id": "CS-20260922-0001"
          }
        }
      ]
    }
  },
  "client_msg_no": "csat-CS-20260922-0001"
}

6. 动态改卡(POST /v1/bot/message/edit + card_seq CAS)

6.1 机制概述(✅ 源码确认)

项目 值 确认状态
端点 POST /v1/bot/message/edit ✅ 路由+用途确认
并发控制 octo_message_card_revision 表 + card_seq CAS(Compare-And-Swap)乐观锁 ✅ 源码确认
修订清理 POST /v1/bot/message/card/revisions/clear(清除卡片修订墓碑) ✅ 路由确认
权限边界 只能编辑 Bot 自己发的消息(否则 403 message_edit_forbidden) ✅ 确认
一致性语义 多次并发编辑不冲突,card_seq 保证最终一致 ✅ 确认
请求体字段 ⚠️ 字段级细节待补(以运行时实测为准) ⚠️ 待确认

6.2 card_seq CAS 乐观锁

读当前 card_seq = N → 提交编辑(expected_seq = N + 新卡片内容)
  ├─ 服务端 seq 仍 = N → 成功,seq 递增为 N+1
  └─ 已变为 N+k(他人先改)→ CAS 失败 → 重读最新卡片 → 重试
  1. 无悲观锁——两人并发改同一张卡不会死锁,后提交者 CAS 失败重试即可;
  2. 最终一致——card_seq 单调递增,客户端最终看到同一最新版本;
  3. CAS 失败要重试——重读最新卡片→重建→再提交;
  4. 🔴 卡片不是权威状态源——业务真实状态以业务系统状态机为唯一权威(案例 2/5 一致实践);卡片是展示层,改卡失败不影响业务正确性。

6.3 防重复提交锁定模式(标准套路)

第一级(体验):动态改卡锁定——点击后立即改卡:actions 整体移除,body 追加
  状态 TextBlock(“✅ 已确认受理”/“已评价,感谢反馈”)→ 按钮消失无从再点
第二级(并发):card_seq CAS——两次并发点击的 card_action 都会到达,改卡只成功一笔
第三级(权威):业务状态机幂等——按 ticket_id/ref_id 状态机流转(已受理的工单
  再收到 confirm 直接忽略)+ event_id 去重(at-least-once 语义,见 §7.2)

### 6.4 改卡调用骨架(示意,字段名以运行时为准)

CAS 冲突 → 循环重试


> 🔴 edit 请求体**精确字段名待运行时确认**(端点与 CAS 机制已源码确认,字段细节在 open-questions 追踪);上线前实测一次真实改卡锁定字段名再固化代码。只能改 Bot 自己发的消息。

---

## 7. 卡片回调处理(card_action 事件)

### 7.1 事件结构(✅ 源码确认)

用户点击 v2 卡片的 Action.Submit 后,OCTO 产生 `card_action` 事件,经**HTTP 长轮询**(`POST /v1/bot/events`)投递给 Bot:

> ⚠️ `card_action` 字段 schema 长期稳定性待官方文档背书(C1);当前结构为源码+插件双源交叉确认,解析代码防御式容错。

### 7.2 接收框架(长轮询 + ACK)

🔴 **Bot 收事件是 HTTP 长轮询,不是 WebSocket,也不是入站 Webhook**——三条铁律:

- 方法必须 **POST**(GET 返回 404);
- `event_id` 断点参数为 **exclusive**(传上次收到的最后一个 ID);
- **at-least-once 投递**——必须按 event_id 去重幂等。

**顺序铁律:先持久化、再处理、最后 ACK**——崩溃恢复最多重放一次未 ACK 事件,不丢已落库事件。事件队列默认保留 **7 天**(TTL 随新事件写入刷新),超窗未消费事件用 `POST /v1/bot/messages/sync` 补拉。

### 7.3 card_action 处理器规范

服务端校验清单(每个 input 一项不漏):

| 校验项 | 说明 |
|--------|------|
| 枚举合法性 | ChoiceSet 值必须在你下发的 choices value 集合内 |
| 长度边界 | Text 按 maxLength 再校验(客户端限制可被绕过) |
| 格式校验 | 电话/编号/日期按业务正则 |
| 防注入 | 文本入库/转发前 sanitize |
| 必填完整性 | Toggle 未勾选、必填为空 → 拒绝并反馈 |
| 身份与权限 | operator_uid 是否有权操作(卡片可能被转发,点击者≠目标人) |
| 幂等 | ref_id 状态机 + event_id + client_token 三重去重 |

---

## 8. 能力探测(GET /v1/bot/card/profile)

### 8.1 manifest 机制

发送卡片前**建议先调 `GET /v1/bot/card/profile`**,返回 **manifest 是运行时能力的权威依据**:

- 服务端按 **per-Bot 策略**下发:`card_enabled`(总开关)、`display_enabled`/`interaction_enabled`/`reasoning_enabled`、`reasoning_template_ref`、`profiles`(是否含 octo/v1、octo/v2)、`card_version`、`limits`(max_payload_bytes/max_nodes/max_depth/max_input_text_bytes/max_inputs_bytes 等);
- 旧版服务端可能不下发该接口或字段不全——集成前确认服务端版本支持;
- profile 变更后通常**下一轮生效**(短期缓存),勿假设即时生效。

### 8.2 探测的工程用法

- 用运行时 limits 做发送前**本地预校验**(大小/节点数),避免 400 往返;
- v2 不在 profiles 里时**降级发纯文本**,不硬发;
- 探测结果做短 TTL 缓存(对齐"下一轮生效"节奏),勿每条消息都探测。

### 8.3 🔴 R8 红线(对外口径)

- ❌ **不得对客户承诺 limits 固定数值**(max_* 均以运行时 manifest 为准,不写进合同/SLA);
- ❌ `card_enabled`/`*_enabled` 等字段仅内部排障用,不写进客户承诺;
- ✅ 可以说:"卡片能力由服务端 per-Bot 策略决定,运行时自动协商,不支持时自动降级纯文本,不报错不丢消息。"

---

## 9. 常见开发错误与排障

### 9.1 错误响应统一格式

### 9.2 卡片错误速查表

| 现象 | 错误码(code) | 根因 | 解决 |
|------|---------------|------|------|
| 400 版本拒绝 | `ErrCardProfileUnsupported` | `card_version` 非 `"1.5"` 或 profile 不可用 | 两处版本都改 `"1.5"`;v1 卡去掉 Input/Submit |
| 400 超大 | `ErrCardTooLarge` | 序列化后 > 512 KiB | 拆卡;长文本改摘要+OpenUrl;超长内容改 type=8 文件 |
| 400 节点超限 | `ErrCardTooManyNodes` | 节点 > 200 | 精简元素;FactSet 替代多个 TextBlock;拆多条消息 |
| 400 深度校验失败 | request_invalid 类 | 嵌套 > 16 | 打平 Container/ColumnSet 嵌套 |
| 400(card+template) | 400 | 同时传 `card` 与 `template`(XOR,双传 400) | 二选一,raw 模式只传 card |
| 4xx(provenance) | 4xx | 伪造 `catalog_provenance`(loud 拒绝) | 删除该字段 |
| 用户 token 发卡被拒 | `ErrMessageCardSendForbidden` | 普通用户 API 绝对拒卡 | 用 Bot 身份(bf_/app_)经 Bot API 发送 |
| 403 编辑无权限 | `message_edit_forbidden` | 编辑非 Bot 自己发的消息 | 只编辑自己发的卡片;注意编辑时限 |
| 403 不在群里 | `not_group_member` | Bot 非目标群成员 | 先加 Bot 入群 |
| 403 未发布 | `bot_unavailable` | App Bot 草稿状态 | 发布后再用 |
| 403 仅私信 | `app_bot_dm_only` | App Bot 在群聊发消息 | App Bot 只能 DM;群场景换 User Bot |
| 429 限流 | (读响应头) | 触发限流桶 | 读 `Retry-After` 指数退避;错峰;卡片合并消息 |
| 401 鉴权失败 | `ErrBotAPIAuthFailed`(单一码防枚举) | 前缀错/无效/缺 Bearer | User Bot 用 `bf_`;检查 header |
| 只见纯文本不见卡 | —(非错误) | 客户端不支持该 profile,自动降级 | 预期行为;关键信息确保降级文本可读 |
| 点 Submit 没收到回调 | — | v1 误用 Submit 被拒;或轮询断/未 ACK | 确认 profile=octo/v2;查轮询 cursor 与网络 |
| 改卡没生效 | CAS 冲突 | card_seq 落后服务端 | 重读最新修订→重建→重试 |
| 事件 GET 404 | — | 长轮询只支持 POST | 改用 POST /v1/bot/events |

### 9.3 排障步骤(标准流程)

1. 先发最简测试卡(§4.3)确认连通性+鉴权+profile

2. 逐步加元素定位:TextBlock → FactSet → Input → Submit

3. 自检(§2.2):大小/节点;两处 “1.5”;v1 无 Input/Submit

4. 回调不通:确认长轮询 POST + event_id exclusive + ACK

5. 服务端 500:客户端日志脱敏,找运维查 octo-server 日志


### 9.4 高频踩坑 Top 6

1. **card_version 类型混淆**:写数字 `1` 或 `"1.0"` → `ErrCardProfileUnsupported`。必须字符串 `"1.5"`(payload 与 card 两处同步)。
2. **v1 里塞 Submit/输入**:v1 白名单不含 → 拒绝;交互需求一律 octo/v2。
3. **忘 Content-Type**:`application/json` 缺失 → 400 request_invalid。
4. **只在 Web 端测**:移动端渲染有限,上线前三端实测(§1.3)。
5. **把本地动作当回调**:OpenUrl/CopyToClipboard/ToggleVisibility **不触发 card_action**,业务逻辑勿挂其上。
6. **不处理 429**:批量发卡(日报广播)会触发限流,读 Retry-After 指数退避+错峰。

---

## 10. 最佳实践

### 10.1 降级设计(假设有人看不到卡片)

- **内容自包含**:降级纯文本后关键信息(编号/时间/联系方式)仍可读——核心事实进 FactSet 与首段说明,勿只放按钮/交互里;
- **能力探测前置**:v2 不可用时降级发文本(§8.2),不硬发;
- **演示即实测**:售前/交付演示在客户目标客户端实测渲染(G10),Web 效果 ≠ 移动端。

### 10.2 最小必要原则(隐私与安全)

来自案例 5 医疗场景,适用于所有敏感行业:

- **卡面只放脱敏摘要**:患者/客户信息只显床号/尾号/姓首字;结果/业务正文留源系统,卡面不放;
- **OpenUrl 跳源系统**:详情经链接跳转(内网可达、带权限页面),卡片本身不是数据载体;
- **输入最小化**:只采业务必需字段,敏感字段(证件号)后置到安全通道核验,卡片只收意愿信息;
- **回调全量留痕**:operator_uid + acted_at + inputs 落库,"谁在何时提交了什么"可回溯。

### 10.3 防重复提交落地清单(§6.3 三级防线)

- [ ] 点击后立即改卡锁定(actions → 状态 TextBlock)
- [ ] card_seq CAS 处理并发冲突(失败重试逻辑)
- [ ] 业务状态机幂等(ref_id 去重;已流转状态忽略重复 confirm)
- [ ] event_id 去重(at-least-once 语义)
- [ ] `client_token`(客户端幂等 token)参与去重(如事件携带)

### 10.4 URL 校验

- **发送前**:OpenUrl 的 url 须合法格式(服务端校验)且对**接收方网络可达**——金融/医疗内网部署只放内网地址,禁止外网链接进通知卡;
- **服务端**:回调带回的 URL 类数据按不可信输入校验,防 SSRF/钓鱼注入;
- 卡片内嵌文件/图片链接走预签名 URL 时,确认对象存储域名对客户端网络可达(R11 防火墙放行)。

### 10.5 其他工程纪律

- **无独立校验端点**:开发期建立"测试 DM + 最简卡 + 逐步加元素"调试流(§2.3/§9.3);
- **client_msg_no 填业务号**:落库后与 card_action.message_id 对账闭环;
- **限流友好**:一张日报卡合并 N 条信息;sendMessage 单目标单条,循环发送错峰+退避;
- **token 安全**:bf_ token = Bot 全部权限凭证,仅存服务端密管,泄露即 BotFather `/revoke`;
- **先落库再 ACK**:persist → handle → ack 顺序固定(§7.2);
- **卡片只是展示层**:🔴 权威状态永远在业务系统;改卡失败要有重试与告警,勿让卡片状态机替代业务状态机。

---

## 附录 A:一页速查

## 附录 B:相关文档

`04/消息类型与发送-v1.0`(Q9:信封/白名单/动态改卡);`04/Bot-API错误码与排障-v1.0`(Q13+A类:硬限/错误码/限流);`04/Webhook与事件订阅-v1.0`(card_action 结构);`04/Bot-API快速入门-v1.0`(鉴权/长轮询);`05/案例2-金融机构`(卡片规范+模板);`05/案例5-医疗集团`(最小必要隐私设计);`08/卡片能力正式口径-v1.0`(协商/降级/承诺边界);`00/open-questions.md`(待确认项)。

## 附录 C:待确认项(不阻塞开发,上线前实测)

| 编号 | 待确认项 | 影响 | 缓解 |
|------|---------|------|------|
| C1 | card_action schema 对外稳定性 | 回调解析代码 | 防御式解析;以运行时观测为准 |
| C2 | type=17 信封字段长期稳定性 | 信封构造 | 当前字段均源码确认;版本升级时回归测试 |
| — | message/edit 请求体字段名 | 改卡代码 | 开发环境实测一次真实改卡锁定字段(§6.4) |
| — | limits 数值随版本变化 | 预校验逻辑 | 启动时拉 manifest 动态读取,不硬编码 |
| — | 群聊不 @Bot 消息是否投递 Bot | 群内交互设计 | 群内交互一律 @Bot 或 DM |

---

## 版本记录

- **v1.0(2026-09-22)**:初版。合并《消息类型与发送》(Q9)、《Bot-API错误码与排障》(Q13+A类)、案例 2/5、《卡片能力正式口径》四源,成独立一站式卡片开发指南。10 章 + 一页速查 + 待确认清单;与源码确认规格(type=17 信封、双 profile、8+6+4 白名单、512KiB/200/16、“1.5” 版本、长轮询+ACK、card_seq CAS)100% 对齐。