@xmanrui/dsh-im 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -97,6 +97,11 @@ function normalizeBot(value) {
97
97
  typeof entry === 'string' && /^[1-9]\d{4,14}$/.test(entry)
98
98
  )))]
99
99
  : [],
100
+ groupAllowedNumbers: Array.isArray(value.accessPolicy?.groupAllowedNumbers)
101
+ ? [...new Set(value.accessPolicy.groupAllowedNumbers.filter((entry) => (
102
+ typeof entry === 'string' && /^[1-9]\d{4,14}$/.test(entry)
103
+ )))]
104
+ : [],
100
105
  },
101
106
  bot: {
102
107
  name: text(value.bot?.name, 'WhatsApp机器人', 100),
@@ -37,6 +37,8 @@ function accessPolicyFor(account) {
37
37
  accessMode,
38
38
  allowedNumbers: Array.isArray(account?.accessPolicy?.allowedNumbers)
39
39
  ? account.accessPolicy.allowedNumbers : [],
40
+ groupAllowedNumbers: Array.isArray(account?.accessPolicy?.groupAllowedNumbers)
41
+ ? account.accessPolicy.groupAllowedNumbers : [],
40
42
  };
41
43
  }
42
44
 
@@ -52,30 +54,39 @@ function allowedNumbersFromText(value) {
52
54
  export function WhatsappAccessSettings({ account, busy = false, onSave }) {
53
55
  const policy = accessPolicyFor(account);
54
56
  const sourceNumbers = policy.allowedNumbers.join('\n');
57
+ const sourceGroupNumbers = policy.groupAllowedNumbers.join('\n');
55
58
  const helpId = React.useId();
56
59
  const [accessMode, setAccessMode] = React.useState(policy.accessMode);
57
60
  const [allowedNumbers, setAllowedNumbers] = React.useState(sourceNumbers);
61
+ const [groupAllowedNumbers, setGroupAllowedNumbers] = React.useState(sourceGroupNumbers);
58
62
  const [error, setError] = React.useState(null);
59
63
 
60
64
  React.useEffect(() => {
61
65
  setAccessMode(policy.accessMode);
62
66
  setAllowedNumbers(sourceNumbers);
67
+ setGroupAllowedNumbers(sourceGroupNumbers);
63
68
  setError(null);
64
- }, [policy.accessMode, sourceNumbers]);
69
+ }, [policy.accessMode, sourceNumbers, sourceGroupNumbers]);
65
70
 
66
71
  const save = async (event) => {
67
72
  event.preventDefault();
68
73
  setError(null);
69
74
  try {
70
75
  const normalized = allowedNumbersFromText(allowedNumbers);
76
+ const normalizedGroup = allowedNumbersFromText(groupAllowedNumbers);
71
77
  if (typeof onSave !== 'function') throw new Error('WhatsApp 访问设置暂不可用。');
72
- await onSave({ accessMode, allowedNumbers: normalized });
78
+ await onSave({
79
+ accessMode,
80
+ allowedNumbers: normalized,
81
+ groupAllowedNumbers: normalizedGroup,
82
+ });
73
83
  } catch (caught) {
74
84
  setError(caught?.message ?? 'WhatsApp 访问设置保存失败。');
75
85
  }
76
86
  };
77
87
 
78
88
  const allowlistEnabled = accessMode === 'private-allowlist';
