From b6adc032a23a18aa0c8f04be5fc39c7a665478ab Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 15:57:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=91=E9=80=81=E5=90=AB?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E6=B6=88=E6=81=AF=E6=97=B6=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E8=AF=9D=E9=A2=98=EF=BC=8C=E9=A3=8E=E6=8E=A7=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E6=8A=8A=E6=B6=88=E6=81=AF=E9=80=81=E9=94=99?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:在会话A输入含链接的消息并点发送,POST /security/urls/check 请求还未返回时切换到 会话B,请求返回后风控弹窗会在会话B里弹出(本该属于会话A),此时点"继续发送",消息被 真实发进了会话B,而不是最初输入它的会话A——跨会话消息误投递。 复现:会话A(kcg051307)输入"https://example.com/racefix?token=fix002"点发送,立即 (在checkURL网络请求返回前)切换到会话B(r12c56b2),风控弹窗在会话B视图下弹出;点 "继续发送"后消息被投递进会话B,会话A完全没有收到,会话B收到了本不该发给它的内容 (本次因B恰好还不是好友触发"发送失败"提示,但发送动作本身已经指向了错误的会话, 如果B是好友会真实送达)。 根因:`getDerivedStateFromProps`在话题切换时会把`riskDialog`重置为null,但这只能防住 "弹窗已经弹出、随后切换话题"这一种时序——它防不住"checkURL还在进行中就切换话题, 此时机的.then()回调在切换之后才执行"这种反过来的时序:这次setState({riskDialog:{...}}) 是在切换完成之后才发生的,不会被这次重置覆盖,于是在当前(错误的)话题下重新弹出弹窗, 且弹窗里的`doSend`闭包沿用的是`this.props.sendMessage`,按确认时的当前话题发送, 不是消息最初输入时所在的话题。 修法:sendMessage()进入URL风险检测分支时已经缓存了`topicForDraft`(用于清草稿), 在checkURL的.then()回调最开头加一道判断——如果`this.state.topic`已经不等于 `topicForDraft`,说明用户已经离开发起这次检测的会话,直接返回false(视同消息被放弃, 不弹窗、不发送),不再继续读取风控结果或setState。这与本文件已有的"两条风险消息连续 触发、后一个覆盖前一个"场景处理逻辑(resolve(false)后覆盖)是同一种"宁可丢弃، 不可 错误投递"的一贯取舍,只是这次要防的是切换会话而不是重复触发。 验证:npm run build:dev后浏览器实测——(1) 会话A输入风险链接点发送,立即切换到会话B, 2秒后确认会话B里没有弹出风控弹窗(修复前必现);(2) 切回会话A确认最后一条消息仍是修复 前测试遗留的旧消息,本次新消息没有出现在A也没有出现在B,两个会话都没有收到,符合 "宁可丢弃不可错投"的设计;(3) 对照组(不切换话题、正常触发风控)依旧能看到弹窗并正确 发送/取消,未被这次改动影响。 Co-Authored-By: Claude Sonnet 5 --- src/views/messages-view.jsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index d4b919dd..7664e09a 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -1654,6 +1654,23 @@ class MessagesView extends React.Component { const RISK_RANK = {block: 2, warn: 1, allow: 0}; const urls = Array.from(new Set(urlMatches)); return Promise.all(urls.map(url => checkURL(url, 'message_compose'))).then(results => { + // The user may have already switched to a different conversation while checkURL was + // still in flight. getDerivedStateFromProps only protects against an ALREADY-shown + // dialog surviving a later topic switch (it nulls riskDialog when nextProps.topic != + // prevState.topic) -- it can't protect against this .then() running AFTER that switch, + // which would otherwise setState({riskDialog:{...}}) fresh right here, popping the risk + // dialog up while a completely different conversation is now open. doSend() still calls + // this.props.sendMessage using whatever topic is current AT CONFIRM TIME (see doSend's + // own comment above), not the one this message was actually typed in, so confirming that + // dialog would silently deliver this message into the wrong conversation. Confirmed live: + // typed a flagged URL in topic A, switched to topic B before checkURL resolved, and the + // resulting dialog rendered while viewing topic B -- confirming it delivered topic A's + // message into topic B instead. If the topic has moved on, treat this message the same + // as if it were abandoned -- no dialog, no send -- the same "lost, but not hung forever" + // outcome already accepted just below for two flagged messages sent in quick succession. + if (this.state.topic !== topicForDraft) { + return false; + } const result = results.reduce((worst, r) => (RISK_RANK[r.decision] || 0) > (RISK_RANK[worst.decision] || 0) ? r : worst); if (result.decision === 'allow') { return doSend();