ARTICLE / 开源项目

补充飞书Markdown表格卡片渲染特性的研究

PR 地址NousResearch/hermes-agent#45386 · 飞书 Markdown 表格渲染为 Interactive Card + CuaDriver 后端缓存修复

研究摘要

agent 的输出里表格是高频形态——对比清单、参数汇总、方案选型,几乎每天都用。但在飞书平台上,这类回复此前会变成空白消息:包含 Markdown 表格的回复走 post 消息类型渲染,表格部分直接丢失,用户看到的是内容为空的回复,整块结构化信息无声蒸发。本研究向 Hermes 上游补充了飞书 Markdown 表格卡片渲染:检测到回复中的 Markdown 表格时,改用飞书 Interactive Card 渲染,以原生表格组件(飞书客户端 v7.4+ 支持)呈现行列结构。同一 PR 还修复了 CuaDriver 后端的缓存失效问题:后端首次启动失败(如缺少 mcp 二进制)后,失效实例被缓存复用,导致 computer use 功能静默坏死。改动规模 +533/-5,涉及 3 个文件,其中飞书表格测试套件 333 行。

一、问题背景:两块各自独立的功能缺口

1.1 飞书表格回复变成空白消息

飞书平台的回复走 post 消息类型,post 类型对 Markdown 表格的支持是缺失的。agent 回复一旦包含表格,渲染结果就是空白——不是报错,不是降级为纯文本,而是静默丢失。这类 bug 的破坏性在于无感知:用户以为回复内容为空,实际上内容在渲染层被吞掉了。

表格越是结构化、信息密度越高的场景,损失越大:方案对比表承载的是决策依据,参数清单承载的是配置要点,表格空白等于把整段关键信息从对话里抹去,且没有任何错误提示引导用户换一种问法。——空白回复甚至不会触发飞书的错误样式,用户在对话流里直接跳过,信息丢失是彻底的。对依赖飞书作为主要工作界面的用户,这直接削弱了 agent 输出结构化内容的可信度——输出得再好,到用户眼前也是空的。

表格丢失的触发面近乎全覆盖:agent 生成 Markdown 表格几乎是无条件的高频行为,只要回复里出现表格语法就命中空白路径;使用飞书作为网关的日常会话,碰到表格的概率极高。问题因此不是偶发瑕疵,而是结构化输出在飞书上的系统性失效。

1.2 CuaDriver 后端缓存的"死会话"问题

CuaDriver 是 Hermes computer use 的驱动后端。后端的获取走缓存:首次调用创建 _backend 并缓存。若首次启动失败(典型如 mcp 二进制缺失),_backend 仍被缓存为"已存在",后续 _get_backend() 返回的是这个带着死会话的过期实例,而不是重新创建——失败被永久固化,computer use 从此静默失效。

这类 bug 的隐蔽性极高:缓存命中路径一切正常,没有任何报错;日志里看不到异常,因为错误只发生在首次调用那个瞬间;排查者即使怀疑到后端,看到的也是"实例存在、接口可调",只是调用的结果永远是失败。失败被固化在缓存里,等于把一次性的环境问题升级成了永久的静默故障。

修复前后还有一个行为差异值得注意:修复前首次失败被永久固化,后续调用不再尝试;修复后每次失败都会重新走启动流程——若 mcp 二进制持续缺失,每次调用都会重复启动失败的完整开销,但失败至少是可见、可重试、可自愈的。用少量重复开销换取可诊断性,是这类修复的合理代价。这个 bug 与表格问题同属一类:失败没有声音

二、特性设计:检测、替换与缓存保鲜

2.1 表格检测与卡片渲染

gateway/platforms/feishu.py 中新增 Markdown 表格检测:回复内容包含表格结构时,不再走 post 消息类型,而是渲染为飞书 Interactive Card,使用飞书原生表格组件承载行列数据。检测放在消息类型选择这一环:有表格走卡片,无表格走原有路径,两条路径互不干扰。

卡片形态的优势在于:表格以组件形式存在,列对齐、表头、单元格边界都由平台原生渲染,而非依赖文本排版——文本表格在窄屏、多列场景下必然错位,组件则始终按规范布局;同时卡片支持后续的交互扩展(按钮、跳转等),为结构化输出留出了进化空间。

