From fc757cc04359e70a1209cef4ffd5536f80c0c31d Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 02:13:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B6=88=E6=81=AF=E5=8F=91?= =?UTF-8?q?=E9=80=81=E5=A4=B1=E8=B4=A5/=E6=8E=92=E9=98=9F=E4=B8=AD?= =?UTF-8?q?=E6=97=B6=E9=95=BF=E6=8C=89=E8=8F=9C=E5=8D=95=E4=BB=8D=E6=8F=90?= =?UTF-8?q?=E4=BE=9B"=E6=92=A4=E5=9B=9E"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 判断只查 params.seq > 0,未排除 tinode-sdk 本地占位 seq(LOCAL_SEQID, 268435455,同样是正数)——发送失败/仍在排队的消息会被误判成"已持久化 的真实消息",点击撤回后端正确拒绝(无此消息),却给用户一个答非所问 的"请求内容有误"提示。 第十一轮最小粒度测试 Ch3-4 发现。 Co-Authored-By: Claude Sonnet 5 --- src/views/messages-view.jsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index c7fc153b..fe446595 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -3,7 +3,7 @@ import React, { Suspense } from 'react'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; -import { Drafty, Tinode, TheCard } from 'tinode-sdk'; +import { Drafty, Tinode, TheCard, LOCAL_SEQID } from 'tinode-sdk'; const CallPanel = React.lazy(_ => import('../widgets/call-panel.jsx')); import ChatMessage from '../widgets/chat-message.jsx'; @@ -1088,10 +1088,22 @@ class MessagesView extends React.Component { // allowAdmin=true, so the backend has always accepted this; the reference screenshot // (消息页面—长按弹出.png) lists "群撤回" as its own, always-shown menu item alongside "撤回" -- // this was previously never offered at all since the only condition checked was authorship. + // + // `params.seq > 0` alone is not enough to mean "a real, server-persisted message": Tinode + // SDK gives every message that hasn't been confirmed by the server yet (still sending, or + // failed outright) a local placeholder seq of LOCAL_SEQID (0xFFFFFFF = 268435455) -- a large + // *positive* number, so it passed this check. Round 11 live testing reproduced this: sending + // a message into a since-blocked P2P topic left a bubble stuck in "发送失败" with seq + // 268435455, and this menu still offered "撤回" for it -- clicking it fired + // POST /topics/.../messages/268435455/recall, which the server correctly rejects (no such + // message), surfacing a confusing "请求内容有误,请稍后重试" toast for an action that should + // never have been offered. `seq < LOCAL_SEQID` excludes exactly that placeholder range, + // matching the same bound the SDK itself uses internally to distinguish a confirmed seq from + // a queued one. const recallWindow = this.state.messagePolicy ? this.state.messagePolicy.recall_window_seconds : 0; const canRecallAsAdmin = topic.isGroupType() && topic.getAccessMode() && topic.getAccessMode().isAdmin(); - if ((params.userFrom == this.props.myUserId || canRecallAsAdmin) && params.seq > 0 && recallWindow > 0 && - params.timestamp && params.timestamp.getTime() > (new Date().getTime() - recallWindow * 1000)) { + if ((params.userFrom == this.props.myUserId || canRecallAsAdmin) && params.seq > 0 && params.seq < LOCAL_SEQID && + recallWindow > 0 && params.timestamp && params.timestamp.getTime() > (new Date().getTime() - recallWindow * 1000)) { menuItems.push('menu_item_recall'); } // Self -> (1) always hard-delete with [Delete] title.