Hide WhatsApp LID ids from session-header peer labels (4.9.1-ops.21).

Resolve nickname/phone via contact LID mapping and show only group title, pushName, and phone.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-05 19:14:38 +08:00
parent 37742043a9
commit a92080e490
5 changed files with 331 additions and 215 deletions

View file

@ -571,7 +571,7 @@ var React25 = __toESM(require("react"), 1);
// package.json // package.json
var package_default = { var package_default = {
name: "dsh-im-ops", name: "dsh-im-ops",
version: "4.9.1-ops.20", version: "4.9.1-ops.21",
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.", description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.",
keywords: [ keywords: [
"deepseek-harness", "deepseek-harness",

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{ {
"name": "dsh-im-ops", "name": "dsh-im-ops",
"version": "4.9.1-ops.20", "version": "4.9.1-ops.21",
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.", "description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.",
"keywords": [ "keywords": [
"deepseek-harness", "deepseek-harness",

View file

@ -25,10 +25,56 @@ export function parseConversationKey(key) {
return conversationId ? { kind: 'group', conversationId, senderId: null } : null; return conversationId ? { kind: 'group', conversationId, senderId: null } : null;
} }
function contactPushName(grant, phone) { /**
if (!phone || !grant || !Array.isArray(grant.contacts)) return ''; * Normalize a WhatsApp LID identity for contact matching.
const hit = grant.contacts.find((contact) => contact?.phone === phone); * Accepts `digits@lid`, `digits:device@lid`, or a bare lid token already stored on contacts.
return typeof hit?.pushName === 'string' ? hit.pushName.trim() : ''; * @param {unknown} value
* @returns {string|null}
*/
export function lidFromWhatsappIdentity(value) {
if (typeof value !== 'string') return null;
const raw = value.trim();
if (!raw) return null;
const at = raw.indexOf('@');
if (at > 0) {
const server = raw.slice(at + 1).toLowerCase();
if (server !== 'lid' && server !== 'hosted.lid') return null;
const user = raw.slice(0, at).split(':')[0].trim();
return user ? `${user}@lid` : null;
}
// Contacts may already store the canonical `digits@lid` form only.
return null;
}
function lidsMatch(stored, needle) {
if (typeof stored !== 'string' || !needle) return false;
const left = lidFromWhatsappIdentity(stored) ?? stored.trim();
return left === needle || left === needle.replace(/@lid$/, '');
}
/**
* Resolve contact phone/pushName from grant by PN or LID.
* @param {object|null} grant
* @param {string|null} identity
* @returns {{ phone: string|null, pushName: string|null }}
*/
export function resolveContactIdentity(grant, identity) {
const phone = phoneFromWhatsappJid(identity) ?? normalizeAccessPhone(identity);
const lid = lidFromWhatsappIdentity(identity);
const contacts = Array.isArray(grant?.contacts) ? grant.contacts : [];
const hit = contacts.find((contact) => {
if (phone && contact?.phone === phone) return true;
if (lid && Array.isArray(contact?.lids) && contact.lids.some((entry) => lidsMatch(entry, lid))) {
return true;
}
return false;
}) ?? null;
const resolvedPhone = phone
?? (typeof hit?.phone === 'string' && hit.phone ? hit.phone : null);
const pushName = typeof hit?.pushName === 'string' && hit.pushName.trim()
? hit.pushName.trim()
: null;
return { phone: resolvedPhone, pushName };
} }
function groupTitle(grant, groupJid) { function groupTitle(grant, groupJid) {
@ -36,12 +82,21 @@ function groupTitle(grant, groupJid) {
return typeof title === 'string' && title.trim() ? title.trim() : ''; return typeof title === 'string' && title.trim() ? title.trim() : '';
} }
function phoneFromIdentity(value) { /**
return phoneFromWhatsappJid(value) ?? normalizeAccessPhone(value); * Join human-facing label parts. Never includes raw JIDs / LIDs.
* @param {...(string|null|undefined)} parts
* @returns {string}
*/
export function formatChannelPeerLabel(...parts) {
return parts
.map((part) => (typeof part === 'string' ? part.trim() : ''))
.filter(Boolean)
.join(' · ');
} }
/** /**
* Build a public channel-peer summary for session-header display. * Build a public channel-peer summary for session-header display.
* Label shows only group title / nickname / phone — never opaque WhatsApp ids.
* @param {{ * @param {{
* channel: string, * channel: string,
* botId: string, * botId: string,
@ -61,12 +116,9 @@ export function resolveChannelPeerFromBinding(input) {
const grant = input.grant && typeof input.grant === 'object' ? input.grant : null; const grant = input.grant && typeof input.grant === 'object' ? input.grant : null;
if (parsed.kind === 'direct') { if (parsed.kind === 'direct') {
const phone = phoneFromIdentity(parsed.conversationId); const { phone, pushName } = resolveContactIdentity(grant, parsed.conversationId);
const pushName = contactPushName(grant, phone); const label = formatChannelPeerLabel(pushName, phone);
const who = pushName || phone || parsed.conversationId; if (!label) return null;
const label = phone && pushName && pushName !== phone
? `${pushName} · ${phone}`
: who;
return Object.freeze({ return Object.freeze({
channel, channel,
botId, botId,
@ -74,8 +126,8 @@ export function resolveChannelPeerFromBinding(input) {
conversationKey, conversationKey,
conversationId: parsed.conversationId, conversationId: parsed.conversationId,
senderId: null, senderId: null,
phone: phone || null, phone,
pushName: pushName || null, pushName,
groupTitle: null, groupTitle: null,
label, label,
}); });
@ -93,13 +145,12 @@ export function resolveChannelPeerFromBinding(input) {
phone: null, phone: null,
pushName: null, pushName: null,
groupTitle: title, groupTitle: title,
label: `群 · ${title}`, label: title,
}); });
} }
const phone = phoneFromIdentity(parsed.senderId); const { phone, pushName } = resolveContactIdentity(grant, parsed.senderId);
const pushName = contactPushName(grant, phone); const label = formatChannelPeerLabel(title, pushName, phone) || title;
const who = pushName || phone || parsed.senderId;
return Object.freeze({ return Object.freeze({
channel, channel,
botId, botId,
@ -107,9 +158,9 @@ export function resolveChannelPeerFromBinding(input) {
conversationKey, conversationKey,
conversationId: parsed.conversationId, conversationId: parsed.conversationId,
senderId: parsed.senderId, senderId: parsed.senderId,
phone: phone || null, phone,
pushName: pushName || null, pushName,
groupTitle: title, groupTitle: title,
label: `${title} · ${who}`, label,
}); });
} }

View file

@ -5,8 +5,11 @@ import { join } from 'node:path';
import { test } from 'node:test'; import { test } from 'node:test';
import { import {
formatChannelPeerLabel,
lidFromWhatsappIdentity,
parseConversationKey, parseConversationKey,
resolveChannelPeerFromBinding, resolveChannelPeerFromBinding,
resolveContactIdentity,
} from '../../../src/channels/shared/channel-peer.mjs'; } from '../../../src/channels/shared/channel-peer.mjs';
import { ConversationStateStore } from '../../../src/channels/shared/conversation-state-store.mjs'; import { ConversationStateStore } from '../../../src/channels/shared/conversation-state-store.mjs';
@ -32,7 +35,11 @@ test('parseConversationKey covers direct, shared group, and per-user group', ()
test('resolveChannelPeerFromBinding builds DM and group labels from access grant', () => { test('resolveChannelPeerFromBinding builds DM and group labels from access grant', () => {
const grant = { const grant = {
contacts: [{ phone: '8618222222222', pushName: 'Alice' }], contacts: [{
phone: '8618222222222',
lids: ['91010910658657@lid'],
pushName: 'Alice',
}],
groups: { groups: {
'120363@g.us': { title: 'Ops Room', admins: [], members: [] }, '120363@g.us': { title: 'Ops Room', admins: [], members: [] },
}, },
@ -47,14 +54,25 @@ test('resolveChannelPeerFromBinding builds DM and group labels from access grant
assert.equal(direct.kind, 'direct'); assert.equal(direct.kind, 'direct');
assert.equal(direct.phone, '8618222222222'); assert.equal(direct.phone, '8618222222222');
const lidDirect = resolveChannelPeerFromBinding({
channel: 'whatsapp',
botId: 'bot-1',
conversationKey: 'direct:91010910658657@lid',
grant,
});
assert.equal(lidDirect.label, 'Alice · 8618222222222');
assert.equal(lidDirect.phone, '8618222222222');
assert.doesNotMatch(lidDirect.label, /@lid/);
const groupUser = resolveChannelPeerFromBinding({ const groupUser = resolveChannelPeerFromBinding({
channel: 'whatsapp', channel: 'whatsapp',
botId: 'bot-1', botId: 'bot-1',
conversationKey: 'group:120363@g.us:user:8618222222222@s.whatsapp.net', conversationKey: 'group:120363@g.us:user:91010910658657@lid',
grant, grant,
}); });
assert.equal(groupUser.label, 'Ops Room · Alice'); assert.equal(groupUser.label, 'Ops Room · Alice · 8618222222222');
assert.equal(groupUser.groupTitle, 'Ops Room'); assert.equal(groupUser.groupTitle, 'Ops Room');
assert.doesNotMatch(groupUser.label, /@lid/);
const groupShared = resolveChannelPeerFromBinding({ const groupShared = resolveChannelPeerFromBinding({
channel: 'whatsapp', channel: 'whatsapp',
@ -62,7 +80,54 @@ test('resolveChannelPeerFromBinding builds DM and group labels from access grant
conversationKey: 'group:120363@g.us', conversationKey: 'group:120363@g.us',
grant, grant,
}); });
assert.equal(groupShared.label, '群 · Ops Room'); assert.equal(groupShared.label, 'Ops Room');
});
test('resolveChannelPeerFromBinding never falls back to raw LID when contact is incomplete', () => {
const grant = {
contacts: [{ lids: ['91010910658657@lid'], pushName: 'Bob' }],
groups: {
'120363@g.us': { title: 'test2', admins: [], members: [] },
},
};
const direct = resolveChannelPeerFromBinding({
channel: 'whatsapp',
botId: 'bot-1',
conversationKey: 'direct:91010910658657@lid',
grant,
});
assert.equal(direct.label, 'Bob');
assert.equal(direct.phone, null);
const unknown = resolveChannelPeerFromBinding({
channel: 'whatsapp',
botId: 'bot-1',
conversationKey: 'direct:111222333444@lid',
grant,
});
assert.equal(unknown, null);
const groupOnlyLid = resolveChannelPeerFromBinding({
channel: 'whatsapp',
botId: 'bot-1',
conversationKey: 'group:120363@g.us:user:111222333444@lid',
grant,
});
assert.equal(groupOnlyLid.label, 'test2');
assert.doesNotMatch(groupOnlyLid.label, /@lid|\d{10,}@/);
});
test('lid and label helpers stay human-facing', () => {
assert.equal(lidFromWhatsappIdentity('91010910658657@lid'), '91010910658657@lid');
assert.equal(lidFromWhatsappIdentity('91010910658657:12@lid'), '91010910658657@lid');
assert.equal(lidFromWhatsappIdentity('8618222222222@s.whatsapp.net'), null);
assert.equal(formatChannelPeerLabel('Ops', '', '8618', null), 'Ops · 8618');
assert.deepEqual(
resolveContactIdentity({
contacts: [{ phone: '8618', lids: ['9@lid'], pushName: 'N' }],
}, '9@lid'),
{ phone: '8618', pushName: 'N' },
);
}); });
test('ConversationStateStore.keyForSession reverses session bindings', async () => { test('ConversationStateStore.keyForSession reverses session bindings', async () => {