mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
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:
parent
37742043a9
commit
a92080e490
5 changed files with 331 additions and 215 deletions
|
|
@ -571,7 +571,7 @@ var React25 = __toESM(require("react"), 1);
|
|||
// package.json
|
||||
var package_default = {
|
||||
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.",
|
||||
keywords: [
|
||||
"deepseek-harness",
|
||||
|
|
|
|||
376
lib/index.js
376
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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.",
|
||||
"keywords": [
|
||||
"deepseek-harness",
|
||||
|
|
|
|||
|
|
@ -25,10 +25,56 @@ export function parseConversationKey(key) {
|
|||
return conversationId ? { kind: 'group', conversationId, senderId: null } : null;
|
||||
}
|
||||
|
||||
function contactPushName(grant, phone) {
|
||||
if (!phone || !grant || !Array.isArray(grant.contacts)) return '';
|
||||
const hit = grant.contacts.find((contact) => contact?.phone === phone);
|
||||
return typeof hit?.pushName === 'string' ? hit.pushName.trim() : '';
|
||||
/**
|
||||
* Normalize a WhatsApp LID identity for contact matching.
|
||||
* Accepts `digits@lid`, `digits:device@lid`, or a bare lid token already stored on contacts.
|
||||
* @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) {
|
||||
|
|
@ -36,12 +82,21 @@ function groupTitle(grant, groupJid) {
|
|||
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.
|
||||
* Label shows only group title / nickname / phone — never opaque WhatsApp ids.
|
||||
* @param {{
|
||||
* channel: string,
|
||||
* botId: string,
|
||||
|
|
@ -61,12 +116,9 @@ export function resolveChannelPeerFromBinding(input) {
|
|||
const grant = input.grant && typeof input.grant === 'object' ? input.grant : null;
|
||||
|
||||
if (parsed.kind === 'direct') {
|
||||
const phone = phoneFromIdentity(parsed.conversationId);
|
||||
const pushName = contactPushName(grant, phone);
|
||||
const who = pushName || phone || parsed.conversationId;
|
||||
const label = phone && pushName && pushName !== phone
|
||||
? `${pushName} · ${phone}`
|
||||
: who;
|
||||
const { phone, pushName } = resolveContactIdentity(grant, parsed.conversationId);
|
||||
const label = formatChannelPeerLabel(pushName, phone);
|
||||
if (!label) return null;
|
||||
return Object.freeze({
|
||||
channel,
|
||||
botId,
|
||||
|
|
@ -74,8 +126,8 @@ export function resolveChannelPeerFromBinding(input) {
|
|||
conversationKey,
|
||||
conversationId: parsed.conversationId,
|
||||
senderId: null,
|
||||
phone: phone || null,
|
||||
pushName: pushName || null,
|
||||
phone,
|
||||
pushName,
|
||||
groupTitle: null,
|
||||
label,
|
||||
});
|
||||
|
|
@ -93,13 +145,12 @@ export function resolveChannelPeerFromBinding(input) {
|
|||
phone: null,
|
||||
pushName: null,
|
||||
groupTitle: title,
|
||||
label: `群 · ${title}`,
|
||||
label: title,
|
||||
});
|
||||
}
|
||||
|
||||
const phone = phoneFromIdentity(parsed.senderId);
|
||||
const pushName = contactPushName(grant, phone);
|
||||
const who = pushName || phone || parsed.senderId;
|
||||
const { phone, pushName } = resolveContactIdentity(grant, parsed.senderId);
|
||||
const label = formatChannelPeerLabel(title, pushName, phone) || title;
|
||||
return Object.freeze({
|
||||
channel,
|
||||
botId,
|
||||
|
|
@ -107,9 +158,9 @@ export function resolveChannelPeerFromBinding(input) {
|
|||
conversationKey,
|
||||
conversationId: parsed.conversationId,
|
||||
senderId: parsed.senderId,
|
||||
phone: phone || null,
|
||||
pushName: pushName || null,
|
||||
phone,
|
||||
pushName,
|
||||
groupTitle: title,
|
||||
label: `${title} · ${who}`,
|
||||
label,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import { join } from 'node:path';
|
|||
import { test } from 'node:test';
|
||||
|
||||
import {
|
||||
formatChannelPeerLabel,
|
||||
lidFromWhatsappIdentity,
|
||||
parseConversationKey,
|
||||
resolveChannelPeerFromBinding,
|
||||
resolveContactIdentity,
|
||||
} from '../../../src/channels/shared/channel-peer.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', () => {
|
||||
const grant = {
|
||||
contacts: [{ phone: '8618222222222', pushName: 'Alice' }],
|
||||
contacts: [{
|
||||
phone: '8618222222222',
|
||||
lids: ['91010910658657@lid'],
|
||||
pushName: 'Alice',
|
||||
}],
|
||||
groups: {
|
||||
'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.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({
|
||||
channel: 'whatsapp',
|
||||
botId: 'bot-1',
|
||||
conversationKey: 'group:120363@g.us:user:8618222222222@s.whatsapp.net',
|
||||
conversationKey: 'group:120363@g.us:user:91010910658657@lid',
|
||||
grant,
|
||||
});
|
||||
assert.equal(groupUser.label, 'Ops Room · Alice');
|
||||
assert.equal(groupUser.label, 'Ops Room · Alice · 8618222222222');
|
||||
assert.equal(groupUser.groupTitle, 'Ops Room');
|
||||
assert.doesNotMatch(groupUser.label, /@lid/);
|
||||
|
||||
const groupShared = resolveChannelPeerFromBinding({
|
||||
channel: 'whatsapp',
|
||||
|
|
@ -62,7 +80,54 @@ test('resolveChannelPeerFromBinding builds DM and group labels from access grant
|
|||
conversationKey: 'group:120363@g.us',
|
||||
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 () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue