feat(send): 支持设置飞书单图渲染尺寸 - #1298
Conversation
|
感谢 PR!这个需求(截图默认全宽太大)确实是个真实痛点,代码里的向后兼容处理也很克制——不传参数时保持 Markdown 输出字节级不变、代码块/行内图/并排多图都不动,这几个边界判断都是对的。 不过我在本地实测时发现一个功能层面的阻断问题,想先同步给你,避免合入后才发现。 主要问题:
|
| 发送的写法 | 飞书存储的结果 |
|---|---|
mode: "small"(本 PR) |
{tag, img_key, alt, preview} — mode 字段消失 |
mode: "tiny"(本 PR) |
与 small 逐字节相同 |
scale_type: "crop_center", size: "small" |
两个字段都保留 |
其中 mode 与 scale_type+size 是放在同一张卡片、同一次请求里做的 A/B 对照,所以可以排除「回读时被归一化」这种解释。
为确认不是我的探针有问题,我又做了阳性对照:同样的 mode: "small" 放进 schema 1.0 卡片,回读时 "mode": "small" 是在的(飞书还自动补了 compact_width: false)。也就是说 1.0 保留、2.0 丢弃,字段确实是被 2.0 拒绝的。
更直接的证据是飞书自己的报错——用 2.0 卡片传历史字段 custom_width 时:
code=230099 ErrPath: ROOT -> body -> elements -> [5](tag: img)
ErrMsg: img mode is not supported
实际影响:用户传了 --image-mode small,CLI 校验通过、卡片发送成功、但图片尺寸没有任何变化,也没有任何报错提示。同时因为改动把独立单图从 Markdown 提升成了原生 img 元素(回读可见消息 content 从  变成 [图片 1],并多出 resources[]),相当于布局变了但尺寸没变。
另外补充一点语义上的差异:飞书的 small 是 40×40、tiny 是 16×16,属于方形裁剪(头像/图标档位),和 PR 描述里 SVG 示意的「等比缩小」不是一回事。
建议的修法
改用 2.0 的写法:
{ tag: 'img', img_key, alt, scale_type: 'crop_center', size: 'small', preview: true }我实测确认可用的 size 取值:stretch / large / medium / small / tiny。注意 crop_center / crop_top / fit_horizontal 属于 scale_type 而不是 size——目前的白名单把这两类值混在了同一个列表里,可以顺便拆开(这样也能支持 crop_top)。
两个实测踩到的坑,供参考:
- 自定义像素
size: "400px 267px"不可用:单独发送会 400 报img size is not allowed,混在多图卡片里则被静默丢弃; custom_width在 2.0 直接 400 报错。
所以如果目标是「截图不要占满屏幕」,crop_center + size 目前只能提供方形裁剪档位(最大 large 160×160),做不到等比缩小——截图按方形裁剪可能会丢掉内容。这块的产品语义可能需要你再确认一下预期效果,也许 large 档位够用,或者需要换个思路。
两个次要建议
buildImageCardElements在imageKeys.length === 0时提前 return(md-card.ts:1137),这条路径没有把imageMode传下去。实测正文直接写而不经过--images时,--image-mode会被静默忽略。修好主问题后这个不一致会变得可见。- 新增的
docs/assets/send-image-mode.svg目前全仓库没有任何引用,建议随主问题一起更正示意效果,或者考虑移除。
已跑过的验证
bun run build通过;bunx vitest run --project unit跑你列的 4 个测试文件:218/218 通过;加上card-builder.test.ts扩展回归:403/403 通过;- 反变异 8 枪:7 枪转红(说明测试确实承重,覆盖了 mode 传递、alt 保留、代码块围栏、向后兼容闸、4 空格缩进闸、img_key 白名单、CLI 参数校验)。
有 1 枪是绿的,顺便提一下:把 src/cli.ts:10042 处 buildImageCardElements(...) 的 imageMode 实参删掉(模拟「参数收了但没接到卡片上」),152 个测试全部通过。这一行正是真正交付功能的接线,目前没有测试覆盖。建议补一条断言,检查 cmdSend 产出的卡片 JSON 里 img 元素带有预期字段。
以上是自动评审的初步意见,可能有疏漏,最终以维护者审阅为准。如果你对 F1 的判断有不同看法,欢迎贴出你那边的实测结果一起对齐——我的验证是在 schema 2.0 卡片上做的,如果你的使用场景走了别的卡片路径,结论可能不同。
|
谢谢实测,确认需求是「完整截图等比缩小」,而非方形裁剪。因此这次修正没有使用
small 对应的核心布局如下(真实 img_key 由上传结果提供): {
"tag": "column_set",
"flex_mode": "none",
"horizontal_spacing": "0px",
"columns": [
{
"tag": "column", "width": "weighted", "weight": 1,
"elements": [{
"tag": "img", "img_key": "<实际图片 key>",
"alt": { "tag": "plain_text", "content": "截图" },
"scale_type": "fit_horizontal", "preview": true
}]
},
{ "tag": "column", "width": "weighted", "weight": 2, "elements": [] }
]
}接线测试已改为真实执行 验证:
影响面仍限于 send 参数、公共飞书卡片预处理的显式非默认模式;不修改 CLI 适配器、PTY/Tmux、会话路由或权限逻辑。macOS 上的 CLI 测试走无 daemon 的 riff env 会话,未做 Linux / 各后端的 live 验证。 方案依据官方 图片组件文档 的 fit_horizontal 完整展示语义,以及 分栏文档 的 weighted/none 布局语义。当前验证到最终发出的 JSON,尚未做飞书服务端回读或真实客户端视觉验证,不能据此声称已经通过 live 验收。 下一步建议用宽截图和长截图分别验证默认、small、tiny,核对无裁剪、窄屏比例和点击原图预览。 |
|
先说两点更正与致歉,再报一个新实测结果。 1. 我上一条推荐的
|
| 档位 | 发送的 weight | 服务端存储的 weight |
|---|---|---|
| large | 3 : 1 | 1 : 1 |
| medium | 1 : 1 | 1 : 1 |
| small | 1 : 2 | 1 : 1 |
| tiny | 1 : 3 | 1 : 1 |
四个档位在服务端全部塌成 1:1,也就是 large/small/tiny 会渲染成和 medium 完全一样的宽度(各占 1/2)。
排除了两个可能的干扰因素:
- 不是"空列"导致的:把第二列换成非空(放一段占位文本),
1:3依然被压成1:1; - 不是我的回读看不见列信息:同一次探针里三列
1:1:1回读为[1, 1, 1],列数和字段都如实返回。
我之前也单独试过其它列宽写法,结论一致:width: "weighted:1" / "weighted:3" 组合语法被压成 auto,width: "200px" 也被压成 auto。也就是说 column_set 目前只能等分,给不出任意比例。
顺带解释一个容易误判的现象:仓库 imageRowElement 生产上并排图是正常的,因为它本来就只写 weight: 1(等权),恰好落在唯一能存活的那一档上。
3. 建议的下一步
方向(column_set + scale_type: fit_horizontal 等比不裁)我完全认同,只是档位粒度需要按服务端实际能力重新定:
- 目前可靠的只有等分:两列 = 1/2,三列 = 1/3,四列 = 1/4(用多个空白占位列凑)。也就是档位只能是
1/N,做不到 3/4。 - 另一条候选是
custom_width(连续宽度,278–580,超出上界会被钳到 580;低于 278 也能存活但属未定义行为)。它是活字段,但服务端会自动补scale_type: crop_center, size: stretch,是否等比缩放尚未经视觉确认,需要肉眼验一次。
4. 关于验证方法的一个建议
你的接线测试改成真实执行 cmdSend 并在 HTTP adapter 层拦截、检查最终 JSON——这个改进很好,也确实堵住了我上次提的覆盖缺口(我复现过:删掉 imageMode 实参后 152 个测试全过)。
不过这次的问题正说明:校验"我们发出去的 JSON"无法发现服务端归一化。飞书会静默丢弃字段(2.0 里的 mode)、压平字段(列 weight)、也会自动补字段(裸 img 补 scale_type: crop_center, size: stretch),而这些都不会报错,发送一律返回成功。
所以对这类卡片布局改动,建议在 assert 发出的 JSON 之外,再补一次服务端回读比对:发一条真实消息后拉回存储的卡片 JSON,逐字段对比「发出去的」与「存下来的」。你提到的下一步(宽截图/长截图的视觉验证)也很有必要。
以上仍是自动评审的初步意见,最终以维护者审阅为准。上面的实测都是在 schema 2.0 卡片上做的,如果你那边回读结果不同,欢迎贴出来一起对齐。
|
补充两条实测结果,都带可直接落地的数据。 1. 好消息:
|
| 发送 | 服务端存储 |
|---|---|
| 2 个等权列 | 列数=2, weight=[1,1] ✅ |
| 3 个等权列 | 列数=3, weight=[1,1,1] ✅ |
| 4 个等权列 | 列数=4, weight=[1,1,1,1] ✅ |
列数和权重都原样保留——服务端只钳「非等权」的 weight,不钳「多个等权列」。所以档位改成用空白占位列凑等分是可行的:
- medium = 2 列 → 1/2(目前已生效)
- small = 3 列 → 1/3
- tiny = 4 列 → 1/4
- large = 3/4 做不到:等分只能给出
1/N。建议砍掉这一档,或改走custom_width(连续宽度 278–580,但服务端会补scale_type: crop_center,是否等比缩放需要肉眼确认一次)。
2. crop_center 模式与不传参数在服务端产出完全相同
回读对照(同一张卡、同一张图):
| 探针 | 服务端存储 |
|---|---|
裸 img(不发任何字段) |
{scale_type: "crop_center", size: "stretch"} |
--image-mode crop_center 发出的 |
{scale_type: "crop_center", size: "stretch"} —— 与上一行逐字节相同 |
对照:crop_center + size: large |
{scale_type: "crop_center", size: "large"} —— 不同 |
第三行是阳性对照,证明这个差异探得出来,不是回读看不见。
原因在 src/im/lark/md-card.ts:1014-1035:crop_center 不在 weights 表里(表里只有 large/medium/small/tiny),所以 ratio 为 undefined → 直接 return img,不套 column_set。而它发出的 crop_center + size: stretch 恰好就是服务端给裸 img 补的默认值。
需要注意的是,它不是完全无副作用:因为 imageMode !== 'fit_horizontal',这条路径仍会把图片从 Markdown 提升成原生 img 元素(消息 content 从  变成 [图片 1],并多出 resources[])——布局变了,尺寸没变。建议直接移除这个模式,或者明确它的预期语义后重新实现。
3. 小结
方向(column_set + scale_type: fit_horizontal 等比不裁)完全正确,mode → scale_type、早退分支补传参数、HTTP 层拦截真实 cmdSend 的接线测试都改得很好(我验证过:删掉 imageMode 实参后新测试会失败,缺口确实堵住了)。
需要处理的是三项:
- 档位改成等权列(
1/N),large砍掉或转custom_width; crop_center模式移除或重新定义;- 文档和 SVG 里的 4 档宽度随之更正——目前描述的 3/4、1/2、1/3、1/4 中只有 1/2 实际生效。
另外建议在现有出站 JSON 断言之外,补一次服务端回读比对:这次的两个问题都属于「发出去的 JSON 完全正确、服务端存下来的不一样」,出站断言在原理上覆盖不到。
以上是自动评审的初步意见,最终以维护者审阅为准。如果你那边回读结果与上面不同,欢迎贴出来一起对齐。
|
已按这轮回读结果修正:
验证:
影响面仍限于显式 image-mode 的单图布局、CLI 参数及文档。默认输出、多图并排、代码块和正文行内图片保持原有行为;未改变其它 CLI 适配器、后端或会话权限路径。 本次依据你提供的等权列服务端回读结果落实修正;我这边完成的是出站 JSON 与构建验证,尚未独立执行真实消息发送、服务端回读或客户端视觉验收,也未重启 live daemon。请继续用宽截图和长截图核对 medium/small/tiny 的区别、无裁剪及原图预览。 |
|
复审了 等权分栏方案已通过真实服务端回读验证我用这个分支的构建实际发送并回读飞书服务端存储的卡片 JSON:
三档的列数各不相同、weight 全部原样保留——压平问题确实解决了,三个档位会渲染出三种不同宽度。
回归面
反变异对新代码做了 4 处变异,全部被测试拦住:
后两条分别复现了这个 PR 已修的两个缺陷,都能被拦住,说明有实际的回归防护。 其他
唯一剩下的客户端视觉验收尚未做。服务端存储已确认正确,但宽截图放进 1/3 列后是否完整显示(不裁掉左右两侧),最终还是要在飞书客户端看一眼。按 以上是自动评审的初步意见,最终以维护者审阅为准。感谢几轮迭代,也感谢你没有照搬我第一轮那条错误的 |
botmux send --images /tmp/screenshot.png --image-mode small "截图"现在可将独立单图渲染为原生飞书img元素,并设置mode: small,避免截图默认全宽显示过大。支持fit_horizontal、crop_center、large、medium、small、tiny;非法值或缺少值均以 exit code 2 退出,帮助文本列出模式和默认值。当前 master 的单图实际通过 Markdown 渲染,
singleImgElement用于并排图片。为保持向后兼容,未传参数或显式指定fit_horizontal时保留原有 Markdown 输出;非默认模式才提升独立单图为原生图片元素。独占一行的占位符和末尾追加的图片均支持尺寸覆盖,保留 alt 和图片预览。正文行内图片、多图并排、代码块保持原有布局。影响面:修改
src/cli.ts的 send 参数解析和飞书图片卡片构建,以及src/im/lark/md-card.ts的公共卡片预处理。新参数默认保持旧行为,其余调用方不受影响;不修改 CLI 适配器、PTY/Tmux 后端、会话路由、adopt/restore、sandbox 或 workflow 权限逻辑。纯参数和卡片 JSON 处理不引入平台相关代码;本次在 macOS 验证,未运行 Linux 或各 CLI 的 live 会话验证。验证:
bun install --frozen-lockfile补齐 canonical checkout 缺少的 yaml 依赖,package.json 与 bun.lock 无改动。bun run build:通过,包括 TypeScript、脚本和 mock 类型检查、Dashboard 打包及资源审计。bun run test test/md-card.test.ts test/cli-send-image-mode.test.ts test/cli-send-dispatch.test.ts test/cli-arg-utils.test.ts:4 个文件、217 项测试全部通过。覆盖六种合法模式解析、非法值与缺值的退出码、help、small/tiny 等原生 img 输出、默认输出一致性、多图与代码块回归。git diff --check:通过。bun run switch:here && bun run daemon:restart,再分别发送默认、small 和 tiny 图片确认显示效果。尺寸示意(非飞书客户端截图,实际尺寸由飞书客户端决定):