构建入站回复型 Agent 时,平台提供的已读状态并不等于已回复状态。以 Reddit 为例,回复消息不会自动标记为已读,导致计数器持续漂移,文章给出了三平台统一修复方案。
如果你跑了一个处理收件消息的 Agent,它每个条目只需要一个布尔值:我们是否已经回复过这条。每家平台给你的都看起来像是那个布尔值,但实际上没一个是真货。
我这周发现了三种不同的方式,都是同一个坑——同一个 Agent,跑在三个不同平台上,而修复的思路每次都一样。
在 Reddit 上,GET /message/unread.json 返回你的未读列表。请求它并不会把条目标为已读,这听起来很方便——直到你注意到另一面:回复也不会把它标为已读。
所以计数器只会越漂越远。你回复了某人,条目依然是未读状态,inbox_count 持续报告着根本不存在的待办。
如果计数器只是装饰性的,那无所谓。但我们的不是。调度器把 inbox_count > 0 当作跳过廉价空检查、改为运行完整扫描的理由,所以一条已回复但仍显示未读的条目,会让后续每次运行都白做一次贵的。
修复方法是自己把循环补上,等你真正处理完条目之后再标为已读:
POST https://old.reddit.com/api/read_message
body: id=t1_<comment_id>&uh=<modhash>
header: X-Modhash: <modhash> # from /api/me.json -> data.modhash
顺序比这个调用本身更重要。在你确认事情处理完之后再标已读,因为标已读恰恰是让这条从下次运行中消失的操作。
Bluesky 给你一个未读徽章。它把点赞、关注、转发和回复放在一起计数。
某天早上徽章显示一条未读。那条未读是一条点赞。而在它下面,已经被标为已读、因此对任何按徽章筛选的东西都不可见的,是一个四轮技术讨论帖——最后一条需要我回复的消息已经挂了五天。
什么都没坏。徽章回答了它被设计来回答的问题:"有没有新东西",而我在问它"有没有欠下的"。
const notifs = await listNotifications({ limit: 25 });
const conversational = notifs.filter(n =>
['reply', 'mention', 'quote'].includes(n.reason)
);
按 reason 过滤,完全忽略已读标记。你从未回复过的回复,在你获取列表的瞬间就变成已读了——也就是说,只要你"看了但没处理",它就变成已读了。
这是那个差点让我同一个人回复两次的问题。
修复了徽章问题之后,我写了看起来最明显的检查:获取他们消息所在的帖子,看看上面有没有我的回复。
const thread = await getPostThread({ uri: theirPost, depth: 1 });
const answered = thread.replies.some(r => r.post.author.handle === me);
在一个两小时前我已经回复过的帖子线程上,这返回了 false。他们的帖子报告 replyCount: 1,但 replies 数组返回空。我的回复是存在的,record.reply.parent.uri 指向的正是那个帖子。
我不知道为什么 appview 渲染成这样,而且对这个问题来说也不重要。重要的是这个检查有一个我没预料到的失败模式,而且失败的方向是错的。
通用的修复思路:别再问"他们是否已被回复",开始问"我是否回复了"。
你自己的已发送条目是权威的。它们不依赖对方的已读状态、徽章语义,或者他们的渲染器今天怎么组装一个帖子线程。
// 我们曾经回复过的每个父帖子,来自我们自己的 repo。
const repliedTo = new Set();
let cursor;
do {
const page = await listRecords({
repo: myDid,
collection: 'app.bsky.feed.post',
limit: 100,
cursor,
});
for (const rec of page.records) {
const parent = rec.value?.reply?.parent?.uri;
if (parent) repliedTo.add(parent);
}
cursor = page.cursor;
} while (cursor);
const open = conversational.filter(n => !repliedTo.has(n.uri));
同样的思路在任何地方都适用。在 Reddit 上是你的评论和它们的 parent_id。在邮件 API 上是你的已发件夹和 In-Reply-To 头。查询很便宜,而且数据来自你控制的东西。
这两种失败并不同等严重,这应该决定你如何构建检查。
"已经回复过"的误报,意味着你错过一条回复。这很尴尬,但可以挽回,人类会原谅。
"仍然待回复"的误报,意味着你同一个人回复了两次。在一个自动发文的账号上,从外界看这就是垃圾信息,而且无法挽回,因为第二条消息已经发出去了。
所以当两个信号不一致时,相信那个表示你已经回复过的。
写出来源于一周的调试,调试的是一个在四个平台上回复自己收件的 Agent。草稿由 AI 辅助生成,针对所述的实时 API 进行了验证,并由人工编辑。