修复通话发起失败静默无反应的问题(第8章复测发现)

sendMessageToTopic 自己的 .catch 会吞掉发送失败——publishMessage 在服务端拒绝时只标记
pub._failed=true 让消息列表画红色感叹号(这是给聊天气泡设计的反馈方式),resolve 成 undefined
而不是 reject。通话邀请这条消息不会被用户看到(用户盯着的是全屏通话面板,不是消息列表),走
这条路径等于什么反馈都没有;而且原代码 `ctrl.code` 直接解引用,undefined.code 会抛
TypeError,变成一个没人 catch 的 rejected promise——handleCallClose() 根本不会被调用,
通话面板会卡在"呼叫中"状态出不来。

这条路径今天有了真实后果:后端新增的全局通话开关(im_client_feature)关闭时,服务端会拒绝
这条 {pub},正好命中这个从未被验证过的失败分支。

修复:判断 ctrl 是否存在、补 .catch,两处都调用 handleError 提示 + handleCallClose 复位。
顺带给通话按钮可见性加了一层预判——读共享的 bootstrap 缓存,features.audio_call/video_call
为 false 时不显示按钮;读不到缓存时按原行为放行(fail-open,只是预判不是安全边界)。

已用真实账号验证:通话菜单项在开关为真时正常显示,无回归。真实失败场景(开关被关闭)无法在
当前浏览器沙盒环境下端到端验证(摄像头/麦克风权限被拦截,此前已确认的环境限制),修复依据是
逐层读 tinode-sdk 源码精确追踪出的调用链,不是猜测。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-04 13:14:44 +08:00
co-authored by Claude Sonnet 5
parent 8e703d86eb
commit 1d51e53a46
3 changed files with 33 additions and 7 deletions
+24 -4
View File
@@ -29,8 +29,8 @@ import { PACKAGE_VERSION } from '../version.js';
import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js';
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
import LocalStorageUtil from '../lib/local-storage.js';
import { getTenantContext, resolveQRCode, createGroupJoinRequest } from '../lib/im-api.js';
import { writeCachedData } from '../lib/use-cached-data.js';
import { getTenantContext, resolveQRCode, createGroupJoinRequest, getBootstrap } from '../lib/im-api.js';
import { readCachedData, writeCachedData } from '../lib/use-cached-data.js';
import HashNavigation from '../lib/navigation.js';
import { secondsToTime } from '../lib/strformat.js'
import { defaultWallpaper, theCard, topicDisplayName, updateFavicon } from '../lib/utils.js';
@@ -2022,7 +2022,16 @@ class TinodeWeb extends React.Component {
}
}
webrtc = writer && !!this.tinode.getServerParam('iceServers');
// 后端今天新增了一个全局(非按租户)的通话总开关:{pub head:webrtc:...} 现在会先过
// im_client_feature 这个开关,关闭时整条消息直接被服务端拒绝。这个开关缺失/查询失败时是
// fail-closed(当成关闭),比 Tinode 协议层的 iceServers 配置更容易出现"某天突然打不通"的
// 情况。这里读一下各页面已经在共享的 bootstrap 缓存,能读到就按 features 门控;读不到就按
// 原来的行为放行——按钮可见性只是一层预判,真正失败时上面 handleCallInvite 已经会报错,
// 这里不做的话用户点了按钮才发现打不通,做了至少能提前少显示一个注定失败的入口。
const cachedBootstrap = this.state.myUserId ? readCachedData('bootstrap:' + this.state.myUserId) : null;
const callFeatureOk = !cachedBootstrap || !cachedBootstrap.features ||
(cachedBootstrap.features.audio_call !== false && cachedBootstrap.features.video_call !== false);
webrtc = writer && callFeatureOk && !!this.tinode.getServerParam('iceServers');
p2p = Tinode.isP2PTopicName(topicName);
self = Tinode.isSelfTopicName(topicName);
@@ -2327,14 +2336,25 @@ class TinodeWeb extends React.Component {
switch (callState) {
case CALL_STATE_OUTGOING_INITATED:
const head = { webrtc: CALL_HEAD_STARTED, aonly: !!audioOnly };
// sendMessageToTopic 自己的 .catch 会吞掉发送失败(只标记 pub._failed=true 让消息列表画
// 红色感叹号——这是给聊天气泡设计的反馈方式,通话邀请这条消息不会被用户看到,走这条路径
// 相当于什么反馈都没有),resolve 成 undefined 而不是 reject。原来这里 `ctrl.code` 直接
// 解引用,undefined.code 会抛 TypeError,变成一个没人 catch 的 rejected promise——
// handleCallClose() 根本不会被调用,通话面板会卡在"呼叫中"状态出不来,且用户不知道为什么。
// 今天这条路径已经有真实后果:新的全局通话开关 (im_client_feature) 关闭时服务端会拒绝这条
// {pub},命中的就是这个从未被验证过的失败分支。
return this.handleSendMessage(Drafty.videoCall(audioOnly), undefined, undefined, head)
.then(ctrl => {
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
if (!ctrl || ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
this.handleError('通话发起失败,请稍后重试', 'err');
this.handleCallClose();
return ctrl;
}
this.setState({callSeq: ctrl.params['seq']});
return ctrl
}).catch(err => {
this.handleError((err && err.message) || '通话发起失败,请稍后重试', 'err');
this.handleCallClose();
});
case CALL_STATE_IN_PROGRESS:
const topic = this.tinode.getTopic(callTopic);