Conversation
一个群聊 bot 会见到很多贴纸,但看过就没了:贴纸只是某条消息的附件,没有任 何地方记下"这张是什么、以后还能不能再发"。想让 bot 主动发贴纸,缺的是一份 它自己的贴纸库。 库放在 bot 工作区里,不进 Postgres,跟 internal/memory/storefs 走同一套 bridge 写文件的机制: - /data/stickers/<包名>-<id>.md 一张贴纸一个文件,YAML frontmatter 记身份, 正文是描述 - /data/STICKERS.md 是按贴纸包分组的索引,每次保存重建 描述之所以是正文而不是 frontmatter 字段,是因为它是人和 agent 会直接改的那 部分——多行 prose 塞进 YAML 早晚会在引号上出事。手改过正文的文件仍然能被 读回来,前提只是 frontmatter 还在;读不动或解析不了的单个条目只跳过并告警, 不会连累整个库,因为这些文件本来就允许被手改坏。 身份上 ref(可发送的 file_id)和 unique_id(跨 bot 稳定但发不出去)分两列存: 只留前者,换 bot token 之后整库作废且无从合并;只留后者,一张也发不出去。 content_hash 一并记下,作为 file_id 失效时重传的落点——只对静态贴纸有效, Telegram 不接受重传的 .tgs 和 .webm。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
两件事让"再发一次刚才那张贴纸"发不出来: 一是身份不全。#1239 之后 sticker_file_id 收窄成"存进媒体库的字节不是贴纸本 身"的标记,这是对的,但除此之外附件上没有任何东西能认出这是一张贴纸、属于哪 个包、是哪一张。补上 sticker_unique_id(跨 bot 稳定,贴纸库据此认条目)、 sticker_set、sticker_kind,并把 #1239 删掉的 sticker_emoji 加回来——删它的理 由是"全仓库没有读者",现在 internal/sticker 把 emoji 作为搜索词和索引里的显 示项,读者是实打实的。可发送引用的取法随之确定:有 sticker_file_id 用它,否则 用附件自己的 file_id。 二是发送。贴纸的 file_id 走 sendPhoto 会被 Telegram 拒掉,所以新增 sticker 附 件类型走 sendSticker。它只在出站存在:入站仍然是 image,因为到达模型的是一张 能看的预览。sendSticker 不收 caption,配文另发一条,而不是静悄悄丢掉模型本来 要说的话。其它平台没有原生贴纸语义,准备阶段会以"attachment reference is required"失败,而不是退化成发一张静态图。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
贴纸库要长起来,得有人往里写:描述由 agent 自己写,它在对话里看到了那张贴纸, 知道它表达什么。save_sticker 省略 platform_key 时绑定本会话最近一张贴纸, search_stickers 按描述、emoji、标签、包名做与匹配,返回可以直接填进 send 的 platform_key。 发送刻意没有并进这两个工具:贴纸走现成的 send,就继续受同一套目标校验和投递 路径管辖,而不是另起一条只有贴纸能走的旁路。 "刚才那张贴纸"是从已有的消息附件里查出来的,没有为此新建任何表——贴纸库本身 在工作区,这条查询只回答"这个会话刚看到哪张贴纸"。它按 sticker_unique_id 过 滤而不是 sticker_file_id:后者只在预览替换发生时才写,用它会漏掉全部静态和动 画贴纸。 带 platform_key 时先查会话再查库:改写几周前存的描述是普通编辑,不该因为"最近 没见过"被拒;但两边都找不到的 key 会当场拒绝,存进去就是一个发出去必然失败的 引用。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
三个问题都在库自己这一层: **保存成功但搜不到。** ListDir 报的是相对于所列目录的名字,代码直接把它当路径 去读,于是读的是 /data/misc-xxx.md,而文件在 /data/stickers/ 下。读失败被当成 "这个条目坏了"跳过,所以搜索若无其事地返回空库——历史条目合并和索引重建也一 起失效了。internal/memory/storefs 每次读之前都过一遍 memoryEntryPath,这层我漏 掉了。现在列出的名字统一解析成库内路径,entry.Path 也据此填。 回归测试用 bufconn 起了一个按真实契约行为的 bridge 假实现(列表给相对名、写入 和带根读取用绝对路径),断言"保存之后搜得到"。纯函数测试挡不住这个 bug:写测试 的人会带着和实现同一个错误假设。 **文件名对身份做有损转换。** 原来把 unique id 小写、去符号、截前十位当文件名。 平台 id 是大小写敏感且含符号的,这么一转两张不同的贴纸可以得到同一个名字:查找 按完整身份判定为新条目,写入却覆盖了另一张贴纸的文件。改成对"平台+完整身份"取 摘要,并保留同名检查——手工建的同名文件也不该被直接盖掉。 **工作区读取没有边界。** 库目录 agent 和用户都能写。现在列表走 ListDirBounded、 读取走带根不跟随软链的接口,单文件、整库各有字节上限。超限的单个文件跳过(它本 来就不是条目),整库超预算则报错而不是少返回几条:静默截断的库和空库在调用方看 来没有区别,而这正是上面那个 bug 的形态。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
原来以为其它平台会在准备阶段以"attachment reference is required"失败,不成立: 搜索结果给的是裸 platform_key,而出站归一化会把空的 source_platform 补成目标 平台,于是一个 Telegram 的 file_id 到了飞书和钉钉就被当成它们自己的原生句柄 收下——飞书随后按 file_key 发出去,钉钉要到 adapter 才拒绝。 补一个 Stickers 能力位,在已有的能力校验里明确拒绝。它和 Media 分开:贴纸引用 和媒体不可互换,没有贴纸概念的平台会把这个引用当成自己的某种文件句柄,发出去 的是另一个东西。校验点在 prepareOutboundForSend,所有 adapter 的发送都经过它, 在任何原生引用解析之前就失败。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
搜索结果补上 source_platform。留空会被出站归一化补成目标平台,等于把一个平台 的句柄冒充成另一个平台自己的——能力检查是兜底,但引用本来就该带着它的来源走。 "最近一张贴纸"改成在工具装配时定下可见边界,而不是执行到那一刻再问数据库最新 的是哪张。群聊里其他人发的消息不触发 agent 也会立即持久化,原来的写法有一个真 实的窗口:模型正在描述 A,B 到达,保存把 A 的描述绑到了 B 上。查询因此多一个 visible_until 边界,而不是取回来再过滤——过滤会让 limit 提前被本轮看不见的行 吃掉。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Author
Type
Summary
一个群聊 bot 会见到很多贴纸,但看过就没了:贴纸只是某条消息的附件,没有任何地方记下"这张是什么、以后还能不能再发"。这个 PR 给 bot 一份自己的贴纸库,并补上把贴纸真的发出去的那条路。
库在工作区,不在 Postgres。 跟
internal/memory/storefs同一套 bridge 写文件机制:/data/stickers/<包名>-<id>.md一张贴纸一个文件,YAML frontmatter 记身份,正文是描述;/data/STICKERS.md是按贴纸包分组的索引。描述是正文而不是 frontmatter 字段,因为它是人和 agent 会直接改的那部分——多行 prose 塞进 YAML 早晚在引号上出事。手改过正文的文件仍然读得回来。身份分两列存。
ref是可发送的 file_id(绑 bot token),unique_id跨 bot 稳定但发不出去。只留前者,换 token 之后整库作废且无从合并;只留后者,一张也发不出去。发送需要一个新的附件类型。 贴纸的 file_id 走
sendPhoto会被 Telegram 拒掉,所以新增sticker类型走sendSticker。它只在出站存在——入站仍然是 image,因为到达模型的是一张能看的预览。sendSticker不收 caption,配文另发一条,而不是静悄悄丢掉模型本来要说的话。描述由 agent 自己写。
save_sticker省略platform_key时绑定本会话最近一张贴纸;search_stickers返回可以直接填进send的platform_key。发送刻意没有并进这两个工具:贴纸走现成的send,继续受同一套目标校验和投递路径管辖。与 #1239 的关系
#1239 把
sticker_file_id收窄成"存进媒体库的字节不是贴纸本身"的标记,并删掉了当时没有读者的sticker_emoji。这两条都保留:sticker_file_id用它,否则用附件自己的file_id。写反了的后果是把一张动图的静态截图发出去。sticker_unique_id过滤而不是sticker_file_id,后者会漏掉全部静态和动画贴纸。sticker_emoji加回来了,因为现在internal/sticker把它作为搜索词和索引显示项,读者是实打实的。没有做的
channel does not support stickers失败,不会退化成发一张静态图。content_hash记下来了但兜底重传还没实现;而且那条路只救静态贴纸,Telegram 不接受重传的.tgs和.webm。Related Issues
Review 后的修正
首轮 review 报了五个问题,都已修复并各自带回归测试:
ListDir给的是相对名,直接拿去读会读到/data/<name>,读失败被跳过,库表现为空entry.Path;回归测试用 bufconn 起了一个按真实契约行为的 bridge 假实现platform_key被归一化成目标平台的原生引用,飞书会按file_key发出去Stickers能力位,在prepareOutboundForSend的能力校验处拒绝;搜索结果同时带上source_platformvisible_until边界,可见范围在工具装配时定下io.ReadAllValidation
go build ./...、go test ./internal/... ./cmd/...全绿(在 rebase 到 fix(telegram): 动画贴纸保留原始 Lottie,渲染器才看得到它 #1239 之后)。golangci-lint run覆盖改动包无新增告警(internal/agent/application/service_run_lifecycle.go的 nilness 是既有的)。sendSticker与 caption 另发、准备阶段保持原生引用不重传。check-ui-contract.mjs没跑(worktree 里没有 node_modules、packages/ui子模块未拉取)。TS 改动是tool-call-registry.ts里两个数组项,三个 locale JSON 已验证可解析。Screenshots / Recordings
后端改动为主。前端只动了工具名的 i18n 文案和 tool-call 的分类/图标映射,没有新增界面,因此没有截图。
Human QA