Show WA nicknames and auto-fill group subjects in access UI (4.9.1-ops.9).

Fetch groupMetadata.subject on @mentions for group cards; render contact pushName under phone rows so admins are not staring at bare numbers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-05 17:29:43 +08:00
parent e8e58924ca
commit a169b164a8
10 changed files with 268 additions and 182 deletions

View file

@ -571,7 +571,7 @@ var React24 = __toESM(require("react"), 1);
// package.json
var package_default = {
name: "dsh-im-ops",
version: "4.9.1-ops.8",
version: "4.9.1-ops.9",
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",
@ -852,6 +852,7 @@ var EN = Object.freeze({
"\u673A\u5668\u4EBA\u8BBE\u7F6E\u9875\u7B7E": "Bot settings tabs",
"\u6295\u9012\u8BBE\u7F6E": "Delivery settings",
"\u8BBF\u95EE\u8BBE\u7F6E": "Access settings",
"\u6682\u65E0\u6635\u79F0\uFF08\u79C1\u804A/@ \u540E\u4F1A\u81EA\u52A8\u8865\u9F50\uFF09": "No nickname yet (filled after DM or @mention)",
"\u7FA4\u804A \xB7 ": "Group \xB7 ",
"\u3002\u8BB0\u5F97\u70B9\u4FDD\u5B58\u3002": ". Remember to save.",
"\u5DF2\u52A0\u5165\u7FA4\u6388\u6743\uFF1A": "Added group access for: ",
@ -12588,6 +12589,18 @@ function contactLabel(contact) {
const phone = contact.phone || "\u5F85\u8865\u7535\u8BDD";
return `${name2} \xB7 ${phone}`;
}
function nicknameForPhone(contacts, phone) {
const digits = digitsOnly(phone);
if (!digits) return "";
const hit = (contacts ?? []).find((contact) => contact.phone === digits || contact.phone === phone);
return typeof hit?.pushName === "string" ? hit.pushName.trim() : "";
}
function NicknameHint({ contacts, phone }) {
const nickname = nicknameForPhone(contacts, phone);
return h2("span", {
className: nickname ? "dim-accessNickname" : "dim-accessNickname dim-accessNicknameEmpty"
}, nickname || "\u6682\u65E0\u6635\u79F0\uFF08\u79C1\u804A/@ \u540E\u4F1A\u81EA\u52A8\u8865\u9F50\uFF09");
}
function groupDisplayName(groupJid, groups) {
const title = groups?.[groupJid]?.title;
if (typeof title === "string" && title.trim()) return title.trim();
@ -12702,7 +12715,8 @@ function MemberRows({
disabled,
placeholder: "8613800000000",
onChange: (phone) => onChange(members.map((entry, i) => i === index ? { ...entry, phone } : entry))
})
}),
h2(NicknameHint, { contacts, phone: member.phone })
),
h2(
"label",
@ -12765,7 +12779,8 @@ function AdminPhones({
disabled: disabled || locked,
placeholder: "8613800000000",
onChange: (next) => onChange(phones.map((entry, i) => i === index ? next : entry))
})
}),
locked ? null : h2(NicknameHint, { contacts, phone })
),
locked ? null : h2("button", {
type: "button",
@ -14595,6 +14610,8 @@ var CSS11 = String.raw`
.dim-accessGroupHeading { min-width: 0; display: grid; gap: 2px; }
.dim-accessGroupHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 21px; font-weight: 650; }
.dim-accessGroupJid { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 16px; overflow-wrap: anywhere; }
.dim-accessNickname { display: block; margin-top: 4px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; font-weight: 560; }
.dim-accessNicknameEmpty { color: var(--dsw-alias-label-tertiary, #8f959e); font-weight: 400; }
.dim-accessUserRow { position: relative; z-index: 0; overflow: visible; }
.dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
.dim-panel .ddt-headingCopy { display: none; }

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{
"name": "dsh-im-ops",
"version": "4.9.1-ops.8",
"version": "4.9.1-ops.9",
"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",

View file

@ -71,6 +71,20 @@ function contactLabel(contact) {
return `${name} · ${phone}`;
}
function nicknameForPhone(contacts, phone) {
const digits = digitsOnly(phone);
if (!digits) return '';
const hit = (contacts ?? []).find((contact) => contact.phone === digits || contact.phone === phone);
return typeof hit?.pushName === 'string' ? hit.pushName.trim() : '';
}
function NicknameHint({ contacts, phone }) {
const nickname = nicknameForPhone(contacts, phone);
return h('span', {
className: nickname ? 'dim-accessNickname' : 'dim-accessNickname dim-accessNicknameEmpty',
}, nickname || '暂无昵称(私聊/@ 后会自动补齐)');
}
function groupDisplayName(groupJid, groups) {
const title = groups?.[groupJid]?.title;
if (typeof title === 'string' && title.trim()) return title.trim();
@ -180,7 +194,8 @@ function MemberRows({
onChange: (phone) => onChange(members.map((entry, i) => (
i === index ? { ...entry, phone } : entry
))),
})),
}),
h(NicknameHint, { contacts, phone: member.phone })),
h('label', { className: 'dim-accessField dim-accessUserCommand' },
h('span', null, '命令权限'),
h('select', {
@ -233,7 +248,8 @@ function AdminPhones({
disabled: disabled || locked,
placeholder: '8613800000000',
onChange: (next) => onChange(phones.map((entry, i) => (i === index ? next : entry))),
})),
}),
locked ? null : h(NicknameHint, { contacts, phone })),
locked ? null : h('button', {
type: 'button',
className: 'dim-deliveryButton',

View file

@ -11,6 +11,7 @@ const EN = Object.freeze({
'机器人设置页签': 'Bot settings tabs',
'投递设置': 'Delivery settings',
'访问设置': 'Access settings',
"暂无昵称(私聊/@ 后会自动补齐)": "No nickname yet (filled after DM or @mention)",
"群聊 · ": "Group · ",
"。记得点保存。": ". Remember to save.",

View file

@ -483,6 +483,8 @@ const CSS = String.raw`
.dim-accessGroupHeading { min-width: 0; display: grid; gap: 2px; }
.dim-accessGroupHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 21px; font-weight: 650; }
.dim-accessGroupJid { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 16px; overflow-wrap: anywhere; }
.dim-accessNickname { display: block; margin-top: 4px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; font-weight: 560; }
.dim-accessNicknameEmpty { color: var(--dsw-alias-label-tertiary, #8f959e); font-weight: 400; }
.dim-accessUserRow { position: relative; z-index: 0; overflow: visible; }
.dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
.dim-panel .ddt-headingCopy { display: none; }

View file

@ -194,6 +194,7 @@ export async function gateWhatsappInbound({
message,
sendText,
accountJid,
resolveGroupTitle,
}) {
const { phone, lid } = resolveInboundPhone(message);
const scene = message.kind === 'group' ? 'group' : 'direct';
@ -205,7 +206,15 @@ export async function gateWhatsappInbound({
current = await rememberWhatsappContact(workspaces, botId, current, message, { phone, lid });
}
if (scene === 'group' && addressed && message.conversationId) {
const title = message.contextSource?.()?.conversationTitle;
let title = message.contextSource?.()?.conversationTitle;
const existingTitle = current.groups?.[message.conversationId]?.title;
if (!title && !existingTitle && typeof resolveGroupTitle === 'function') {
try {
title = await resolveGroupTitle(message.conversationId);
} catch {
title = '';
}
}
const withGroup = ensureGroupBucket(current, message.conversationId, {
...(title ? { title } : {}),
});

View file

@ -238,3 +238,21 @@ export async function enrichWhatsappInboundIdentities(message, raw, {
: {}),
};
}
/**
* Resolve a WhatsApp group subject for display / access-grant group cards.
* @param {{ groupMetadata?: (jid: string) => Promise<{ subject?: string }> }|null|undefined} socket
* @param {string} groupJid
* @returns {Promise<string>}
*/
export async function resolveWhatsappGroupSubject(socket, groupJid) {
if (!socket || typeof socket.groupMetadata !== 'function') return '';
if (typeof groupJid !== 'string' || !groupJid.endsWith('@g.us')) return '';
try {
const meta = await socket.groupMetadata(groupJid);
const subject = typeof meta?.subject === 'string' ? meta.subject.trim() : '';
return subject.slice(0, 128);
} catch {
return '';
}
}

View file

@ -12,7 +12,7 @@ import { t } from '../shared/i18n.mjs';
import { ImagePromptError } from '../shared/image-prompt.mjs';
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
import { enrichWhatsappInboundIdentities } from './whatsapp-identity.mjs';
import { enrichWhatsappInboundIdentities, resolveWhatsappGroupSubject } from './whatsapp-identity.mjs';
import {
gateWhatsappInbound,
loadWhatsappAccessGrant,
@ -754,6 +754,7 @@ export class WhatsappRuntime {
});
if (handled) return;
grant = this.#workspaces.accessGrantFor(this.#botId) ?? grant;
const socket = context?.socket ?? this.#session?.socket;
const gated = await gateWhatsappInbound({
workspaces: this.#workspaces,
botId: this.#botId,
@ -761,6 +762,7 @@ export class WhatsappRuntime {
message,
sendText,
accountJid: this.#config.accountJid,
resolveGroupTitle: (groupJid) => resolveWhatsappGroupSubject(socket, groupJid),
});
if (!gated.allowed) {
if (gated.accessDecision) {

View file

@ -0,0 +1,21 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolveWhatsappGroupSubject } from '../../../src/channels/whatsapp/whatsapp-identity.mjs';
test('resolveWhatsappGroupSubject reads Baileys groupMetadata.subject', async () => {
const subject = await resolveWhatsappGroupSubject({
groupMetadata: async (jid) => {
assert.equal(jid, '120363429229984366@g.us');
return { subject: ' 运维值班群 ' };
},
}, '120363429229984366@g.us');
assert.equal(subject, '运维值班群');
});
test('resolveWhatsappGroupSubject fails soft without socket metadata', async () => {
assert.equal(await resolveWhatsappGroupSubject(null, '120363429229984366@g.us'), '');
assert.equal(await resolveWhatsappGroupSubject({
groupMetadata: async () => { throw new Error('offline'); },
}, '120363429229984366@g.us'), '');
});