89
+ const groupAllowlistEnabled = accessMode === 'open';
79
90
  const labels = {
80
91
  'self-only': '仅自己模式',
81
92
  'private-allowlist': '指定联系人模式',
@@ -103,7 +114,7 @@ export function WhatsappAccessSettings({ account, busy = false, onSave }) {
103
114
  h('span', null, '响应自聊和白名单联系人的私聊,忽略群聊。')),
104
115
  h('span', { className: 'dwa-accessTooltipItem' },
105
116
  h('strong', null, '开放响应模式'),
106
- h('span', null, '响应所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员的提及或回复。')))))),
117
+ h('span', null, '响应所有私聊、已绑定账号自己发出的群聊消息,以及允许成员的提及或回复;群聊号码列表留空时允许所有群成员。')))))),
107
118
  h('label', { className: 'dwa-accessField' },
108
119
  h('span', null, '模式'),
109
120
  h('select', {
@@ -115,18 +126,28 @@ export function WhatsappAccessSettings({ account, busy = false, onSave }) {
115
126
  h('option', { value: 'self-only' }, '仅自己模式(默认)'),
116
127
  h('option', { value: 'private-allowlist' }, '指定联系人模式'),
117
128
  h('option', { value: 'open' }, '开放响应模式'))),
118
- allowlistEnabled
129
+ (allowlistEnabled || groupAllowlistEnabled)
119
130
  ? h('label', { className: 'dwa-accessField' },
120
- h('span', null, '允许私聊的 WhatsApp 电话号码'),
131
+ h('span', null, allowlistEnabled
132
+ ? '允许私聊的 WhatsApp 电话号码'
133
+ : '允许在群聊中呼叫机器人的 WhatsApp 电话号码'),
121
134
  h('textarea', {
122
- value: allowedNumbers,
135
+ value: allowlistEnabled ? allowedNumbers : groupAllowedNumbers,
123
136
  disabled: busy,
124
137
  rows: 3,
125
138
  placeholder: '每行一个含国家或地区代码的号码',
126
- 'aria-label': '允许私聊的 WhatsApp 电话号码',
127
- onChange: (event) => { setAllowedNumbers(event.target.value); setError(null); },
139
+ 'aria-label': allowlistEnabled
140
+ ? '允许私聊的 WhatsApp 电话号码'
141
+ : '允许在群聊中呼叫机器人的 WhatsApp 电话号码',
142
+ onChange: (event) => {
143
+ if (allowlistEnabled) setAllowedNumbers(event.target.value);
144
+ else setGroupAllowedNumbers(event.target.value);
145
+ setError(null);
146
+ },
128
147
  }),
129
- h('small', null, '可以包含开头的 +,保存时会自动移除。'))
148
+ h('small', null, allowlistEnabled
149
+ ? '可以包含开头的 +,保存时会自动移除。'
150
+ : '留空表示所有群成员都可以通过提及或回复呼叫机器人。'))
130
151
  : null,
131
152
  allowlistEnabled && allowedNumbers.trim() === ''
132
153
  ? h('p', { className: 'dwa-accessWarning', role: 'status' },
@@ -380,9 +380,12 @@ const EN = Object.freeze({
380
380
  '只响应已绑定 WhatsApp 账号的自聊消息。': 'Only respond to self-chat messages from the linked WhatsApp account.',
381
381
  '响应自聊和白名单联系人的私聊,忽略群聊。': 'Respond to self-chat and allowlisted direct messages; ignore group messages.',
382
382
  '响应所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员的提及或回复。': 'Respond to all direct messages, group messages sent by the linked account, and mentions or replies from other group members.',
383
+ '响应所有私聊、已绑定账号自己发出的群聊消息,以及允许成员的提及或回复;群聊号码列表留空时允许所有群成员。': 'Respond to all direct messages, group messages sent by the linked account, and mentions or replies from allowed members; leaving the group number list empty allows every group member.',
383
384
  '允许私聊的 WhatsApp 电话号码': 'WhatsApp phone numbers allowed to send direct messages',
385
+ '允许在群聊中呼叫机器人的 WhatsApp 电话号码': 'WhatsApp phone numbers allowed to call the bot in group chats',
384
386
  '每行一个含国家或地区代码的号码': 'One number with country or region code per line',
385
387
  '可以包含开头的 +,保存时会自动移除。': 'A leading + is allowed and removed when saved.',
388
+ '留空表示所有群成员都可以通过提及或回复呼叫机器人。': 'Leave empty to let every group member call the bot by mentioning it or replying to it.',
386
389
  '仅指定联系人模式使用白名单,切换模式时会保留。': 'Only Selected contacts uses the allowlist; it is retained when modes change.',
387
390
  '白名单为空;保存后将只接受自聊消息。': 'The allowlist is empty; only self-chat messages will be accepted after saving.',
388
391
  '电话号码必须包含国家或地区代码,每行一个。': 'Each phone number must include a country or region code on its own line.',
@@ -161,7 +161,6 @@ export function IMSettingsTab({
161
161
  }) {
162
162
  const [selected, setSelected] = React.useState('weixin');
163
163
  const [loopbackRecovery, setLoopbackRecovery] = React.useState(null);
164
- const versionTooltipId = React.useId();
165
164
  const githubTooltipId = React.useId();
166
165
  const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0];
167
166
  const reportLoopbackRecovery = React.useCallback((recovery) => {
@@ -198,20 +197,11 @@ export function IMSettingsTab({
198
197
  return h(WorkspaceDirectoryPickerContext.Provider, { value: workspaceDirectoryPicker },
199
198
  h('section', { className: 'dim-page', 'aria-label': 'IM机器人设置' },
200
199
  h('header', { className: 'dim-title' },
201
- h('div', {
202
- className: 'dim-brand',
203
- tabIndex: 0,
204
- 'aria-describedby': versionTooltipId,
205
- },
206
- h('strong', { className: 'dim-brandName' }, 'DSH-IM'),
207
- h('p', null, '让 DeepSeek Harness 触手可及'),
208
- h('span', {
209
- id: versionTooltipId,
210
- className: 'dim-versionTooltip',
211
- role: 'tooltip',
212
- },
213
- h('span', null, '当前版本'),
214
- h('strong', null, `v${IM_PLUGIN_VERSION}`))),
200
+ h('div', { className: 'dim-brand' },
201
+ h('div', { className: 'dim-brandHeading' },
202
+ h('strong', { className: 'dim-brandName' }, 'DSH-IM'),
203
+ h('span', { className: 'dim-brandVersion' }, `v${IM_PLUGIN_VERSION}`)),
204
+ h('p', null, '让 DeepSeek Harness 触手可及')),
215
205
  h('span', { className: 'dim-githubAction' },
216
206
  h('a', {
217
207
  className: 'dim-githubLink',
@@ -12,13 +12,11 @@ const CSS = String.raw`
12
12
  }
13
13
  .dim-page *, .dim-page *::before, .dim-page *::after { box-sizing: border-box; }
14
14
  .dim-title { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 18px; }
15
- .dim-brand { position: relative; min-width: 0; width: max-content; max-width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; margin: -2px -6px; padding: 2px 6px; border-radius: 8px; cursor: help; }
16
- .dim-brand:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
15
+ .dim-brand { min-width: 0; width: max-content; max-width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; margin: -2px -6px; padding: 2px 6px; border-radius: 8px; }
16
+ .dim-brandHeading { display: flex; align-items: baseline; gap: 8px; white-space: nowrap; }
17
17
  .dim-brandName { color: var(--dsw-alias-label-primary, #1f2329); font-size: 20px; line-height: 24px; font-weight: 800; letter-spacing: .04em; }
18
+ .dim-brandVersion { color: var(--dsw-alias-label-tertiary, #8f959e); font: 500 10px/16px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0; }
18
19
  .dim-title p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; font-weight: 500; white-space: nowrap; }
19
- .dim-versionTooltip { position: absolute; top: calc(100% + 8px); left: 0; z-index: 20; width: max-content; max-width: min(220px, 80vw); display: inline-flex; align-items: center; gap: 6px; padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
20
- .dim-versionTooltip strong { color: var(--dsw-alias-label-primary, #1f2329); font: 600 11px/16px ui-monospace, SFMono-Regular, Menlo, monospace; }
21
- .dim-brand:hover .dim-versionTooltip, .dim-brand:focus .dim-versionTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
22
20
  .dim-githubAction { position: relative; display: inline-flex; flex: none; }
23
21
  .dim-githubLink { min-height: 30px; display: inline-flex; align-items: center; gap: 5px; flex: none; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); font-size: 12px; line-height: normal; font-weight: 560; text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
24
22
  .dim-githubLink:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
@@ -247,8 +245,6 @@ const CSS = String.raw`
247
245
  .dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
248
246
  @container (max-width: 680px) {
249
247
  .dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { gap: 6px; }
250
- .dim-panel .dim-botCardTop { flex-direction: column; align-items: stretch; }
251
- .dim-panel .dim-botHealthGroup { justify-items: start; }
252
248
  .dim-panel .dim-bindActions { gap: 6px; }
253
249
  .dim-panel .bxf-headingTools .dim-scanButton, .dim-panel .dxw-tools .dim-scanButton, .dim-panel .ddt-tools .dim-scanButton, .dim-panel .dim-credentialButton { gap: 5px; padding-inline: 8px; font-size: 12px; }
254
250
  .dim-panel .dim-actionIcon { width: 13px; height: 13px; flex-basis: 13px; }
@@ -272,10 +268,6 @@ const CSS = String.raw`
272
268
  .dim-rail { max-height: none; overflow: visible; padding-right: 1px; }
273
269
  .dim-channel { min-height: 48px; }
274
270
  }
275
- @media (max-width: 720px) {
276
- .dim-panel .dim-botCardTop { flex-direction: column; align-items: stretch; }
277
- .dim-panel .dim-botHealthGroup { justify-items: start; }
278
- }
279
271
  @media (max-width: 560px) {
280
272
  .dim-title { flex-direction: column; gap: 10px; }
281
273
  .dim-title p { white-space: normal; }
@@ -53,8 +53,8 @@ function payloadFailure(endpoint, payload) {
53
53
  && payload.confirm === true ? null : 'bot.delete requires a botId and confirm=true.';
54
54
  }
55
55
  if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
56
- if (!exactKeys(payload, ['botId', 'accessMode', 'allowedNumbers'])
57
- || Object.keys(payload).length !== 3
56
+ if (!exactKeys(payload, ['botId', 'accessMode', 'allowedNumbers', 'groupAllowedNumbers'])
57
+ || Object.keys(payload).length !== 4
58
58
  || !validId(payload.botId)) return '请输入有效的 WhatsApp 访问模式和电话号码。';
59
59
  try {
60
60
  normalizeWhatsappAccessPolicy(payload);
@@ -296,12 +296,23 @@ function splitUtf8(text, maxBytes = MAX_REPLY_BYTES) {
296
296
  return chunks;
297
297
  }
298
298
 
299
- function progressText(update) {
300
- if (update?.type === 'text') return update.text;
299
+ function thinkingProgressText(update) {
301
300
  if (update?.type === 'tool') return t('正在使用{name}…', { name: update.name });
302
301
  return update?.text;
303
302
  }
304
303
 
304
+ function streamContent(thinkingText, answerText = '', { finish = false } = {}) {
305
+ const thinking = String(thinkingText ?? '')
306
+ .replace(/<\/?think>/gi, '')
307
+ .trim();
308
+ const answer = String(answerText ?? '').trim();
309
+ if (!thinking) return answer;
310
+ const thinkBlock = finish || answer
311
+ ? `<think>${thinking}</think>`
312
+ : `<think>${thinking}`;
313
+ return answer ? `${thinkBlock}\n${answer}` : thinkBlock;
314
+ }
315
+
305
316
  function artifactFailureText(fileName, error) {
306
317
  const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim()
307
318
  || t('结果文件');
@@ -867,6 +878,8 @@ export class WecomHarnessBridge {
867
878
  const key = conversationKey(frame);
868
879
  let streamId = null;
869
880
  let streamStarted = false;
881
+ let streamThinkingText = t('正在思考中…');
882
+ let streamAnswerText = '';
870
883
  let batchSettled = batchSubmission === null;
871
884
  let promptRecorded = false;
872
885
  try {
@@ -920,7 +933,12 @@ export class WecomHarnessBridge {
920
933
 
921
934
  streamId = this.#generateReqId('stream');
922
935
  try {
923
- await this.#client.replyStream(frame, streamId, t('正在思考中…'), false);
936
+ await this.#client.replyStream(
937
+ frame,
938
+ streamId,
939
+ streamContent(streamThinkingText),
940
+ false,
941
+ );
924
942
  streamStarted = true;
925
943
  } catch (error) {
926
944
  this.#logger.warn?.('[dsh-im:wecom] unable to start a stream; using an active reply:', error);
@@ -945,8 +963,15 @@ export class WecomHarnessBridge {
945
963
  control: { owner: this, key },
946
964
  onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === 'function'
947
965
  ? async (update) => {
948
- const progress = splitUtf8(progressText(update))[0];
949
- if (progress) await this.#client.replyStreamNonBlocking(frame, streamId, progress, false);
966
+ if (update?.type === 'text') {
967
+ streamAnswerText = update.text;
968
+ } else {
969
+ streamThinkingText = thinkingProgressText(update) || streamThinkingText;
970
+ }
971
+ const preview = splitUtf8(
972
+ streamContent(streamThinkingText, streamAnswerText),
973
+ )[0];
974
+ if (preview) await this.#client.replyStreamNonBlocking(frame, streamId, preview, false);
950
975
  }
951
976
  : undefined,
952
977
  onInteraction: (interaction) => this.#handleInteraction(interaction, {
@@ -966,18 +991,20 @@ export class WecomHarnessBridge {
966
991
 
967
992
  this.#signal?.throwIfAborted();
968
993
  const displayAnswer = answerTextForDelivery(answer, artifacts);
969
- const chunks = splitUtf8(displayAnswer);
994
+ const streamChunks = splitUtf8(
995
+ streamContent(streamThinkingText, displayAnswer, { finish: true }),
996
+ );
970
997
  let finalSent = false;
971
998
  let textReceipt = null;
972
999
  let textSendError = null;
973
1000
  try {
974
- if (streamStarted && chunks.length > 0) {
1001
+ if (streamStarted && streamChunks.length > 0) {
975
1002
  try {
976
1003
  const providerMessageIds = [];
977
- const streamed = await this.#client.replyStream(frame, streamId, chunks[0], true);
1004
+ const streamed = await this.#client.replyStream(frame, streamId, streamChunks[0], true);
978
1005
  const streamedMessageId = providerMessageId(streamed);
979
1006
  if (streamedMessageId) providerMessageIds.push(streamedMessageId);
980
- for (const chunk of chunks.slice(1)) {
1007
+ for (const chunk of streamChunks.slice(1)) {
981
1008
  const sent = await this.#client.sendMessage(
982
1009
  chatId,
983
1010
  { msgtype: 'markdown', markdown: { content: chunk } },
@@ -1040,7 +1067,12 @@ export class WecomHarnessBridge {
1040
1067
  }
1041
1068
  if (error?.code === 'turn-stopped') {
1042
1069
  if (streamStarted && streamId) {
1043
- await this.#client.replyStream(frame, streamId, t('已停止。'), true)
1070
+ await this.#client.replyStream(
1071
+ frame,
1072
+ streamId,
1073
+ streamContent(streamThinkingText, t('已停止。'), { finish: true }),
1074
+ true,
1075
+ )
1044
1076
  .catch(() => undefined);
1045
1077
  }
1046
1078
  if (!promptRecorded) await this.#state.markSeen(messageId);
@@ -1063,7 +1095,12 @@ export class WecomHarnessBridge {
1063
1095
  : errorText;
1064
1096
  try {
1065
1097
  if (streamStarted && streamId) {
1066
- await this.#client.replyStream(frame, streamId, visibleError, true);
1098
+ await this.#client.replyStream(
1099
+ frame,
1100
+ streamId,
1101
+ streamContent(streamThinkingText, visibleError, { finish: true }),
1102
+ true,
1103
+ );
1067
1104
  } else {
1068
1105
  await this.#sendImmediate(frame, chatId, visibleError);
1069
1106
  }
@@ -63,6 +63,7 @@ export function normalizeWhatsappAccessPolicy(value = {}) {
63
63
  return Object.freeze({
64
64
  accessMode,
65
65
  allowedNumbers: normalizeWhatsappAllowedNumbers(value.allowedNumbers),
66
+ groupAllowedNumbers: normalizeWhatsappAllowedNumbers(value.groupAllowedNumbers),
66
67
  });
67
68
  }
68
69
 
@@ -334,6 +334,7 @@ export class WhatsappController {
334
334
  connectedAt: new Date().toISOString(),
335
335
  accessMode: previous?.accessMode,
336
336
  allowedNumbers: previous?.allowedNumbers,
337
+ groupAllowedNumbers: previous?.groupAllowedNumbers,
337
338
  };
338
339
  try {
339
340
  if (record.controller.signal.aborted || this.#closed) throw Object.assign(new Error(), { name: 'AbortError' });
@@ -3,6 +3,7 @@ import { createHash, randomBytes } from 'node:crypto';
3
3
  import {
4
4
  areJidsSameUser,
5
5
  downloadMediaMessage,
6
+ jidNormalizedUser,
6
7
  normalizeMessageContent,
7
8
  } from '@whiskeysockets/baileys';
8
9
 
@@ -13,6 +14,7 @@ import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifac
13
14
  import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
14
15
  import {
15
16
  WHATSAPP_ACCESS_MODES,
17
+ normalizeWhatsappAccountJid,
16
18
  normalizeWhatsappAccessPolicy,
17
19
  } from './config-store.mjs';
18
20
  import { createWhatsappWebSession } from './whatsapp-web-session.mjs';
@@ -204,8 +206,21 @@ export function createWhatsappMediaDownloader({
204
206
  });
205
207
  }
206
208
 
209
+ function whatsappAccountMatcher(accountJid, aliases) {
210
+ const accountJids = new Set(
211
+ [accountJid, ...(Array.isArray(aliases) ? aliases : [])]
212
+ .map((jid) => normalizeWhatsappAccountJid(jidNormalizedUser(jid)))
213
+ .filter(Boolean),
214
+ );
215
+ return (jid) => {
216
+ const normalized = normalizeWhatsappAccountJid(jidNormalizedUser(jid));
217
+ return normalized !== null && accountJids.has(normalized);
218
+ };
219
+ }
220
+
207
221
  export function normalizeWhatsappMessage(message, accountJid, {
208
222
  download = downloadMediaMessage,
223
+ accountAliases = [],
209
224
  } = {}) {
210
225
  const remoteJid = typeof message?.key?.remoteJid === 'string' ? message.key.remoteJid : '';
211
226
  const alternateRemoteJid = typeof message?.key?.remoteJidAlt === 'string'
@@ -215,8 +230,9 @@ export function normalizeWhatsappMessage(message, accountJid, {
215
230
  || remoteJid.endsWith('@newsletter')) return null;
216
231
  const group = remoteJid.endsWith('@g.us');
217
232
  const fromMe = message.key.fromMe === true;
233
+ const matchesAccount = whatsappAccountMatcher(accountJid, accountAliases);
218
234
  const selfChat = fromMe && !group
219
- && [remoteJid, alternateRemoteJid].some((jid) => jid && areJidsSameUser(jid, accountJid));
235
+ && [remoteJid, alternateRemoteJid].some(matchesAccount);
220
236
  if (fromMe && !selfChat && !group) return null;
221
237
  const senderJid = fromMe ? accountJid : group ? message.key.participant : remoteJid;
222
238
  const senderAlternateJid = group && !fromMe ? message.key.participantAlt : alternateRemoteJid;
@@ -225,9 +241,9 @@ export function normalizeWhatsappMessage(message, accountJid, {
225
241
  const content = normalizeMessageContent(message.message);
226
242
  const context = messageContext(content);
227
243
  const mentioned = Array.isArray(context?.mentionedJid)
228
- && context.mentionedJid.some((jid) => areJidsSameUser(jid, accountJid));
244
+ && context.mentionedJid.some(matchesAccount);
229
245
  const replyToSelf = typeof context?.participant === 'string'
230
- && areJidsSameUser(context.participant, accountJid);
246
+ && matchesAccount(context.participant);
231
247
  const image = whatsappImageSource(message, content, download, { viewOnce });
232
248
  const file = whatsappFileSource(message, content, download);
233
249
  return {
@@ -235,6 +251,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
235
251
  providerMessageId: messageId,
236
252
  senderId: senderJid,
237
253
  senderAlternateId: typeof senderAlternateJid === 'string' ? senderAlternateJid : '',
254
+ senderIsSelf: fromMe,
238
255
  senderIsBot: false,
239
256
  kind: group ? 'group' : 'direct',
240
257
  conversationId: remoteJid,
@@ -253,12 +270,22 @@ export function normalizeWhatsappMessage(message, accountJid, {
253
270
  export function whatsappInboundAllowed(message, {
254
271
  accessMode = WHATSAPP_ACCESS_MODES.selfOnly,
255
272
  allowedNumbers = new Set(),
273
+ groupAllowedNumbers = new Set(),
256
274
  } = {}) {
257
- if (accessMode === WHATSAPP_ACCESS_MODES.open) return true;
275
+ if (accessMode === WHATSAPP_ACCESS_MODES.open) {
276
+ if (message?.kind !== 'group' || message.senderIsSelf === true) return true;
277
+ if (!(groupAllowedNumbers instanceof Set)) return false;
278
+ if (groupAllowedNumbers.size === 0) return true;
279
+ const senderJids = [message.senderId, message.senderAlternateId]
280
+ .filter((jid) => typeof jid === 'string' && jid.endsWith('@s.whatsapp.net'));
281
+ return [...groupAllowedNumbers].some((number) => senderJids.some((jid) => (
282
+ areJidsSameUser(jid, `${number}@s.whatsapp.net`)
283
+ )));
284
+ }
258
285
  if (message?.kind !== 'direct') return false;
259
286
  if (message.selfChat === true) return true;
260
- if (accessMode !== WHATSAPP_ACCESS_MODES.privateAllowlist
261
- || !(allowedNumbers instanceof Set)) return false;
287
+ if (accessMode !== WHATSAPP_ACCESS_MODES.privateAllowlist) return false;
288
+ if (!(allowedNumbers instanceof Set)) return false;
262
289
  const senderJids = [message.senderId, message.senderAlternateId]
263
290
  .filter((jid) => typeof jid === 'string' && jid.endsWith('@s.whatsapp.net'));
264
291
  return [...allowedNumbers].some((number) => senderJids.some((jid) => (
@@ -542,6 +569,7 @@ export class WhatsappRuntime {
542
569
  #mediaUploadTimeoutMs;
543
570
  #accessMode;
544
571
  #allowedPrivateNumbers;
572
+ #allowedGroupNumbers;
545
573
  #createSession;
546
574
  #status = createWhatsappRuntimeStatus();
547
575
  #abortController = null;
@@ -590,6 +618,7 @@ export class WhatsappRuntime {
590
618
  const policy = normalizeWhatsappAccessPolicy(value);
591
619
  this.#accessMode = policy.accessMode;
592
620
  this.#allowedPrivateNumbers = new Set(policy.allowedNumbers);
621
+ this.#allowedGroupNumbers = new Set(policy.groupAllowedNumbers);
593
622
  this.#config = { ...this.#config, ...policy };
594
623
  return policy;
595
624
  }
@@ -624,7 +653,13 @@ export class WhatsappRuntime {
624
653
  { code: 'relink-required' },
625
654
  )),
626
655
  onMessage: async (raw, context) => {
656
+ const linkedAccount = context?.socket?.user;
627
657
  const message = normalizeWhatsappMessage(raw, this.#config.accountJid, {
658
+ accountAliases: [
659
+ linkedAccount?.id,
660
+ linkedAccount?.lid,
661
+ linkedAccount?.phoneNumber,
662
+ ],
628
663
  download: createWhatsappMediaDownloader({
629
664
  socket: context?.socket,
630
665
  logger: this.#logger,
@@ -635,6 +670,7 @@ export class WhatsappRuntime {
635
670
  if (!whatsappInboundAllowed(message, {
636
671
  accessMode: this.#accessMode,
637
672
  allowedNumbers: this.#allowedPrivateNumbers,
673
+ groupAllowedNumbers: this.#allowedGroupNumbers,
638
674
  })) {
639
675
  this.#status.messagesRejected += 1;
640
676
  this.#status.lastRejectedAt = new Date().toISOString();