mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-08 23:13:17 +08:00
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:
parent
e8e58924ca
commit
a169b164a8
10 changed files with 268 additions and 182 deletions
|
|
@ -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; }
|
||||
|
|
|
|||
348
lib/index.js
348
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.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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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 } : {}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
21
test/channels/whatsapp/group-subject.test.mjs
Normal file
21
test/channels/whatsapp/group-subject.test.mjs
Normal 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'), '');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue