From 50767f701f243e0bef4156bb0fd28baae1282b99 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 17 Sep 2026 12:52:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B7=B2=E5=BB=BA=E7=AB=8B?= =?UTF-8?q?=E7=9A=84P2P=E4=BC=9A=E8=AF=9D=E8=AF=A6=E6=83=85=E6=A0=8F/?= =?UTF-8?q?=E5=A4=B4=E5=83=8F404=EF=BC=9A=E8=AF=9D=E9=A2=98=E5=90=8D?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=E5=AF=B9=E6=96=B9user=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户报告:控制台反复出现 GET /v1/im/users/p2pp77kUAkgqpVbZoiTpjPWtg/profile 404。根因:这个应用的多处代码(DetailColumn的好友资料/拉黑/备注/删除好友,以及 本会话早前刚加的头像peerProfileQuery)一直假设"P2P会话的topicId就是对方的 user_id"——这个假设只在"第一次用对方usr...地址打开、还从未真正subscribe过" 这个短暂窗口内成立。读tinode-sdk源码确认:一旦subscribe成功,SDK会把这个话题 从usr...地址重命名为服务端分配的真实p2p...哈希名,而Topic.topicType()自己的 前缀表里根本没有p2p这一项(只有usr映射到P2P类型),导致topic.isP2PType()/ topic.p2pPeerDesc()对已建立的会话恒定返回false/undefined——SDK自己内建的"取 对方身份"方法在这个场景下也是坏的,不只是这个项目自己的代码有问题。 只要一个P2P会话有过真实的消息历史(几乎所有现存对话都是这样),它的topicId 就是这个不能反解的p2p...哈希,而不是对方user_id,直接拿去查/v1/im/users/{id}/ profile、匹配好友列表的user_id、传给拉黑/删除好友接口,全部会用错id。 修复:topics.ts新增getP2PPeerUserId(topic, myUserId)——topicId本身是usr... 前缀时直接返回(还没subscribe过,本来就对),否则读topic.subscribers()真实 的订阅者列表,找出不是自己的那个人的真实user_id;订阅者列表还没加载出来时返回 undefined而不是猜一个不可能对的值,调用方原本就有的enabled门控会正确地等待 而不是提前拿错误id发请求。tinode-sdk.d.ts补充subscribers()方法和Subscription 类型声明(原来完全没声明这个方法)。DetailColumn.tsx/ConversationView.tsx改用 这个真实user_id去查资料/匹配好友/头像取色,不再直接假设topicId能用。 实机验证:真实会话grpMc5uN...不对,是p2pp77kUAkgqpVbZoiTpjPWtg(systest04↔ systest02,有真实历史消息的老会话)——修复前详情栏"ID:"整行空白、404貫穿控制 台;修复后详情栏正确显示"ID:usrp77kUAkgqpU",好友资料/备注/拉黑状态全部正确 关联到真实好友记录。 --- .../features/messages/ConversationView.tsx | 23 +++++++++++--- web/src/features/messages/DetailColumn.tsx | 29 ++++++++++++----- web/src/lib/tinode/tinode-sdk.d.ts | 26 ++++++++++++++++ web/src/lib/tinode/topics.ts | 31 +++++++++++++++++++ 4 files changed, 97 insertions(+), 12 deletions(-) diff --git a/web/src/features/messages/ConversationView.tsx b/web/src/features/messages/ConversationView.tsx index adf58de1..3eec38a4 100644 --- a/web/src/features/messages/ConversationView.tsx +++ b/web/src/features/messages/ConversationView.tsx @@ -35,6 +35,7 @@ import { closeConversation, ensureTopicAttached, forgetTopic, + getP2PPeerUserId, markConfirmedRead, openConversation, setActiveConversationTopic, @@ -463,16 +464,23 @@ export function ConversationView() { // `['me']` key NavRail/ChatListColumn/SettingsLayout already keep warm, so this is a cache read in // the normal case, not a new request. const meQuery = useQuery({ queryKey: ['me'], queryFn: getMe }); + // NOT topicId -- see getP2PPeerUserId's own doc comment. A P2P topic with any real history is + // addressed by a real, opaque `p2p...` name, not the peer's `usr...` id (that only holds true in + // the brief window before the very first subscribe) -- confirmed live: without this, this query + // 404s (`GET /users/{p2p...}/profile`) for every established conversation, and only ever appeared + // to work in earlier testing because that test happened to use a conversation opened for the first + // time in the same session, before the SDK's own rename kicked in. + const peerUserId = !isGroup && !isSelf && topicId ? getP2PPeerUserId(tinode.getTopic(topicId), myUserId) : undefined; // Real gap this was written to close: the chat header/message-row avatars only ever drew the // letter-tile fallback, even after a real custom avatar was uploaded and correctly showing up on // the contacts pages (FriendProfileView/ContactsListColumn) -- those read this exact same REST // profile, the conversation screen just never fetched it. Same query DetailColumn already runs - // (`['user-profile', topicId]`), so this dedupes with it whenever the detail panel happens to be + // (`['user-profile', peerUserId]`), so this dedupes with it whenever the detail panel happens to be // open, and only fetches for a real P2P peer, never for a group or `slf`. const peerProfileQuery = useQuery({ - queryKey: ['user-profile', topicId], - queryFn: () => getUserProfile(topicId || ''), - enabled: !!topicId && !isGroup && !isSelf, + queryKey: ['user-profile', peerUserId], + queryFn: () => getUserProfile(peerUserId || ''), + enabled: !!peerUserId, }); const membersQuery = useQuery({ queryKey: ['group-members', topicId], @@ -525,6 +533,11 @@ export function ConversationView() { const myAvatarFileId = meQuery.data?.profile.avatar_file_id; const peerAvatarFileId = isSelf ? myAvatarFileId : peerProfileQuery.data?.avatar_file_id; const headerAvatarFileId = isGroup ? groupProfileQuery.data?.avatar_file_id : peerAvatarFileId; + // Color-hash id, not the file lookup above -- topicId is right for a group (its own identity), but + // for P2P this should be the peer's stable usr id (peerUserId), matching the same hash ContactsListColumn/ + // FriendProfileView use for this same person, rather than the p2p topic name (which isP2P/self also + // never had a usr-shaped id for in the first place). + const headerAvatarId = isGroup ? topicId : peerUserId || topicId; // `peerName` is the GROUP's own name for a group topic, not any member's -- reusing it to label // "who is being quoted" (as both the reply-composer preview and the actual sent quote block used // to) meant every single group-chat reply permanently displayed "回复 <群名>:…" for every member, @@ -824,7 +837,7 @@ export function ConversationView() {