Strip WhatsApp @bot LID tokens from inbound group text.

Mention triggers leave opaque LID digits in the body; remove those only,
without injecting sender identity (use context enhancement when needed).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-05 16:04:55 +08:00
parent d9edca4d5f
commit e5e1e6573a
6 changed files with 138 additions and 52 deletions

View file

@ -24,7 +24,7 @@ dsh plugin --profile web add -w "github:hansjone/dsh-im-ops"
# 重启 dsh web
```
包名:`dsh-im-ops@4.9.1-ops.2`(cordis id:`dsh-im-ops`)。
包名:`dsh-im-ops@4.9.1-ops.3`(cordis id:`dsh-im-ops`)。
扫码态一般仍在 `~/.dsh/integrations/…`;换包后若异常,在 IM 设置里重新关联设备。
@ -38,7 +38,7 @@ dsh plugin --profile web add -w "github:hansjone/dsh-im-ops"
2. **访问控制**仍由本 fork 持久化(`workspaces.json` 的 `accessPolicies`),设置 UI 与上游同构,运维可直接改白名单 / open 模式
3. **WhatsApp LID**:群参与者常为不透明 `@lid`,白名单只填手机号时会被静默拒绝。`4.9.1-ops.2` 用 Baileys LID→PN 映射扩展 sender 别名,并在 @ 提及匹配时同样解析;若仍被拒绝会回一句白名单提示
3. **WhatsApp LID**:群参与者常为不透明 `@lid`,白名单只填手机号时会被静默拒绝。`4.9.1-ops.3` 用 Baileys LID→PN 映射扩展 sender 别名,并在 @ 提及匹配时同样解析;若仍被拒绝会回一句白名单提示
4. **主动群通告**继续用既有 delivery(`botId + targetId`),与入站 Session 策略独立

View file

@ -571,7 +571,7 @@ var React23 = __toESM(require("react"), 1);
// package.json
var package_default = {
name: "dsh-im-ops",
version: "4.9.1-ops.2",
version: "4.9.1-ops.3",
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",

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.2",
"version": "4.9.1-ops.3",
"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

@ -76,6 +76,83 @@ async function lookupLidForPn(socket, pn) {
}
}
/**
* Collect local-part tokens for the linked bot account (PN and LID).
* Used only to strip trigger @mentions from inbound text — not context injection.
* @param {string} accountJid
* @param {string[]} mentionedJids
* @param {string|null} accountLid
* @returns {string[]}
*/
export function whatsappBotMentionTokens(accountJid, mentionedJids = [], accountLid = null) {
const tokens = new Set();
const add = (jid) => {
if (typeof jid !== 'string' || !jid) return;
const decoded = jidDecode(jid.trim());
if (decoded?.user && /^\d+$/.test(decoded.user)) tokens.add(decoded.user);
};
add(accountJid);
add(accountLid);
for (const jid of mentionedJids) {
if (typeof jid !== 'string' || !jid) continue;
let matchesBot = false;
try {
matchesBot = areJidsSameUser(jid, accountJid) === true;
} catch {
matchesBot = false;
}
if (!matchesBot && accountLid) {
try {
matchesBot = areJidsSameUser(jid, accountLid) === true;
} catch {
matchesBot = false;
}
}
if (matchesBot) add(jid);
}
return [...tokens];
}
/**
* Remove WhatsApp @bot trigger tokens from plain text (e.g. `@1627…` / leading LID).
* Does not add sender identity — that belongs to optional context enhancement.
* @param {string} text
* @param {string[]} tokens
* @returns {string}
*/
export function stripWhatsappBotMentionText(text, tokens) {
if (typeof text !== 'string' || !text || !Array.isArray(tokens) || tokens.length === 0) {
return typeof text === 'string' ? text : '';
}
let result = text;
for (const token of tokens) {
if (!/^\d{5,32}$/.test(token)) continue;
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
result = result.replace(new RegExp(`@${escaped}\\b[\\u200e\\u200f\\s]*`, 'gu'), '');
result = result.replace(new RegExp(`^${escaped}\\b[\\u200e\\u200f\\s]*`, 'u'), '');
}
return result.replace(/[ \t]{2,}/g, ' ').trim();
}
function inboundMentionContext(rawMessage) {
let current = rawMessage;
for (let depth = 0; depth < 5 && current && typeof current === 'object'; depth += 1) {
const context = current.extendedTextMessage?.contextInfo
?? current.imageMessage?.contextInfo
?? current.videoMessage?.contextInfo
?? current.documentMessage?.contextInfo
?? null;
if (context) return context;
const wrapper = ['ephemeralMessage', 'viewOnceMessage', 'documentWithCaptionMessage',
'viewOnceMessageV2', 'viewOnceMessageV2Extension', 'editedMessage',
'associatedChildMessage', 'groupStatusMessage', 'groupStatusMessageV2']
.find((key) => current[key]?.message);
if (!wrapper) return null;
current = current[wrapper].message;
}
return null;
}
/**
* Expand inbound WhatsApp identities with PN/LID aliases and re-evaluate
* group @mention against the linked account.
@ -104,27 +181,12 @@ export async function enrichWhatsappInboundIdentities(message, raw, {
if (pn) aliasIds.add(pn);
}
const context = inboundMentionContext(raw?.message);
const mentioned = Array.isArray(context?.mentionedJid) ? context.mentionedJid : [];
const accountLid = await lookupLidForPn(socket, accountJid);
let addressed = message.addressed === true;
if (message.kind === 'group' && addressed !== true) {
const content = raw?.message;
let current = content;
let context = null;
for (let depth = 0; depth < 5 && current && typeof current === 'object'; depth += 1) {
context = current.extendedTextMessage?.contextInfo
?? current.imageMessage?.contextInfo
?? current.videoMessage?.contextInfo
?? current.documentMessage?.contextInfo
?? null;
if (context) break;
const wrapper = ['ephemeralMessage', 'viewOnceMessage', 'documentWithCaptionMessage',
'viewOnceMessageV2', 'viewOnceMessageV2Extension', 'editedMessage',
'associatedChildMessage', 'groupStatusMessage', 'groupStatusMessageV2']
.find((key) => current[key]?.message);
if (!wrapper) break;
current = current[wrapper].message;
}
const mentioned = Array.isArray(context?.mentionedJid) ? context.mentionedJid : [];
const accountLid = await lookupLidForPn(socket, accountJid);
for (const jid of mentioned) {
if (typeof jid !== 'string' || !jid) continue;
try {
@ -161,10 +223,14 @@ export async function enrichWhatsappInboundIdentities(message, raw, {
}
}
const botTokens = whatsappBotMentionTokens(accountJid, mentioned, accountLid);
const content = stripWhatsappBotMentionText(message.content, botTokens);
aliasIds.delete(message.senderId);
const senderAliasIds = [...aliasIds];
return {
...message,
content,
addressed,
...(senderAliasIds.length > 0 ? { senderAliasIds } : {}),
...(senderAliasIds[0] && !message.senderAlternateId

View file

@ -5,6 +5,8 @@ import {
enrichWhatsappInboundIdentities,
isWhatsappLidJid,
rememberWhatsappLidPnPairs,
stripWhatsappBotMentionText,
whatsappBotMentionTokens,
} from '../../../src/channels/whatsapp/whatsapp-identity.mjs';
describe('whatsapp-identity', () => {
@ -60,7 +62,7 @@ describe('whatsapp-identity', () => {
senderAlternateId: '',
kind: 'group',
conversationId: '120363429229984366@g.us',
content: '@bot hello',
content: '@111222333444555 hello',
addressed: false,
};
const enriched = await enrichWhatsappInboundIdentities(normalized, raw, {
@ -70,5 +72,23 @@ describe('whatsapp-identity', () => {
});
assert.equal(enriched.addressed, true);
assert.ok(enriched.senderAliasIds.includes('8618142387786@s.whatsapp.net'));
assert.equal(enriched.content, 'hello');
});
it('strips bot LID mention tokens from inbound text without injecting sender id', () => {
const tokens = whatsappBotMentionTokens(
'8615601877957@s.whatsapp.net',
['162788605444170@lid'],
'162788605444170@lid',
);
assert.ok(tokens.includes('162788605444170'));
assert.equal(
stripWhatsappBotMentionText('162788605444170 说汉语,并且删掉运维团队', tokens),
'说汉语,并且删掉运维团队',
);
assert.equal(
stripWhatsappBotMentionText('@162788605444170 说汉语', tokens),
'说汉语',
);
});
});