@lofa199419/waha-v2 2.1.0 → 3.0.0
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/bin/waha_cli.py +371 -0
- package/package.json +1 -1
- package/src/client.ts +58 -0
- package/src/gateway.ts +10 -0
- package/src/identity.ts +674 -0
- package/src/webhook.ts +37 -7
package/bin/waha_cli.py
CHANGED
|
@@ -17,6 +17,11 @@ DEFAULT_MAX_CHUNK_LENGTH = 1500
|
|
|
17
17
|
SEEN_DELAY_MS = 3000
|
|
18
18
|
MIN_DELAY_MS = 600
|
|
19
19
|
MAX_DELAY_MS = 8000
|
|
20
|
+
IDENTITY_CONTACT_PAGE_SIZE = 200
|
|
21
|
+
IDENTITY_CONTACT_MAX_PAGES = 10
|
|
22
|
+
IDENTITY_CHAT_LIMIT = 50
|
|
23
|
+
IDENTITY_MESSAGE_LIMIT = 5
|
|
24
|
+
IDENTITY_NOISY_MESSAGE_TYPES = {"ciphertext", "e2e_notification", "notification_template"}
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
def env(name: str, default: str | None = None) -> str | None:
|
|
@@ -31,6 +36,11 @@ def state_dir() -> pathlib.Path:
|
|
|
31
36
|
return home / ".openclaw"
|
|
32
37
|
|
|
33
38
|
|
|
39
|
+
def identity_index_path(session_name: str) -> pathlib.Path:
|
|
40
|
+
safe = "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in (session_name or "default"))
|
|
41
|
+
return state_dir() / "extensions" / OPENCLAW_CHANNEL_ID / "identity" / f"{safe}.json"
|
|
42
|
+
|
|
43
|
+
|
|
34
44
|
def parse_dotenv(path: pathlib.Path) -> dict[str, str]:
|
|
35
45
|
values: dict[str, str] = {}
|
|
36
46
|
if not path.exists():
|
|
@@ -201,6 +211,166 @@ def send_presence(endpoint: str, session: str, chat_id: str) -> None:
|
|
|
201
211
|
waha_request("POST", endpoint, {"session": session, "chatId": chat_id}, session_name=session)
|
|
202
212
|
|
|
203
213
|
|
|
214
|
+
def normalize_raw_id(value) -> str | None:
|
|
215
|
+
if value is None:
|
|
216
|
+
return None
|
|
217
|
+
if isinstance(value, str):
|
|
218
|
+
raw = value.strip().lower()
|
|
219
|
+
return raw or None
|
|
220
|
+
if isinstance(value, dict):
|
|
221
|
+
serialized = value.get("_serialized")
|
|
222
|
+
if isinstance(serialized, str):
|
|
223
|
+
raw = serialized.strip().lower()
|
|
224
|
+
return raw or None
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def extract_digits(value: str | None) -> str | None:
|
|
229
|
+
digits = "".join(ch for ch in str(value or "") if ch.isdigit())
|
|
230
|
+
return digits or None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def to_chat_phone_id(value: str | None) -> str | None:
|
|
234
|
+
digits = extract_digits(value)
|
|
235
|
+
return f"{digits}@c.us" if digits else None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def parse_jid(value) -> dict | None:
|
|
239
|
+
raw = normalize_raw_id(value)
|
|
240
|
+
if not raw:
|
|
241
|
+
return None
|
|
242
|
+
left, at, server = raw.partition("@")
|
|
243
|
+
user, colon, device = left.partition(":")
|
|
244
|
+
bare = f"{user}@{server}" if at else raw
|
|
245
|
+
kind = "unknown"
|
|
246
|
+
if server == "g.us":
|
|
247
|
+
kind = "group"
|
|
248
|
+
elif server == "broadcast":
|
|
249
|
+
kind = "broadcast"
|
|
250
|
+
elif colon:
|
|
251
|
+
kind = "device"
|
|
252
|
+
elif server == "lid":
|
|
253
|
+
kind = "lid"
|
|
254
|
+
elif server in {"c.us", "s.whatsapp.net"}:
|
|
255
|
+
kind = "direct"
|
|
256
|
+
return {
|
|
257
|
+
"raw": raw,
|
|
258
|
+
"bare": bare,
|
|
259
|
+
"server": server or None,
|
|
260
|
+
"user": user or None,
|
|
261
|
+
"device": device or None,
|
|
262
|
+
"kind": kind,
|
|
263
|
+
"phoneE164": extract_digits(user) if server in {"c.us", "s.whatsapp.net"} else None,
|
|
264
|
+
"lid": bare if server == "lid" else None,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def add_alias(target: set[str], raw_value) -> None:
|
|
269
|
+
parsed = parse_jid(raw_value)
|
|
270
|
+
if not parsed:
|
|
271
|
+
return
|
|
272
|
+
target.add(parsed["raw"])
|
|
273
|
+
target.add(parsed["bare"])
|
|
274
|
+
if parsed.get("phoneE164"):
|
|
275
|
+
target.add(f"{parsed['phoneE164']}@c.us")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def merge_preferred_text(current: str | None, nxt: str | None) -> str | None:
|
|
279
|
+
nxt = str(nxt or "").strip()
|
|
280
|
+
if not nxt:
|
|
281
|
+
return current
|
|
282
|
+
current = str(current or "").strip()
|
|
283
|
+
return current or nxt
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def merge_preferred_phone(current: str | None, nxt: str | None) -> str | None:
|
|
287
|
+
return current or extract_digits(nxt)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def extract_name(record) -> str | None:
|
|
291
|
+
if not isinstance(record, dict):
|
|
292
|
+
return None
|
|
293
|
+
business = record.get("businessProfile")
|
|
294
|
+
candidates = [
|
|
295
|
+
record.get("name"),
|
|
296
|
+
record.get("pushname"),
|
|
297
|
+
record.get("shortName"),
|
|
298
|
+
record.get("verifiedName"),
|
|
299
|
+
business.get("verifiedName") if isinstance(business, dict) else None,
|
|
300
|
+
record.get("subject"),
|
|
301
|
+
]
|
|
302
|
+
for candidate in candidates:
|
|
303
|
+
text = str(candidate or "").strip()
|
|
304
|
+
if text:
|
|
305
|
+
return text
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def pick_message_body(record: dict) -> str | None:
|
|
310
|
+
direct = str(record.get("body") or "").strip()
|
|
311
|
+
if direct:
|
|
312
|
+
return direct
|
|
313
|
+
nested = record.get("_data")
|
|
314
|
+
if isinstance(nested, dict):
|
|
315
|
+
nested_body = str(nested.get("body") or "").strip()
|
|
316
|
+
if nested_body:
|
|
317
|
+
return nested_body
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def sanitize_message_body(body: str | None, message_type: str | None) -> str | None:
|
|
322
|
+
text = str(body or "").strip()
|
|
323
|
+
if not text:
|
|
324
|
+
return None
|
|
325
|
+
compact = "".join(text.split())
|
|
326
|
+
binary_like = (
|
|
327
|
+
len(compact) > 160
|
|
328
|
+
and len(compact) % 4 == 0
|
|
329
|
+
and all(ch.isalnum() or ch in "+/=" for ch in compact)
|
|
330
|
+
and (compact.startswith("/9j/") or compact.startswith("iVBOR") or compact.startswith("UklGR"))
|
|
331
|
+
)
|
|
332
|
+
if binary_like:
|
|
333
|
+
return None
|
|
334
|
+
if str(message_type or "").strip().lower() in IDENTITY_NOISY_MESSAGE_TYPES:
|
|
335
|
+
return None
|
|
336
|
+
return text
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def summarize_recent_messages(values, self_aliases: set[str]) -> list[dict]:
|
|
340
|
+
summaries: list[dict] = []
|
|
341
|
+
if not isinstance(values, list):
|
|
342
|
+
return summaries
|
|
343
|
+
for value in values:
|
|
344
|
+
if not isinstance(value, dict):
|
|
345
|
+
continue
|
|
346
|
+
nested = value.get("_data")
|
|
347
|
+
nested = nested if isinstance(nested, dict) else {}
|
|
348
|
+
raw_from = normalize_raw_id(value.get("from") or nested.get("from"))
|
|
349
|
+
raw_to = normalize_raw_id(value.get("to") or nested.get("to"))
|
|
350
|
+
raw_type = str(value.get("type") or nested.get("type") or "").strip() or None
|
|
351
|
+
timestamp = value.get("timestamp")
|
|
352
|
+
if not isinstance(timestamp, int):
|
|
353
|
+
timestamp = nested.get("t") if isinstance(nested.get("t"), int) else None
|
|
354
|
+
from_me = value.get("fromMe") is True
|
|
355
|
+
direction = "out" if from_me or (raw_from and (raw_from in self_aliases or (parse_jid(raw_from) or {}).get("bare") in self_aliases)) else "in"
|
|
356
|
+
body = sanitize_message_body(pick_message_body(value), raw_type)
|
|
357
|
+
if str(raw_type or "").lower() in IDENTITY_NOISY_MESSAGE_TYPES and not body:
|
|
358
|
+
continue
|
|
359
|
+
summaries.append(
|
|
360
|
+
{
|
|
361
|
+
"id": str(value.get("id") or "").strip() or None,
|
|
362
|
+
"timestamp": timestamp,
|
|
363
|
+
"direction": direction,
|
|
364
|
+
"from": raw_from,
|
|
365
|
+
"to": raw_to,
|
|
366
|
+
"body": body,
|
|
367
|
+
"type": raw_type,
|
|
368
|
+
"hasMedia": value.get("hasMedia") is True,
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
return summaries
|
|
372
|
+
|
|
373
|
+
|
|
204
374
|
def waha_request(method: str, path: str, body: dict | None = None, query: dict | None = None, session_name: str | None = None):
|
|
205
375
|
base_url = normalize_base_url(require_resolved("WAHA_URL", session_name))
|
|
206
376
|
api_key = require_resolved("WAHA_API_KEY", session_name)
|
|
@@ -270,6 +440,7 @@ Chats and Messages:
|
|
|
270
440
|
waha-get-chats [--session NAME] [--limit N] [--offset N]
|
|
271
441
|
waha-get-messages --chat-id ID [--session NAME] [--limit N] [--offset N]
|
|
272
442
|
waha-send-text --chat-id ID --text TEXT [--session NAME]
|
|
443
|
+
waha-rebuild-identity-index [--session NAME]
|
|
273
444
|
|
|
274
445
|
Optional env:
|
|
275
446
|
WAHA_SESSION_DEFAULT / WAHA_DEFAULT_SESSION
|
|
@@ -477,6 +648,203 @@ def cmd_send_text(args):
|
|
|
477
648
|
print(f"Chunk {index} Message ID: {message_id}")
|
|
478
649
|
|
|
479
650
|
|
|
651
|
+
def cmd_rebuild_identity_index(args):
|
|
652
|
+
session = args.session
|
|
653
|
+
lid_rows = waha_request(
|
|
654
|
+
"GET",
|
|
655
|
+
f"/api/{urllib.parse.quote(session, safe='')}/lids",
|
|
656
|
+
session_name=session,
|
|
657
|
+
)
|
|
658
|
+
lid_rows = lid_rows if isinstance(lid_rows, list) else []
|
|
659
|
+
lid_mappings: dict[str, str] = {}
|
|
660
|
+
phone_to_lid: dict[str, str] = {}
|
|
661
|
+
for row in lid_rows:
|
|
662
|
+
if not isinstance(row, dict):
|
|
663
|
+
continue
|
|
664
|
+
lid = normalize_raw_id(row.get("lid"))
|
|
665
|
+
pn = normalize_raw_id(row.get("pn")) or to_chat_phone_id(str(row.get("pn") or ""))
|
|
666
|
+
if not lid or not pn:
|
|
667
|
+
continue
|
|
668
|
+
lid_mappings[lid] = pn
|
|
669
|
+
phone_to_lid[pn] = lid
|
|
670
|
+
|
|
671
|
+
contacts: list[dict] = []
|
|
672
|
+
for page in range(IDENTITY_CONTACT_MAX_PAGES):
|
|
673
|
+
offset = page * IDENTITY_CONTACT_PAGE_SIZE
|
|
674
|
+
batch = waha_request(
|
|
675
|
+
"GET",
|
|
676
|
+
"/api/contacts/all",
|
|
677
|
+
query={"session": session, "limit": IDENTITY_CONTACT_PAGE_SIZE, "offset": offset},
|
|
678
|
+
session_name=session,
|
|
679
|
+
)
|
|
680
|
+
if not isinstance(batch, list) or not batch:
|
|
681
|
+
break
|
|
682
|
+
contacts.extend(item for item in batch if isinstance(item, dict))
|
|
683
|
+
if len(batch) < IDENTITY_CONTACT_PAGE_SIZE:
|
|
684
|
+
break
|
|
685
|
+
|
|
686
|
+
self_aliases: set[str] = set()
|
|
687
|
+
self_display_name = None
|
|
688
|
+
self_phone = None
|
|
689
|
+
for contact in contacts:
|
|
690
|
+
if contact.get("isMe") is not True:
|
|
691
|
+
continue
|
|
692
|
+
add_alias(self_aliases, contact.get("id"))
|
|
693
|
+
self_phone = merge_preferred_phone(self_phone, str(contact.get("number") or ""))
|
|
694
|
+
self_display_name = merge_preferred_text(self_display_name, extract_name(contact))
|
|
695
|
+
|
|
696
|
+
peers: dict[str, dict] = {}
|
|
697
|
+
lookup: dict[str, str] = {}
|
|
698
|
+
canonical: dict[str, str] = {}
|
|
699
|
+
|
|
700
|
+
def upsert_peer(chat_id: str, patch: dict, alias_ids: set[str] | None = None) -> None:
|
|
701
|
+
parsed = parse_jid(chat_id) or {}
|
|
702
|
+
existing = peers.get(chat_id, {
|
|
703
|
+
"chatId": chat_id,
|
|
704
|
+
"kind": parsed.get("kind", "unknown"),
|
|
705
|
+
"aliases": [],
|
|
706
|
+
"recentMessages": [],
|
|
707
|
+
"updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
708
|
+
})
|
|
709
|
+
alias_set = set(existing.get("aliases", []))
|
|
710
|
+
add_alias(alias_set, chat_id)
|
|
711
|
+
for alias in alias_ids or set():
|
|
712
|
+
add_alias(alias_set, alias)
|
|
713
|
+
next_peer = dict(existing)
|
|
714
|
+
next_peer.update(patch)
|
|
715
|
+
next_peer["kind"] = patch.get("kind", existing.get("kind", "unknown"))
|
|
716
|
+
next_peer["displayName"] = merge_preferred_text(existing.get("displayName"), patch.get("displayName"))
|
|
717
|
+
next_peer["verifiedName"] = merge_preferred_text(existing.get("verifiedName"), patch.get("verifiedName"))
|
|
718
|
+
next_peer["phoneE164"] = merge_preferred_phone(existing.get("phoneE164"), patch.get("phoneE164"))
|
|
719
|
+
next_peer["lid"] = merge_preferred_text(existing.get("lid"), patch.get("lid"))
|
|
720
|
+
next_peer["aliases"] = sorted(alias_set)
|
|
721
|
+
next_peer["recentMessages"] = patch.get("recentMessages", existing.get("recentMessages", []))
|
|
722
|
+
next_peer["lastMessageAt"] = patch.get("lastMessageAt", existing.get("lastMessageAt"))
|
|
723
|
+
next_peer["updatedAt"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
724
|
+
peers[chat_id] = next_peer
|
|
725
|
+
for alias in next_peer["aliases"]:
|
|
726
|
+
lookup[alias] = chat_id
|
|
727
|
+
parsed_alias = parse_jid(alias) or {}
|
|
728
|
+
if parsed_alias.get("kind") == "lid":
|
|
729
|
+
mapped_phone = lid_mappings.get(parsed_alias.get("bare"))
|
|
730
|
+
if mapped_phone:
|
|
731
|
+
canonical[alias] = mapped_phone
|
|
732
|
+
elif parsed_alias.get("kind") == "direct" and parsed_alias.get("phoneE164"):
|
|
733
|
+
canonical[alias] = f"{parsed_alias['phoneE164']}@c.us"
|
|
734
|
+
lookup[chat_id] = chat_id
|
|
735
|
+
if next_peer.get("phoneE164"):
|
|
736
|
+
canonical[f"{next_peer['phoneE164']}@c.us"] = f"{next_peer['phoneE164']}@c.us"
|
|
737
|
+
if next_peer.get("lid") and lid_mappings.get(next_peer["lid"]):
|
|
738
|
+
canonical[next_peer["lid"]] = lid_mappings[next_peer["lid"]]
|
|
739
|
+
|
|
740
|
+
for contact in contacts:
|
|
741
|
+
contact_id = normalize_raw_id(contact.get("id"))
|
|
742
|
+
if not contact_id:
|
|
743
|
+
continue
|
|
744
|
+
parsed = parse_jid(contact_id) or {}
|
|
745
|
+
aliases: set[str] = set()
|
|
746
|
+
add_alias(aliases, contact_id)
|
|
747
|
+
business = contact.get("businessProfile")
|
|
748
|
+
if isinstance(business, dict):
|
|
749
|
+
add_alias(aliases, business.get("id"))
|
|
750
|
+
phone_digits = extract_digits(str(contact.get("number") or "")) if parsed.get("kind") == "direct" else None
|
|
751
|
+
if phone_digits:
|
|
752
|
+
aliases.add(f"{phone_digits}@c.us")
|
|
753
|
+
upsert_peer(
|
|
754
|
+
contact_id,
|
|
755
|
+
{
|
|
756
|
+
"kind": parsed.get("kind", "unknown"),
|
|
757
|
+
"displayName": extract_name(contact),
|
|
758
|
+
"verifiedName": str(contact.get("verifiedName") or "").strip() or None,
|
|
759
|
+
"phoneE164": parsed.get("phoneE164") or phone_digits,
|
|
760
|
+
"lid": parsed.get("lid"),
|
|
761
|
+
},
|
|
762
|
+
aliases,
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
chats = waha_request(
|
|
766
|
+
"GET",
|
|
767
|
+
f"/api/{urllib.parse.quote(session, safe='')}/chats",
|
|
768
|
+
query={"limit": IDENTITY_CHAT_LIMIT, "offset": 0},
|
|
769
|
+
session_name=session,
|
|
770
|
+
)
|
|
771
|
+
chats = chats if isinstance(chats, list) else []
|
|
772
|
+
for chat in chats:
|
|
773
|
+
if not isinstance(chat, dict):
|
|
774
|
+
continue
|
|
775
|
+
chat_id = normalize_raw_id(chat.get("id") or chat.get("chatId"))
|
|
776
|
+
if not chat_id:
|
|
777
|
+
continue
|
|
778
|
+
parsed_chat = parse_jid(chat_id) or {}
|
|
779
|
+
messages = waha_request(
|
|
780
|
+
"GET",
|
|
781
|
+
f"/api/{urllib.parse.quote(session, safe='')}/chats/{urllib.parse.quote(chat_id, safe='')}/messages",
|
|
782
|
+
query={"limit": IDENTITY_MESSAGE_LIMIT},
|
|
783
|
+
session_name=session,
|
|
784
|
+
)
|
|
785
|
+
summaries = summarize_recent_messages(messages if isinstance(messages, list) else [], self_aliases)
|
|
786
|
+
alias_set: set[str] = set()
|
|
787
|
+
add_alias(alias_set, chat_id)
|
|
788
|
+
resolved_phone = parsed_chat.get("phoneE164")
|
|
789
|
+
resolved_lid = parsed_chat.get("lid")
|
|
790
|
+
if parsed_chat.get("lid") and lid_mappings.get(parsed_chat["lid"]):
|
|
791
|
+
resolved_phone = merge_preferred_phone(resolved_phone, lid_mappings.get(parsed_chat["lid"]))
|
|
792
|
+
if parsed_chat.get("kind") == "direct" and parsed_chat.get("phoneE164"):
|
|
793
|
+
direct_chat_id = f"{parsed_chat['phoneE164']}@c.us"
|
|
794
|
+
if direct_chat_id in phone_to_lid:
|
|
795
|
+
resolved_lid = merge_preferred_text(resolved_lid, phone_to_lid[direct_chat_id])
|
|
796
|
+
for message in summaries:
|
|
797
|
+
counterpart_id = message.get("to") if message.get("direction") == "out" else message.get("from")
|
|
798
|
+
counterpart_raw = normalize_raw_id(counterpart_id)
|
|
799
|
+
counterpart_parsed = parse_jid(counterpart_id) or {}
|
|
800
|
+
if counterpart_raw and counterpart_raw not in self_aliases and counterpart_parsed.get("bare") not in self_aliases:
|
|
801
|
+
add_alias(alias_set, counterpart_raw)
|
|
802
|
+
resolved_phone = merge_preferred_phone(resolved_phone, counterpart_parsed.get("phoneE164"))
|
|
803
|
+
resolved_lid = merge_preferred_text(resolved_lid, counterpart_parsed.get("lid"))
|
|
804
|
+
if counterpart_parsed.get("lid") and lid_mappings.get(counterpart_parsed["lid"]):
|
|
805
|
+
resolved_phone = merge_preferred_phone(resolved_phone, lid_mappings.get(counterpart_parsed["lid"]))
|
|
806
|
+
if counterpart_parsed.get("kind") == "direct" and counterpart_parsed.get("phoneE164"):
|
|
807
|
+
direct_counterpart_id = f"{counterpart_parsed['phoneE164']}@c.us"
|
|
808
|
+
if direct_counterpart_id in phone_to_lid:
|
|
809
|
+
resolved_lid = merge_preferred_text(resolved_lid, phone_to_lid[direct_counterpart_id])
|
|
810
|
+
upsert_peer(
|
|
811
|
+
chat_id,
|
|
812
|
+
{
|
|
813
|
+
"kind": "group" if chat.get("isGroup") is True else parsed_chat.get("kind", "unknown"),
|
|
814
|
+
"displayName": extract_name(chat),
|
|
815
|
+
"phoneE164": resolved_phone,
|
|
816
|
+
"lid": resolved_lid,
|
|
817
|
+
"recentMessages": summaries,
|
|
818
|
+
"lastMessageAt": summaries[0]["timestamp"] if summaries else None,
|
|
819
|
+
},
|
|
820
|
+
alias_set,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
index = {
|
|
824
|
+
"version": 1,
|
|
825
|
+
"accountId": session,
|
|
826
|
+
"session": session,
|
|
827
|
+
"refreshedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
828
|
+
"stateDir": str(state_dir()),
|
|
829
|
+
"self": {
|
|
830
|
+
"displayName": self_display_name,
|
|
831
|
+
"phoneE164": self_phone,
|
|
832
|
+
"aliases": sorted(self_aliases),
|
|
833
|
+
} if self_aliases or self_display_name or self_phone else None,
|
|
834
|
+
"peers": dict(sorted(peers.items())),
|
|
835
|
+
"lookup": dict(sorted(lookup.items())),
|
|
836
|
+
"canonical": dict(sorted(canonical.items())),
|
|
837
|
+
"lidMappings": dict(sorted(lid_mappings.items())),
|
|
838
|
+
}
|
|
839
|
+
path = identity_index_path(session)
|
|
840
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
841
|
+
path.write_text(json.dumps(index, indent=2, ensure_ascii=False) + "\n")
|
|
842
|
+
print(f"Identity index rebuilt for session '{session}'.")
|
|
843
|
+
print(f"Path: {path}")
|
|
844
|
+
print(f"Peers: {len(peers)}")
|
|
845
|
+
print(f"LID mappings: {len(lid_mappings)}")
|
|
846
|
+
|
|
847
|
+
|
|
480
848
|
COMMANDS = {
|
|
481
849
|
"waha-list-sessions": ("List sessions", cmd_list_sessions),
|
|
482
850
|
"waha-get-session": ("Get session", cmd_get_session),
|
|
@@ -493,6 +861,7 @@ COMMANDS = {
|
|
|
493
861
|
"waha-list-chats": ("List chats", cmd_get_chats),
|
|
494
862
|
"waha-get-messages": ("Get messages", cmd_get_messages),
|
|
495
863
|
"waha-send-text": ("Send text", cmd_send_text),
|
|
864
|
+
"waha-rebuild-identity-index": ("Rebuild identity index", cmd_rebuild_identity_index),
|
|
496
865
|
}
|
|
497
866
|
|
|
498
867
|
|
|
@@ -518,6 +887,8 @@ def command_parser(command: str):
|
|
|
518
887
|
add_session_arg(parser)
|
|
519
888
|
parser.add_argument("--chat-id", required=True)
|
|
520
889
|
parser.add_argument("--text", required=True)
|
|
890
|
+
elif command == "waha-rebuild-identity-index":
|
|
891
|
+
add_session_arg(parser)
|
|
521
892
|
return parser
|
|
522
893
|
|
|
523
894
|
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -25,6 +25,11 @@ export type WahaV2PollPayload = {
|
|
|
25
25
|
multipleAnswers?: boolean;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
export type WahaV2LidMapping = {
|
|
29
|
+
lid?: string;
|
|
30
|
+
pn?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
28
33
|
export class WahaV2Client {
|
|
29
34
|
private readonly inner: WahaClient;
|
|
30
35
|
private readonly _config: WahaV2ClientConfig;
|
|
@@ -107,6 +112,59 @@ export class WahaV2Client {
|
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
async listLidMappings(session: string): Promise<WahaV2LidMapping[]> {
|
|
116
|
+
const url = `${this._config.baseUrl}/api/${encodeURIComponent(session)}/lids`;
|
|
117
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
118
|
+
if (this._config.apiKey) headers["X-Api-Key"] = this._config.apiKey;
|
|
119
|
+
const response = await fetch(url, { method: "GET", headers });
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
const body = await response.text().catch(() => "");
|
|
122
|
+
const suffix = body ? ` body=${body.slice(0, 240)}` : "";
|
|
123
|
+
throw new Error(`WAHA /lids lookup failed: ${response.status} ${response.statusText}${suffix}`);
|
|
124
|
+
}
|
|
125
|
+
const json = (await response.json().catch(() => [])) as unknown;
|
|
126
|
+
return Array.isArray(json) ? (json as WahaV2LidMapping[]) : [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getPhoneNumberByLid(session: string, lid: string): Promise<WahaV2LidMapping | undefined> {
|
|
130
|
+
const url = `${this._config.baseUrl}/api/${encodeURIComponent(session)}/lids/${encodeURIComponent(lid)}`;
|
|
131
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
132
|
+
if (this._config.apiKey) headers["X-Api-Key"] = this._config.apiKey;
|
|
133
|
+
const response = await fetch(url, { method: "GET", headers });
|
|
134
|
+
if (response.status === 404) return undefined;
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
const body = await response.text().catch(() => "");
|
|
137
|
+
const suffix = body ? ` body=${body.slice(0, 240)}` : "";
|
|
138
|
+
throw new Error(
|
|
139
|
+
`WAHA /lids/{lid} lookup failed: ${response.status} ${response.statusText}${suffix}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return (await response.json().catch(() => undefined)) as WahaV2LidMapping | undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getLidByPhoneNumber(
|
|
146
|
+
session: string,
|
|
147
|
+
phoneNumberOrChatId: string,
|
|
148
|
+
): Promise<WahaV2LidMapping | undefined> {
|
|
149
|
+
const normalized = String(phoneNumberOrChatId ?? "").trim().toLowerCase();
|
|
150
|
+
const phoneNumber = normalized.endsWith("@c.us")
|
|
151
|
+
? normalized.slice(0, -5)
|
|
152
|
+
: normalized.replace(/\D+/g, "");
|
|
153
|
+
const url = `${this._config.baseUrl}/api/${encodeURIComponent(session)}/lids/pn/${encodeURIComponent(phoneNumber)}`;
|
|
154
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
155
|
+
if (this._config.apiKey) headers["X-Api-Key"] = this._config.apiKey;
|
|
156
|
+
const response = await fetch(url, { method: "GET", headers });
|
|
157
|
+
if (response.status === 404) return undefined;
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
const body = await response.text().catch(() => "");
|
|
160
|
+
const suffix = body ? ` body=${body.slice(0, 240)}` : "";
|
|
161
|
+
throw new Error(
|
|
162
|
+
`WAHA /lids/pn/{phone} lookup failed: ${response.status} ${response.statusText}${suffix}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return (await response.json().catch(() => undefined)) as WahaV2LidMapping | undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
110
168
|
// ---------------------------------------------------------------------------
|
|
111
169
|
// Session management
|
|
112
170
|
// ---------------------------------------------------------------------------
|
package/src/gateway.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChannelGatewayAdapter } from "openclaw/plugin-sdk";
|
|
2
2
|
import { createWahaV2Client } from "./client.js";
|
|
3
|
+
import { warmWahaV2IdentityIndex } from "./identity.js";
|
|
3
4
|
import { probeWahaV2Session } from "./probe.js";
|
|
4
5
|
import { getWahaV2Logger } from "./runtime.js";
|
|
5
6
|
import { removeWahaV2Client, setWahaV2Client } from "./runtime.js";
|
|
@@ -132,11 +133,20 @@ export const wahaV2Gateway: ChannelGatewayAdapter<ResolvedWahaV2Account> = {
|
|
|
132
133
|
`baseUrl=${account.baseUrl} session=${account.session} connected=${probe.ok}`,
|
|
133
134
|
);
|
|
134
135
|
|
|
136
|
+
if (probe.ok && !abortSignal.aborted) {
|
|
137
|
+
void warmWahaV2IdentityIndex(client, account, "startup").catch(() => {});
|
|
138
|
+
}
|
|
139
|
+
|
|
135
140
|
// Health monitoring — re-probe every 60 s and update connected state.
|
|
141
|
+
let lastConnected = probe.ok;
|
|
136
142
|
let healthInterval: ReturnType<typeof setInterval> | null = setInterval(async () => {
|
|
137
143
|
if (abortSignal.aborted) return;
|
|
138
144
|
const health = await probeWahaV2Session(client, account.session).catch(() => ({ ok: false }));
|
|
139
145
|
setStatus({ accountId: account.accountId, running: true, connected: health.ok });
|
|
146
|
+
if (health.ok && !lastConnected) {
|
|
147
|
+
void warmWahaV2IdentityIndex(client, account, "reconnected").catch(() => {});
|
|
148
|
+
}
|
|
149
|
+
lastConnected = health.ok;
|
|
140
150
|
}, HEALTH_INTERVAL_MS);
|
|
141
151
|
|
|
142
152
|
abortSignal.addEventListener(
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import type { WahaV2Client, WahaV2LidMapping } from "./client.js";
|
|
5
|
+
import { getWahaV2Logger } from "./runtime.js";
|
|
6
|
+
import type { ResolvedWahaV2Account } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const IDENTITY_INDEX_VERSION = 1;
|
|
9
|
+
const CONTACT_PAGE_SIZE = 200;
|
|
10
|
+
const CONTACT_MAX_PAGES = 10;
|
|
11
|
+
const CHAT_SYNC_LIMIT = 50;
|
|
12
|
+
const RECENT_MESSAGE_LIMIT = 5;
|
|
13
|
+
const NOISY_MESSAGE_TYPES = new Set(["ciphertext", "e2e_notification", "notification_template"]);
|
|
14
|
+
|
|
15
|
+
type WahaPeerKind = "direct" | "group" | "lid" | "device" | "broadcast" | "unknown";
|
|
16
|
+
|
|
17
|
+
type WahaParsedJid = {
|
|
18
|
+
raw: string;
|
|
19
|
+
bare: string;
|
|
20
|
+
server?: string;
|
|
21
|
+
user?: string;
|
|
22
|
+
device?: string;
|
|
23
|
+
kind: WahaPeerKind;
|
|
24
|
+
phoneE164?: string;
|
|
25
|
+
lid?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type WahaV2IdentityMessageSummary = {
|
|
29
|
+
id?: string;
|
|
30
|
+
timestamp?: number;
|
|
31
|
+
direction: "in" | "out";
|
|
32
|
+
from?: string;
|
|
33
|
+
to?: string;
|
|
34
|
+
body?: string;
|
|
35
|
+
type?: string;
|
|
36
|
+
hasMedia?: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type WahaV2IdentityPeerRecord = {
|
|
40
|
+
chatId: string;
|
|
41
|
+
kind: WahaPeerKind;
|
|
42
|
+
displayName?: string;
|
|
43
|
+
verifiedName?: string;
|
|
44
|
+
phoneE164?: string;
|
|
45
|
+
lid?: string;
|
|
46
|
+
aliases: string[];
|
|
47
|
+
recentMessages: WahaV2IdentityMessageSummary[];
|
|
48
|
+
lastMessageAt?: number;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type WahaV2IdentityIndex = {
|
|
53
|
+
version: number;
|
|
54
|
+
accountId: string;
|
|
55
|
+
session: string;
|
|
56
|
+
refreshedAt: string;
|
|
57
|
+
stateDir: string;
|
|
58
|
+
self?: {
|
|
59
|
+
displayName?: string;
|
|
60
|
+
phoneE164?: string;
|
|
61
|
+
aliases: string[];
|
|
62
|
+
};
|
|
63
|
+
peers: Record<string, WahaV2IdentityPeerRecord>;
|
|
64
|
+
lookup: Record<string, string>;
|
|
65
|
+
canonical: Record<string, string>;
|
|
66
|
+
lidMappings: Record<string, string>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type WahaV2ResolvedIdentity = {
|
|
70
|
+
chat?: WahaV2IdentityPeerRecord;
|
|
71
|
+
sender?: WahaV2IdentityPeerRecord;
|
|
72
|
+
canonicalChatId?: string;
|
|
73
|
+
canonicalSenderId?: string;
|
|
74
|
+
indexPath: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const identityCache = new Map<string, WahaV2IdentityIndex>();
|
|
78
|
+
const warmInFlight = new Map<string, Promise<WahaV2IdentityIndex>>();
|
|
79
|
+
|
|
80
|
+
function cacheKey(account: { accountId: string; session: string }): string {
|
|
81
|
+
return `${account.accountId}:${account.session}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sanitizeFileComponent(value: string): string {
|
|
85
|
+
const cleaned = String(value ?? "").trim();
|
|
86
|
+
return (cleaned || "default").replace(/[^a-z0-9._-]+/gi, "_");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function extractDigits(value: string | undefined): string | undefined {
|
|
90
|
+
const digits = String(value ?? "").replace(/\D+/g, "");
|
|
91
|
+
return digits || undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeRawId(value: unknown): string | undefined {
|
|
95
|
+
if (!value) return undefined;
|
|
96
|
+
if (typeof value === "string") {
|
|
97
|
+
const normalized = value.trim().toLowerCase();
|
|
98
|
+
return normalized || undefined;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === "object") {
|
|
101
|
+
const serialized = (value as { _serialized?: unknown })._serialized;
|
|
102
|
+
if (typeof serialized === "string") {
|
|
103
|
+
const normalized = serialized.trim().toLowerCase();
|
|
104
|
+
return normalized || undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function toChatPhoneId(value: string | undefined): string | undefined {
|
|
111
|
+
const digits = extractDigits(value);
|
|
112
|
+
return digits ? `${digits}@c.us` : undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseJid(rawValue: unknown): WahaParsedJid | undefined {
|
|
116
|
+
const raw = normalizeRawId(rawValue);
|
|
117
|
+
if (!raw) return undefined;
|
|
118
|
+
|
|
119
|
+
const [left, server] = raw.split("@", 2);
|
|
120
|
+
const [user, device] = String(left ?? "").split(":", 2);
|
|
121
|
+
const bare = server ? `${user}@${server}` : raw;
|
|
122
|
+
const baseDigits = extractDigits(user);
|
|
123
|
+
|
|
124
|
+
let kind: WahaPeerKind = "unknown";
|
|
125
|
+
if (server === "g.us") kind = "group";
|
|
126
|
+
else if (server === "broadcast") kind = "broadcast";
|
|
127
|
+
else if (device) kind = "device";
|
|
128
|
+
else if (server === "lid") kind = "lid";
|
|
129
|
+
else if (server === "c.us" || server === "s.whatsapp.net") kind = "direct";
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
raw,
|
|
133
|
+
bare,
|
|
134
|
+
server,
|
|
135
|
+
user: user || undefined,
|
|
136
|
+
device: device || undefined,
|
|
137
|
+
kind,
|
|
138
|
+
phoneE164: server === "c.us" || server === "s.whatsapp.net" ? baseDigits : undefined,
|
|
139
|
+
lid: server === "lid" ? bare : undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractName(value: unknown): string | undefined {
|
|
144
|
+
if (!value || typeof value !== "object") return undefined;
|
|
145
|
+
const record = value as Record<string, unknown>;
|
|
146
|
+
const candidates = [
|
|
147
|
+
record.name,
|
|
148
|
+
record.pushname,
|
|
149
|
+
record.shortName,
|
|
150
|
+
record.verifiedName,
|
|
151
|
+
(record.businessProfile as Record<string, unknown> | undefined)?.verifiedName,
|
|
152
|
+
record.subject,
|
|
153
|
+
];
|
|
154
|
+
for (const candidate of candidates) {
|
|
155
|
+
const text = String(candidate ?? "").trim();
|
|
156
|
+
if (text) return text;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function pickMessageBody(value: Record<string, unknown>): string | undefined {
|
|
162
|
+
const direct = String(value.body ?? "").trim();
|
|
163
|
+
if (direct) return direct;
|
|
164
|
+
const nested = value._data as Record<string, unknown> | undefined;
|
|
165
|
+
const nestedBody = String(nested?.body ?? "").trim();
|
|
166
|
+
if (nestedBody) return nestedBody;
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sanitizeMessageBody(body: string | undefined, type?: string): string | undefined {
|
|
171
|
+
const text = String(body ?? "").trim();
|
|
172
|
+
if (!text) return undefined;
|
|
173
|
+
const compact = text.replace(/\s+/g, "");
|
|
174
|
+
const binaryLike =
|
|
175
|
+
compact.length > 160 &&
|
|
176
|
+
compact.length % 4 === 0 &&
|
|
177
|
+
/^[A-Za-z0-9+/=]+$/.test(compact) &&
|
|
178
|
+
(compact.startsWith("/9j/") || compact.startsWith("iVBOR") || compact.startsWith("UklGR"));
|
|
179
|
+
if (binaryLike) return undefined;
|
|
180
|
+
if (NOISY_MESSAGE_TYPES.has(String(type ?? "").trim().toLowerCase())) return undefined;
|
|
181
|
+
return text;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function summarizeRecentMessages(
|
|
185
|
+
values: unknown[],
|
|
186
|
+
selfAliases: Set<string>,
|
|
187
|
+
): WahaV2IdentityMessageSummary[] {
|
|
188
|
+
if (!Array.isArray(values)) return [];
|
|
189
|
+
const summaries: WahaV2IdentityMessageSummary[] = [];
|
|
190
|
+
for (const value of values) {
|
|
191
|
+
if (!value || typeof value !== "object") continue;
|
|
192
|
+
const record = value as Record<string, unknown>;
|
|
193
|
+
const nested = record._data as Record<string, unknown> | undefined;
|
|
194
|
+
const rawFrom = normalizeRawId(record.from ?? nested?.from);
|
|
195
|
+
const rawTo = normalizeRawId(record.to ?? nested?.to);
|
|
196
|
+
const fromMe = record.fromMe === true;
|
|
197
|
+
const timestamp =
|
|
198
|
+
typeof record.timestamp === "number"
|
|
199
|
+
? record.timestamp
|
|
200
|
+
: typeof nested?.t === "number"
|
|
201
|
+
? (nested.t as number)
|
|
202
|
+
: undefined;
|
|
203
|
+
const rawType = String(record.type ?? nested?.type ?? "").trim() || undefined;
|
|
204
|
+
const direction: "in" | "out" =
|
|
205
|
+
fromMe || (rawFrom ? selfAliases.has(rawFrom) || selfAliases.has(parseJid(rawFrom)?.bare ?? "") : false)
|
|
206
|
+
? "out"
|
|
207
|
+
: "in";
|
|
208
|
+
const sanitizedBody = sanitizeMessageBody(pickMessageBody(record), rawType);
|
|
209
|
+
if (NOISY_MESSAGE_TYPES.has(String(rawType ?? "").toLowerCase()) && !sanitizedBody) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
summaries.push({
|
|
214
|
+
id: String(record.id ?? "").trim() || undefined,
|
|
215
|
+
timestamp,
|
|
216
|
+
direction,
|
|
217
|
+
from: rawFrom,
|
|
218
|
+
to: rawTo,
|
|
219
|
+
body: sanitizedBody,
|
|
220
|
+
type: rawType,
|
|
221
|
+
hasMedia: record.hasMedia === true,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return summaries;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function addAlias(target: Set<string>, rawValue: unknown): void {
|
|
228
|
+
const parsed = parseJid(rawValue);
|
|
229
|
+
if (!parsed) return;
|
|
230
|
+
target.add(parsed.raw);
|
|
231
|
+
target.add(parsed.bare);
|
|
232
|
+
if (parsed.phoneE164) {
|
|
233
|
+
target.add(`${parsed.phoneE164}@c.us`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeLidMapping(value: WahaV2LidMapping | undefined): WahaV2LidMapping | undefined {
|
|
238
|
+
if (!value) return undefined;
|
|
239
|
+
const lid = normalizeRawId(value.lid);
|
|
240
|
+
const pn = normalizeRawId(value.pn) ?? toChatPhoneId(String(value.pn ?? ""));
|
|
241
|
+
if (!lid && !pn) return undefined;
|
|
242
|
+
return { lid, pn };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveStateDir(): string {
|
|
246
|
+
const configured = String(process.env.OPENCLAW_STATE_DIR ?? "").trim();
|
|
247
|
+
return configured || join(homedir(), ".openclaw");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveIdentityIndexPath(account: { session: string }): string {
|
|
251
|
+
return join(
|
|
252
|
+
resolveStateDir(),
|
|
253
|
+
"extensions",
|
|
254
|
+
"waha-v2",
|
|
255
|
+
"identity",
|
|
256
|
+
`${sanitizeFileComponent(account.session)}.json`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function writeIdentityIndex(index: WahaV2IdentityIndex): Promise<void> {
|
|
261
|
+
const path = resolveIdentityIndexPath(index);
|
|
262
|
+
await mkdir(dirname(path), { recursive: true });
|
|
263
|
+
await writeFile(path, `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function formatRecentMessageForAgent(message: WahaV2IdentityMessageSummary): string {
|
|
267
|
+
const direction = message.direction === "out" ? "out" : "in";
|
|
268
|
+
const timestamp = message.timestamp
|
|
269
|
+
? new Date(message.timestamp * 1000).toISOString()
|
|
270
|
+
: "unknown-time";
|
|
271
|
+
const body = String(message.body ?? "").replace(/\s+/g, " ").trim();
|
|
272
|
+
const summary = body || `[${message.type ?? (message.hasMedia ? "media" : "message")}]`;
|
|
273
|
+
return `- ${direction} ${timestamp} ${summary.slice(0, 180)}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function mergePreferredText(current: string | undefined, next: string | undefined): string | undefined {
|
|
277
|
+
const candidate = String(next ?? "").trim();
|
|
278
|
+
if (!candidate) return current;
|
|
279
|
+
const existing = String(current ?? "").trim();
|
|
280
|
+
return existing || candidate;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function mergePreferredPhone(current: string | undefined, next: string | undefined): string | undefined {
|
|
284
|
+
return current ?? extractDigits(next);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function buildIdentityIndex(
|
|
288
|
+
client: WahaV2Client,
|
|
289
|
+
account: ResolvedWahaV2Account,
|
|
290
|
+
): Promise<WahaV2IdentityIndex> {
|
|
291
|
+
const lidMappingEntries = await client.listLidMappings(account.session).catch((err) => {
|
|
292
|
+
getWahaV2Logger().warn(
|
|
293
|
+
`waha-v2: identity lid sync failed for session "${account.session}": ${String(err)}`,
|
|
294
|
+
);
|
|
295
|
+
return [];
|
|
296
|
+
});
|
|
297
|
+
const lidMappings = new Map<string, string>();
|
|
298
|
+
const phoneToLid = new Map<string, string>();
|
|
299
|
+
for (const entry of lidMappingEntries) {
|
|
300
|
+
const normalized = normalizeLidMapping(entry);
|
|
301
|
+
if (!normalized?.lid || !normalized.pn) continue;
|
|
302
|
+
lidMappings.set(normalized.lid, normalized.pn);
|
|
303
|
+
phoneToLid.set(normalized.pn, normalized.lid);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const contacts: unknown[] = [];
|
|
307
|
+
for (let page = 0; page < CONTACT_MAX_PAGES; page += 1) {
|
|
308
|
+
const offset = page * CONTACT_PAGE_SIZE;
|
|
309
|
+
const current = await client.listContacts(account.session, CONTACT_PAGE_SIZE, offset).catch((err) => {
|
|
310
|
+
getWahaV2Logger().warn(
|
|
311
|
+
`waha-v2: identity contact sync failed for session "${account.session}" at offset ${offset}: ${String(err)}`,
|
|
312
|
+
);
|
|
313
|
+
return [];
|
|
314
|
+
});
|
|
315
|
+
if (!Array.isArray(current) || current.length === 0) break;
|
|
316
|
+
contacts.push(...current);
|
|
317
|
+
if (current.length < CONTACT_PAGE_SIZE) break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const selfAliasSet = new Set<string>();
|
|
321
|
+
const selfDisplayNames: string[] = [];
|
|
322
|
+
let selfPhoneE164: string | undefined;
|
|
323
|
+
|
|
324
|
+
for (const contact of contacts) {
|
|
325
|
+
if (!contact || typeof contact !== "object") continue;
|
|
326
|
+
const record = contact as Record<string, unknown>;
|
|
327
|
+
if (record.isMe !== true) continue;
|
|
328
|
+
addAlias(selfAliasSet, record.id);
|
|
329
|
+
selfPhoneE164 = mergePreferredPhone(selfPhoneE164, String(record.number ?? ""));
|
|
330
|
+
const displayName = extractName(record);
|
|
331
|
+
if (displayName) selfDisplayNames.push(displayName);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const peers = new Map<string, WahaV2IdentityPeerRecord>();
|
|
335
|
+
const lookup = new Map<string, string>();
|
|
336
|
+
const canonical = new Map<string, string>();
|
|
337
|
+
|
|
338
|
+
const upsertPeer = (chatId: string, patch: Partial<WahaV2IdentityPeerRecord>, aliasIds?: Iterable<string>) => {
|
|
339
|
+
const parsed = parseJid(chatId);
|
|
340
|
+
const existing =
|
|
341
|
+
peers.get(chatId) ??
|
|
342
|
+
({
|
|
343
|
+
chatId,
|
|
344
|
+
kind: parsed?.kind ?? "unknown",
|
|
345
|
+
aliases: [],
|
|
346
|
+
recentMessages: [],
|
|
347
|
+
updatedAt: new Date().toISOString(),
|
|
348
|
+
} satisfies WahaV2IdentityPeerRecord);
|
|
349
|
+
|
|
350
|
+
const aliasSet = new Set(existing.aliases);
|
|
351
|
+
addAlias(aliasSet, chatId);
|
|
352
|
+
if (aliasIds) {
|
|
353
|
+
for (const alias of aliasIds) {
|
|
354
|
+
addAlias(aliasSet, alias);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const next: WahaV2IdentityPeerRecord = {
|
|
359
|
+
...existing,
|
|
360
|
+
...patch,
|
|
361
|
+
kind: patch.kind ?? existing.kind,
|
|
362
|
+
displayName: mergePreferredText(existing.displayName, patch.displayName),
|
|
363
|
+
verifiedName: mergePreferredText(existing.verifiedName, patch.verifiedName),
|
|
364
|
+
phoneE164: mergePreferredPhone(existing.phoneE164, patch.phoneE164),
|
|
365
|
+
lid: mergePreferredText(existing.lid, patch.lid),
|
|
366
|
+
aliases: Array.from(aliasSet).sort(),
|
|
367
|
+
recentMessages: patch.recentMessages ?? existing.recentMessages,
|
|
368
|
+
lastMessageAt: patch.lastMessageAt ?? existing.lastMessageAt,
|
|
369
|
+
updatedAt: new Date().toISOString(),
|
|
370
|
+
};
|
|
371
|
+
peers.set(chatId, next);
|
|
372
|
+
for (const alias of next.aliases) {
|
|
373
|
+
lookup.set(alias, chatId);
|
|
374
|
+
}
|
|
375
|
+
lookup.set(chatId, chatId);
|
|
376
|
+
if (next.phoneE164) {
|
|
377
|
+
canonical.set(`${next.phoneE164}@c.us`, `${next.phoneE164}@c.us`);
|
|
378
|
+
}
|
|
379
|
+
if (next.lid) {
|
|
380
|
+
const mappedPhone = lidMappings.get(next.lid);
|
|
381
|
+
if (mappedPhone) canonical.set(next.lid, mappedPhone);
|
|
382
|
+
}
|
|
383
|
+
for (const alias of next.aliases) {
|
|
384
|
+
const parsedAlias = parseJid(alias);
|
|
385
|
+
if (parsedAlias?.kind === "lid") {
|
|
386
|
+
const mappedPhone = lidMappings.get(parsedAlias.bare);
|
|
387
|
+
if (mappedPhone) canonical.set(alias, mappedPhone);
|
|
388
|
+
} else if (parsedAlias?.kind === "direct" && parsedAlias.phoneE164) {
|
|
389
|
+
canonical.set(alias, `${parsedAlias.phoneE164}@c.us`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
for (const contact of contacts) {
|
|
395
|
+
if (!contact || typeof contact !== "object") continue;
|
|
396
|
+
const record = contact as Record<string, unknown>;
|
|
397
|
+
const contactId = normalizeRawId(record.id);
|
|
398
|
+
if (!contactId) continue;
|
|
399
|
+
const parsed = parseJid(contactId);
|
|
400
|
+
const aliases = new Set<string>();
|
|
401
|
+
addAlias(aliases, contactId);
|
|
402
|
+
const businessId = normalizeRawId((record.businessProfile as Record<string, unknown> | undefined)?.id);
|
|
403
|
+
if (businessId) addAlias(aliases, businessId);
|
|
404
|
+
const phoneDigits =
|
|
405
|
+
parsed?.kind === "direct" ? extractDigits(String(record.number ?? "")) : undefined;
|
|
406
|
+
if (phoneDigits) aliases.add(`${phoneDigits}@c.us`);
|
|
407
|
+
upsertPeer(
|
|
408
|
+
contactId,
|
|
409
|
+
{
|
|
410
|
+
kind: parsed?.kind ?? "unknown",
|
|
411
|
+
displayName: extractName(record),
|
|
412
|
+
verifiedName: String(record.verifiedName ?? "").trim() || undefined,
|
|
413
|
+
phoneE164: parsed?.phoneE164 ?? phoneDigits,
|
|
414
|
+
lid: parsed?.lid,
|
|
415
|
+
},
|
|
416
|
+
aliases,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const chats = await client.listChats(account.session, CHAT_SYNC_LIMIT, 0).catch((err) => {
|
|
421
|
+
getWahaV2Logger().warn(
|
|
422
|
+
`waha-v2: identity chat sync failed for session "${account.session}": ${String(err)}`,
|
|
423
|
+
);
|
|
424
|
+
return [];
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (Array.isArray(chats)) {
|
|
428
|
+
for (const chat of chats) {
|
|
429
|
+
if (!chat || typeof chat !== "object") continue;
|
|
430
|
+
const record = chat as Record<string, unknown>;
|
|
431
|
+
const chatId = normalizeRawId(record.id ?? record.chatId);
|
|
432
|
+
if (!chatId) continue;
|
|
433
|
+
const parsedChat = parseJid(chatId);
|
|
434
|
+
const recentMessages = await client
|
|
435
|
+
.getChatMessages(account.session, chatId, RECENT_MESSAGE_LIMIT, false)
|
|
436
|
+
.catch((err) => {
|
|
437
|
+
getWahaV2Logger().warn(
|
|
438
|
+
`waha-v2: identity message sync failed for ${chatId} (${account.session}): ${String(err)}`,
|
|
439
|
+
);
|
|
440
|
+
return [];
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const summaries = summarizeRecentMessages(recentMessages, selfAliasSet);
|
|
444
|
+
const aliasSet = new Set<string>();
|
|
445
|
+
addAlias(aliasSet, chatId);
|
|
446
|
+
|
|
447
|
+
let resolvedPhone = parsedChat?.phoneE164;
|
|
448
|
+
let resolvedLid = parsedChat?.lid;
|
|
449
|
+
if (parsedChat?.lid && lidMappings.has(parsedChat.lid)) {
|
|
450
|
+
resolvedPhone = mergePreferredPhone(resolvedPhone, lidMappings.get(parsedChat.lid));
|
|
451
|
+
}
|
|
452
|
+
if (parsedChat?.kind === "direct" && parsedChat.phoneE164) {
|
|
453
|
+
const directChatId = `${parsedChat.phoneE164}@c.us`;
|
|
454
|
+
if (phoneToLid.has(directChatId)) {
|
|
455
|
+
resolvedLid = mergePreferredText(resolvedLid, phoneToLid.get(directChatId));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (summaries.length > 0) {
|
|
459
|
+
for (const message of summaries) {
|
|
460
|
+
const counterpartId = message.direction === "out" ? message.to : message.from;
|
|
461
|
+
const counterpartRaw = normalizeRawId(counterpartId);
|
|
462
|
+
if (
|
|
463
|
+
counterpartRaw &&
|
|
464
|
+
!selfAliasSet.has(counterpartRaw) &&
|
|
465
|
+
!selfAliasSet.has(parseJid(counterpartRaw)?.bare ?? "")
|
|
466
|
+
) {
|
|
467
|
+
addAlias(aliasSet, counterpartRaw);
|
|
468
|
+
}
|
|
469
|
+
const parsedCounterpart = parseJid(counterpartId);
|
|
470
|
+
resolvedPhone = mergePreferredPhone(resolvedPhone, parsedCounterpart?.phoneE164);
|
|
471
|
+
resolvedLid = mergePreferredText(resolvedLid, parsedCounterpart?.lid);
|
|
472
|
+
if (parsedCounterpart?.lid && lidMappings.has(parsedCounterpart.lid)) {
|
|
473
|
+
resolvedPhone = mergePreferredPhone(resolvedPhone, lidMappings.get(parsedCounterpart.lid));
|
|
474
|
+
}
|
|
475
|
+
if (parsedCounterpart?.kind === "direct" && parsedCounterpart.phoneE164) {
|
|
476
|
+
const directCounterpartId = `${parsedCounterpart.phoneE164}@c.us`;
|
|
477
|
+
if (phoneToLid.has(directCounterpartId)) {
|
|
478
|
+
resolvedLid = mergePreferredText(resolvedLid, phoneToLid.get(directCounterpartId));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
upsertPeer(
|
|
485
|
+
chatId,
|
|
486
|
+
{
|
|
487
|
+
kind: record.isGroup === true ? "group" : (parsedChat?.kind ?? "unknown"),
|
|
488
|
+
displayName: extractName(record),
|
|
489
|
+
phoneE164: resolvedPhone,
|
|
490
|
+
lid: resolvedLid,
|
|
491
|
+
recentMessages: summaries,
|
|
492
|
+
lastMessageAt: summaries[0]?.timestamp,
|
|
493
|
+
},
|
|
494
|
+
aliasSet,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const index: WahaV2IdentityIndex = {
|
|
500
|
+
version: IDENTITY_INDEX_VERSION,
|
|
501
|
+
accountId: account.accountId,
|
|
502
|
+
session: account.session,
|
|
503
|
+
refreshedAt: new Date().toISOString(),
|
|
504
|
+
stateDir: resolveStateDir(),
|
|
505
|
+
self:
|
|
506
|
+
selfAliasSet.size > 0 || selfPhoneE164 || selfDisplayNames.length > 0
|
|
507
|
+
? {
|
|
508
|
+
displayName: selfDisplayNames[0],
|
|
509
|
+
phoneE164: selfPhoneE164,
|
|
510
|
+
aliases: Array.from(selfAliasSet).sort(),
|
|
511
|
+
}
|
|
512
|
+
: undefined,
|
|
513
|
+
peers: Object.fromEntries(Array.from(peers.entries()).sort(([a], [b]) => a.localeCompare(b))),
|
|
514
|
+
lookup: Object.fromEntries(Array.from(lookup.entries()).sort(([a], [b]) => a.localeCompare(b))),
|
|
515
|
+
canonical: Object.fromEntries(
|
|
516
|
+
Array.from(canonical.entries()).sort(([a], [b]) => a.localeCompare(b)),
|
|
517
|
+
),
|
|
518
|
+
lidMappings: Object.fromEntries(
|
|
519
|
+
Array.from(lidMappings.entries()).sort(([a], [b]) => a.localeCompare(b)),
|
|
520
|
+
),
|
|
521
|
+
};
|
|
522
|
+
return index;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function loadIdentityIndex(account: ResolvedWahaV2Account): Promise<WahaV2IdentityIndex | undefined> {
|
|
526
|
+
const key = cacheKey(account);
|
|
527
|
+
const cached = identityCache.get(key);
|
|
528
|
+
if (cached) return cached;
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const raw = await readFile(resolveIdentityIndexPath(account), "utf8");
|
|
532
|
+
const parsed = JSON.parse(raw) as Partial<WahaV2IdentityIndex>;
|
|
533
|
+
const hydrated = {
|
|
534
|
+
version: parsed.version ?? IDENTITY_INDEX_VERSION,
|
|
535
|
+
accountId: parsed.accountId ?? account.accountId,
|
|
536
|
+
session: parsed.session ?? account.session,
|
|
537
|
+
refreshedAt: parsed.refreshedAt ?? new Date(0).toISOString(),
|
|
538
|
+
stateDir: parsed.stateDir ?? resolveStateDir(),
|
|
539
|
+
self: parsed.self,
|
|
540
|
+
peers: parsed.peers ?? {},
|
|
541
|
+
lookup: parsed.lookup ?? {},
|
|
542
|
+
canonical: parsed.canonical ?? {},
|
|
543
|
+
lidMappings: parsed.lidMappings ?? {},
|
|
544
|
+
} satisfies WahaV2IdentityIndex;
|
|
545
|
+
identityCache.set(key, hydrated);
|
|
546
|
+
return hydrated;
|
|
547
|
+
} catch {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export async function warmWahaV2IdentityIndex(
|
|
553
|
+
client: WahaV2Client,
|
|
554
|
+
account: ResolvedWahaV2Account,
|
|
555
|
+
reason: string,
|
|
556
|
+
): Promise<WahaV2IdentityIndex> {
|
|
557
|
+
const key = cacheKey(account);
|
|
558
|
+
const existing = warmInFlight.get(key);
|
|
559
|
+
if (existing) return existing;
|
|
560
|
+
|
|
561
|
+
const task = (async () => {
|
|
562
|
+
getWahaV2Logger().info(
|
|
563
|
+
`waha-v2: identity warmup started for account "${account.accountId}" session="${account.session}" reason=${reason}`,
|
|
564
|
+
);
|
|
565
|
+
const index = await buildIdentityIndex(client, account);
|
|
566
|
+
await writeIdentityIndex(index);
|
|
567
|
+
identityCache.set(key, index);
|
|
568
|
+
getWahaV2Logger().info(
|
|
569
|
+
`waha-v2: identity warmup completed for account "${account.accountId}" session="${account.session}" peers=${Object.keys(index.peers).length}`,
|
|
570
|
+
);
|
|
571
|
+
return index;
|
|
572
|
+
})()
|
|
573
|
+
.catch((err) => {
|
|
574
|
+
getWahaV2Logger().warn(
|
|
575
|
+
`waha-v2: identity warmup failed for account "${account.accountId}" session="${account.session}": ${String(err)}`,
|
|
576
|
+
);
|
|
577
|
+
throw err;
|
|
578
|
+
})
|
|
579
|
+
.finally(() => {
|
|
580
|
+
warmInFlight.delete(key);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
warmInFlight.set(key, task);
|
|
584
|
+
return task;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export async function resolveWahaV2Identity(
|
|
588
|
+
account: ResolvedWahaV2Account,
|
|
589
|
+
params: { chatId?: string; senderId?: string },
|
|
590
|
+
): Promise<WahaV2ResolvedIdentity> {
|
|
591
|
+
const index = await loadIdentityIndex(account);
|
|
592
|
+
const path = resolveIdentityIndexPath(account);
|
|
593
|
+
if (!index) return { indexPath: path };
|
|
594
|
+
|
|
595
|
+
const chatId = normalizeRawId(params.chatId);
|
|
596
|
+
const senderId = normalizeRawId(params.senderId);
|
|
597
|
+
const resolvePeer = (rawValue?: string): WahaV2IdentityPeerRecord | undefined => {
|
|
598
|
+
const parsed = parseJid(rawValue);
|
|
599
|
+
if (!parsed) return undefined;
|
|
600
|
+
const candidates = [parsed.raw, parsed.bare];
|
|
601
|
+
if (parsed.phoneE164) candidates.push(`${parsed.phoneE164}@c.us`);
|
|
602
|
+
for (const candidate of candidates) {
|
|
603
|
+
const chatKey = index.lookup[candidate];
|
|
604
|
+
if (chatKey && index.peers[chatKey]) return index.peers[chatKey];
|
|
605
|
+
}
|
|
606
|
+
return undefined;
|
|
607
|
+
};
|
|
608
|
+
const resolveCanonical = (rawValue?: string): string | undefined => {
|
|
609
|
+
const parsed = parseJid(rawValue);
|
|
610
|
+
if (!parsed) return undefined;
|
|
611
|
+
const candidates = [parsed.raw, parsed.bare];
|
|
612
|
+
if (parsed.phoneE164) candidates.push(`${parsed.phoneE164}@c.us`);
|
|
613
|
+
for (const candidate of candidates) {
|
|
614
|
+
const canonicalId = index.canonical[candidate];
|
|
615
|
+
if (canonicalId) return canonicalId;
|
|
616
|
+
}
|
|
617
|
+
if (parsed.kind === "direct" && parsed.phoneE164) return `${parsed.phoneE164}@c.us`;
|
|
618
|
+
if (parsed.kind === "group") return parsed.raw;
|
|
619
|
+
return undefined;
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
chat: resolvePeer(chatId),
|
|
624
|
+
sender: resolvePeer(senderId),
|
|
625
|
+
canonicalChatId: resolveCanonical(chatId),
|
|
626
|
+
canonicalSenderId: resolveCanonical(senderId),
|
|
627
|
+
indexPath: path,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export function buildWahaV2IdentityContext(params: {
|
|
632
|
+
chatId?: string;
|
|
633
|
+
senderId?: string;
|
|
634
|
+
isGroup: boolean;
|
|
635
|
+
resolved: WahaV2ResolvedIdentity;
|
|
636
|
+
}): string | undefined {
|
|
637
|
+
const lines: string[] = [];
|
|
638
|
+
const chatId = normalizeRawId(params.chatId);
|
|
639
|
+
const senderId = normalizeRawId(params.senderId);
|
|
640
|
+
const chat = params.resolved.chat;
|
|
641
|
+
const sender = params.resolved.sender;
|
|
642
|
+
|
|
643
|
+
if (!chatId && !senderId && !chat && !sender) return undefined;
|
|
644
|
+
|
|
645
|
+
lines.push("[WHATSAPP IDENTITY]");
|
|
646
|
+
if (chatId) lines.push(`ChatId: ${chatId}`);
|
|
647
|
+
if (params.resolved.canonicalChatId) lines.push(`CanonicalChatId: ${params.resolved.canonicalChatId}`);
|
|
648
|
+
if (chat?.displayName) lines.push(`ChatName: ${chat.displayName}`);
|
|
649
|
+
if (params.isGroup) {
|
|
650
|
+
if (senderId) lines.push(`SenderId: ${senderId}`);
|
|
651
|
+
if (params.resolved.canonicalSenderId) {
|
|
652
|
+
lines.push(`CanonicalSenderId: ${params.resolved.canonicalSenderId}`);
|
|
653
|
+
}
|
|
654
|
+
if (sender?.displayName) lines.push(`SenderName: ${sender.displayName}`);
|
|
655
|
+
if (sender?.phoneE164) lines.push(`SenderPhone: ${sender.phoneE164}`);
|
|
656
|
+
} else {
|
|
657
|
+
if (senderId) lines.push(`PeerId: ${senderId}`);
|
|
658
|
+
if (params.resolved.canonicalSenderId) {
|
|
659
|
+
lines.push(`CanonicalPeerId: ${params.resolved.canonicalSenderId}`);
|
|
660
|
+
}
|
|
661
|
+
if ((sender ?? chat)?.displayName) lines.push(`PeerName: ${(sender ?? chat)?.displayName}`);
|
|
662
|
+
if ((sender ?? chat)?.phoneE164) lines.push(`PeerPhone: ${(sender ?? chat)?.phoneE164}`);
|
|
663
|
+
}
|
|
664
|
+
if ((sender ?? chat)?.lid) lines.push(`PeerLid: ${(sender ?? chat)?.lid}`);
|
|
665
|
+
const recent = chat?.recentMessages ?? [];
|
|
666
|
+
if (recent.length > 0) {
|
|
667
|
+
lines.push("RecentMessages:");
|
|
668
|
+
for (const message of recent.slice(0, RECENT_MESSAGE_LIMIT)) {
|
|
669
|
+
lines.push(formatRecentMessageForAgent(message));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
lines.push(`IdentityIndex: ${params.resolved.indexPath}`);
|
|
673
|
+
return `${lines.join("\n")}\n\n`;
|
|
674
|
+
}
|
package/src/webhook.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { readJsonBodyWithLimit, sleep } from "openclaw/plugin-sdk";
|
|
|
7
7
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
8
8
|
import { resolveWahaV2Account, resolveWahaV2AccountBySession } from "./accounts.js";
|
|
9
9
|
import { calcTypingDelayMs, chunkWahaMessage } from "./deliver.js";
|
|
10
|
+
import { buildWahaV2IdentityContext, resolveWahaV2Identity } from "./identity.js";
|
|
10
11
|
import { getWahaV2Logger, getWahaV2Runtime, requireWahaV2Client } from "./runtime.js";
|
|
11
12
|
import {
|
|
12
13
|
WAHA_V2_CHANNEL_ID,
|
|
@@ -280,10 +281,9 @@ async function applyLabelDirectivesToChat(
|
|
|
280
281
|
);
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
/**
|
|
284
|
+
/** Preserve WA JIDs as-is while normalizing case/whitespace. */
|
|
284
285
|
function normalizeSenderId(raw: string): string {
|
|
285
|
-
|
|
286
|
-
return `${raw.replace(/\D+/g, "")}@c.us`;
|
|
286
|
+
return String(raw ?? "").trim().toLowerCase();
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
function matchesOwnerPauseWord(text: string, words?: string[]): string | undefined {
|
|
@@ -533,10 +533,31 @@ async function processWahaV2EventImmediate(
|
|
|
533
533
|
// Build a descriptive body when there's media but no caption text.
|
|
534
534
|
const mediaLabel = mediaPath ? mediaTypeLabel(payload.type, mediaMime) : undefined;
|
|
535
535
|
const bodyForAgent = text || (mediaLabel ? `[${mediaLabel}]` : "");
|
|
536
|
+
const resolvedIdentity = await resolveWahaV2Identity(account, { chatId, senderId }).catch((err) => {
|
|
537
|
+
getWahaV2Logger().warn(`waha-v2: identity resolution failed (${chatId}): ${String(err)}`);
|
|
538
|
+
return {
|
|
539
|
+
chat: undefined,
|
|
540
|
+
sender: undefined,
|
|
541
|
+
canonicalChatId: undefined,
|
|
542
|
+
canonicalSenderId: undefined,
|
|
543
|
+
indexPath: "unavailable",
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
const canonicalChatId =
|
|
547
|
+
isGroup ? chatId : (resolvedIdentity.canonicalChatId ?? resolvedIdentity.canonicalSenderId ?? chatId);
|
|
548
|
+
const canonicalSenderId = isGroup
|
|
549
|
+
? (resolvedIdentity.canonicalSenderId ?? senderId)
|
|
550
|
+
: (resolvedIdentity.canonicalSenderId ?? resolvedIdentity.canonicalChatId ?? senderId);
|
|
551
|
+
const identityPrefix = buildWahaV2IdentityContext({
|
|
552
|
+
chatId,
|
|
553
|
+
senderId,
|
|
554
|
+
isGroup,
|
|
555
|
+
resolved: resolvedIdentity,
|
|
556
|
+
});
|
|
536
557
|
const policyPrefix = policyInstruction
|
|
537
558
|
? `[CHAT POLICY]\nLabels: ${labelSummary}\nInstruction: ${policyInstruction}\n\n`
|
|
538
559
|
: "";
|
|
539
|
-
const bodyForAgentWithPolicy = `${policyPrefix}${bodyForAgent}`.trim();
|
|
560
|
+
const bodyForAgentWithPolicy = `${identityPrefix ?? ""}${policyPrefix}${bodyForAgent}`.trim();
|
|
540
561
|
|
|
541
562
|
// If we have both caption text and media, surface both in Body.
|
|
542
563
|
const combinedBody = text && mediaLabel ? `${text}\n[${mediaLabel}]` : bodyForAgent;
|
|
@@ -545,7 +566,7 @@ async function processWahaV2EventImmediate(
|
|
|
545
566
|
cfg,
|
|
546
567
|
channel: WAHA_V2_CHANNEL_ID,
|
|
547
568
|
accountId: account.accountId,
|
|
548
|
-
peer: { kind: isGroup ? "group" : "direct", id:
|
|
569
|
+
peer: { kind: isGroup ? "group" : "direct", id: canonicalChatId },
|
|
549
570
|
});
|
|
550
571
|
|
|
551
572
|
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
@@ -557,14 +578,23 @@ async function processWahaV2EventImmediate(
|
|
|
557
578
|
BodyForAgent: bodyForAgentWithPolicy,
|
|
558
579
|
RawBody: text,
|
|
559
580
|
CommandBody: text,
|
|
560
|
-
From: `${WAHA_V2_CHANNEL_ID}:${
|
|
581
|
+
From: `${WAHA_V2_CHANNEL_ID}:${canonicalSenderId}`,
|
|
561
582
|
To: `${WAHA_V2_CHANNEL_ID}:${account.session}`,
|
|
562
583
|
SessionKey: route.sessionKey,
|
|
563
584
|
AccountId: route.accountId,
|
|
564
585
|
ChatType: isGroup ? ("group" as const) : ("direct" as const),
|
|
565
586
|
Provider: WAHA_V2_CHANNEL_ID,
|
|
566
587
|
Surface: WAHA_V2_CHANNEL_ID,
|
|
567
|
-
SenderId:
|
|
588
|
+
SenderId: canonicalSenderId,
|
|
589
|
+
WahaChatId: chatId,
|
|
590
|
+
WahaSenderId: senderId,
|
|
591
|
+
WahaCanonicalChatId: canonicalChatId,
|
|
592
|
+
WahaCanonicalSenderId: canonicalSenderId,
|
|
593
|
+
WahaChatName: resolvedIdentity.chat?.displayName,
|
|
594
|
+
WahaChatPhone: resolvedIdentity.chat?.phoneE164,
|
|
595
|
+
WahaSenderName: resolvedIdentity.sender?.displayName ?? resolvedIdentity.chat?.displayName,
|
|
596
|
+
WahaSenderPhone: resolvedIdentity.sender?.phoneE164 ?? resolvedIdentity.chat?.phoneE164,
|
|
597
|
+
WahaIdentityIndexPath: resolvedIdentity.indexPath,
|
|
568
598
|
MessageSid: payload.id,
|
|
569
599
|
Timestamp: payload.timestamp ? payload.timestamp * 1000 : Date.now(),
|
|
570
600
|
// Media fields — consumed by the agent framework to send the file to the AI.
|