修复发送含链接消息时切换话题,风控弹窗可能把消息送错会话

现象:在会话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 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-06 15:57:22 +08:00
co-authored by Claude Sonnet 5
parent 644b663ed1
commit b6adc032a2
+17
View File
@@ -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();