修复Zentao批量报告的6个前端bug:来电UI不弹出/黑名单发送无提示/群名校验/群人数与通话记录显示uid

用户提供了一份Zentao导出的bug清单(#21/23/24/25/26/27/28共7条),逐条实机复现
排查,修复了其中6个真正的前端bug,1个是纯后端问题(禁言不生效)已记录到
docs/IM-后端问题清单.md,1个(好友申请404/500)未能独立复现只记录了测试团队
的原始报告,另1个(离线状态推送)实测后确认并非报告描述的"永久不更新",只是
有正常的心跳超时延迟。

- **#23 来电UI完全不弹出**(根因最深的一个):initCallSignaling用的是
  `me.getMeTopic().contacts()`枚举联系人再查最新消息——但这个方法只反映`me`
  话题自己的订阅同步进度,一个P2P话题可能早就真正订阅上、能收到消息,`me`的
  同步却还没跟上,导致这个联系人在枚举里直接消失。用两个真实账号实机验证:
  被叫方`me.contacts()`只枚举出1个群,主叫方完全没出现,来电消息静默丢失。
  topics.ts里`getChatListSnapshot()`自己的注释早就写明了这个坑、并且已经改
  用`mapTopics()`规避,只是`stores/calls.ts`这个监听器当时没有跟着改。现在
  统一复用`getChatListSnapshot()`,实机验证来电卡片正确弹出。

- **#28 拉黑后发送消息无原因提示**:读tinode-sdk源码确认发送失败的具体原因
  从架构上就拿不到——`Topic#publishMessage`自己的`.catch()`把服务端拒绝原因
  吞掉、只留一个`_failed`标记,不会重新抛出。改成事前判断:新增
  `getConversationContext()`(P2P专用,`relationship`字段区分
  `blocked_by_me`/`blocked_by_peer`),发送区直接换成对应文案的横幅,两个
  方向都拿真实账号互相拉黑验证过。

- **#26 群名超长/留空自动名两个校验问题**:`MAX_TITLE_LENGTH`一直是60,但
  后端`im_http.go`真实校验的是32字(读源码确认),导致33-59字之间的群名能
  输入、能提交,然后被后端拒绝只显示通用错误。改成32,从输入框这一层就不
  再放行超长名。另外群名留空时自动拼接的成员昵称组合,理论上可能被内容审核
  拒绝——这种情况下用户根本不知道"名字"是问题所在,补了针对性提示。

- **#27 通话记录/群列表显示原始uid、群人数显示0人**(2处独立bug):
  1. `CallHistoryView.tsx`按`peer_user_id`(真实usr id)去匹配聊天列表的
     `contact.topic`(建立过真实会话后已经是服务端重命名过的p2p哈希),两个
     不是同一个id空间,永远匹配不上,一直显示原始id。改成用`item.topic`
     (已确认和chatlist的topic是同一个值)去匹配。
  2. 实测发现`GET /groups`列表接口的`member_count`会返回0,但同一个群
     `GET /groups/{topic}`单独查询正确返回4——两个接口的计数器不一致(后端
     已知问题,见问题8),前端这次统一改成"读到0就不显示人数"而不是显示一
     个已确认错误的数字。

以上改动均跑过`tsc -b --noEmit`/`oxlint`/`vite build`,除#26外全部用两个真实
账号(systest02/systest04)互相操作实机验证。
This commit is contained in:
Your Name
2026-09-17 21:51:00 +08:00
parent 50767f701f
commit 0c5b329179
8 changed files with 124 additions and 22 deletions
+14 -4
View File
@@ -28,9 +28,12 @@ function statusLabel(item: CallHistoryItem): string {
/** README §7 doesn't spec a dedicated call-history screen beyond the "最近通话" idea implicit in
* NavRail's own 通话记录 entry -- this is a plain list off the confirmed real `GET /v1/im/calls`
* endpoint, whose response shape (see calls.ts's own comment) turned out completely different
* from the doc's own minimal example once checked live: `peer_user_id` (a real `usr...` id, unlike
* `topic`, which comes back `p2p...`-hashed and unusable for navigation/re-dialing) is resolved to
* a display name via the chat list, same lookup ChatListColumn itself uses.
* from the doc's own minimal example once checked live: `peer_user_id` (a real `usr...` id) is used
* to navigate/re-dial (any valid Tinode P2P address works there, resolved by the SDK same as opening
* a friend's profile does); `topic` (the same opaque `p2p...` form the chat list itself caches once
* a conversation has real history) is what display-name resolution matches against instead -- the
* two are different identifier spaces and matching name lookup against `peer_user_id` was a real,
* live-confirmed bug (Zentao #27) that always fell through to the raw uid.
*/
export function CallHistoryView() {
const navigate = useNavigate();
@@ -80,7 +83,14 @@ export function CallHistoryView() {
// something the viewer missed and should feel warned about. This used to redden EVERY
// non-'finished' result alike.
const isMissed = result === 'missed' && incoming;
const name = contacts.find((c) => c.topic === item.peer_user_id)?.public?.fn || item.peer_user_id;
// NOT `c.topic === item.peer_user_id` -- live-confirmed real bug (Zentao #27): `peer_user_id`
// is a real `usr...` id, but an established P2P conversation's `contact.topic` is the SDK's
// renamed, opaque `p2p...` form (see getP2PPeerUserId's own doc comment in topics.ts for the
// full mechanism) -- the two are different identifier spaces and this comparison never
// matched for any call with real history, always falling through to the raw uid. `item.topic`
// is that same `p2p...` form already (confirmed live: identical string to what the chat list
// caches for this conversation), so matching on it directly is the correct, direct fix.
const name = contacts.find((c) => c.topic === item.topic)?.public?.fn || item.peer_user_id;
const ts = item.started_at || item.created_at;
return (
<div key={`${item.call_id}`} className="flex items-center gap-3 rounded-xl bg-white px-4 py-3">