从消息类型的选择逻辑看,检测与替换是叠加而非覆盖:既有 post 类型路径原样保留,表格检测只在其命中时切换目标类型。这保证了非表格回复的行为完全不变,也降低了引入卡片渲染对既有消息流程的风险。渲染前置条件是飞书客户端 v7.4+,旧版本客户端不支持原生表格组件,需要降级策略兜底。

检测策略的一个关键取舍:判断发生在消息类型选择环节,一次性决定走卡片还是走原路径,不改变 agent 侧的输出格式——agent 仍然生成 Markdown,由网关负责适配呈现。这是内容生成与平台呈现解耦的体现:后续其他平台的表格适配(钉钉、Slack 等)可以复用同一思路,在各自的网关层做转换。

2.2 CuaDriver 缓存保鲜检查

修复的核心是一条前置检查:复用缓存前先验证 _session._started。缓存实例的会话必须处于已启动状态才可复用,否则视为失效、走重建路径。修复让"启动失败 → 缓存毒化 → 永久坏死"的链条在第一个环节就被截断:失败后的下一次调用会重新创建后端,失败是暂时的、可自愈的。这个修复的巧妙之处在于检查成本极低——一次布尔字段判定——却消除了整类"缓存了坏实例"问题。

缓存保鲜检查是一个通用模式:后端、连接池、会话管理器里普遍存在“缓存了坏实例”的隐患,_session._started 这类轻量前置判定成本极低,值得作为复用经验沉淀。

2.3 测试先行

PR 配套了完整的测试套件 tests/gateway/test_feishu_table_card.py(333 行),覆盖表格检测与卡片渲染的主要路径。两处修复都落在可单测的纯逻辑层——消息类型选择与缓存复用判定——具备可靠的回归保护。测试先行也意味着修复的可验证性没有被留在"人工看一遍"的层面。

三、实测结果

PR body 记录了以下验证结果:

验证项结果
表格卡片单元测试(test_feishu_table_card.py,333 行)通过
人工验证:飞书私聊中表格渲染为卡片通过

测试覆盖与人工验证的组合,确认了表格从"空白消息"到"原生卡片"的端到端行为变更:单元测试锁定检测与渲染逻辑,人工验证确认真实飞书环境下的呈现效果。CuaDriver 缓存修复未在 PR body 中记录独立测试数据,其正确性主要依赖修复逻辑本身与后续回归。

人工验证仅覆盖私聊(DM)一种场景:群聊、话题模式、多客户端版本的组合验证缺失;卡片在群聊中的呈现(是否与私聊一致、是否需要额外权限)留待真实环境补充。

四、能力边界

  • 卡片渲染要求飞书客户端 v7.4+,旧客户端会退回旧行为,表格仍可能不可见——降级路径的呈现效果未被 PR body 记录;
  • 检测逻辑只覆盖 Markdown 表格形态,其他 post 类型不支持的内容(复杂嵌套、混合布局)不在本次修复范围;
  • CuaDriver 修复只针对"会话未启动"这一类失效模式,后端崩溃、进程残留、权限吊销等其他异常路径未被 PR body 记录;
  • 表格卡片是渲染层替换,不改动 agent 生成表格的内容逻辑,超宽表格、超长单元格在卡片内的表现依赖平台组件自身行为;
  • 测试套件覆盖的是纯逻辑层,多客户端版本、多表格形态的组合验证仍依赖真实环境回归。
  • 卡片消息走新的发送通道,其限流、配额与 post 类型不同,批量表格场景下的行为未被记录;
  • 飞书客户端 v7.4+ 的前提意味着企业管控的旧版本客户端环境会长期停留在空白消息的旧行为;
  • 超长表格与超大单元格在卡片组件内的截断与滚动行为依赖平台实现,未被 PR body 记录;
  • CuaDriver 修复覆盖的启动失败场景以“缺少 mcp 二进制”为典型,其他导致会话未启动的成因(配置缺失、权限不足)是否同样被 _session._started 捕获未被记录。

五、PR 信息

  • PR 地址:https://github.com/NousResearch/hermes-agent/pull/45386
  • 改动规模:+533 / -5,3 个文件(gateway/platforms/feishu.py、tests/gateway/test_feishu_table_card.py、tools/computer_use/tool.py)
  • 状态:closed
  • 提交时间:2026-06-13

本文记录 x7peeps 向 Hermes Agent 上游贡献的特性研究,所有数据来自 PR 实测记录。