lark-oapi 2.0.0.dev2__py3-none-any.whl → 2.0.0.dev4__py3-none-any.whl
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.
- lark_oapi/channel/channel.py +149 -23
- lark_oapi/channel/normalize/comment.py +76 -17
- lark_oapi/channel/normalize/mentions.py +21 -0
- lark_oapi/channel/normalize/pipeline.py +16 -1
- lark_oapi/core/const.py +1 -1
- {lark_oapi-2.0.0.dev2.dist-info → lark_oapi-2.0.0.dev4.dist-info}/METADATA +1 -1
- {lark_oapi-2.0.0.dev2.dist-info → lark_oapi-2.0.0.dev4.dist-info}/RECORD +10 -10
- {lark_oapi-2.0.0.dev2.dist-info → lark_oapi-2.0.0.dev4.dist-info}/WHEEL +0 -0
- {lark_oapi-2.0.0.dev2.dist-info → lark_oapi-2.0.0.dev4.dist-info}/licenses/LICENSE +0 -0
- {lark_oapi-2.0.0.dev2.dist-info → lark_oapi-2.0.0.dev4.dist-info}/top_level.txt +0 -0
lark_oapi/channel/channel.py
CHANGED
|
@@ -70,6 +70,7 @@ from .config import (
|
|
|
70
70
|
from .driver import LarkClientDriver
|
|
71
71
|
from .errors import FeishuChannelError, FeishuChannelErrorCode
|
|
72
72
|
from .identity import IdentityResolver, NameCache
|
|
73
|
+
from .normalize.comment import normalize_comment
|
|
73
74
|
from .normalize.dedup import Deduper, InMemoryDedupStore
|
|
74
75
|
from .normalize.pipeline import InboundPipeline, PipelineConfig, PipelineDeps
|
|
75
76
|
from .outbound.routing import infer_receive_id_type
|
|
@@ -95,6 +96,22 @@ EventHandler = Callable[..., Any]
|
|
|
95
96
|
Unsubscribe = Callable[[], None]
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
def _card_action_identity(action: Any) -> str:
|
|
100
|
+
"""Stable dedup fragment for a card action.
|
|
101
|
+
|
|
102
|
+
Node-SDK aligned: different buttons on the same card click to different
|
|
103
|
+
``(tag, value)`` pairs, and those must dedup-distinctly; a genuine WS
|
|
104
|
+
redelivery of the *same* click hashes identically and is suppressed.
|
|
105
|
+
"""
|
|
106
|
+
tag = getattr(action, "tag", "") or ""
|
|
107
|
+
value = getattr(action, "value", None)
|
|
108
|
+
try:
|
|
109
|
+
value_repr = json.dumps(value, sort_keys=True, ensure_ascii=False)
|
|
110
|
+
except (TypeError, ValueError):
|
|
111
|
+
value_repr = repr(value)
|
|
112
|
+
return f"{tag}:{value_repr}"
|
|
113
|
+
|
|
114
|
+
|
|
98
115
|
# ---------------------------------------------------------------------------
|
|
99
116
|
# FeishuChannel
|
|
100
117
|
# ---------------------------------------------------------------------------
|
|
@@ -771,6 +788,18 @@ class FeishuChannel:
|
|
|
771
788
|
b = getattr(b, register)(handler)
|
|
772
789
|
except Exception: # pragma: no cover
|
|
773
790
|
pass
|
|
791
|
+
# drive.notice.comment_add_v1 has no typed processor in the
|
|
792
|
+
# generated SDK. The wire payload may arrive under either schema:
|
|
793
|
+
# the legacy callback channel uses p1 (event has ``uuid``), but
|
|
794
|
+
# the modern WS frontier wraps the same event in a p2 envelope
|
|
795
|
+
# (``schema=2.0``). Register the customized-event handler under
|
|
796
|
+
# both so neither path logs "processor not found".
|
|
797
|
+
b = b.register_p1_customized_event(
|
|
798
|
+
"drive.notice.comment_add_v1", self._on_p1_comment_add
|
|
799
|
+
)
|
|
800
|
+
b = b.register_p2_customized_event(
|
|
801
|
+
"drive.notice.comment_add_v1", self._on_p1_comment_add
|
|
802
|
+
)
|
|
774
803
|
return b.build()
|
|
775
804
|
|
|
776
805
|
# ------------------------------------------------------------------
|
|
@@ -804,6 +833,9 @@ class FeishuChannel:
|
|
|
804
833
|
def _on_p2_message_read(self, data: Any) -> None:
|
|
805
834
|
self.schedule(self._handle_message_read_event(data))
|
|
806
835
|
|
|
836
|
+
def _on_p1_comment_add(self, data: Any) -> None:
|
|
837
|
+
self.schedule(self._handle_comment_event(data))
|
|
838
|
+
|
|
807
839
|
# ------------------------------------------------------------------
|
|
808
840
|
# Async event handlers
|
|
809
841
|
# ------------------------------------------------------------------
|
|
@@ -866,25 +898,80 @@ class FeishuChannel:
|
|
|
866
898
|
operator_open_id = getattr(
|
|
867
899
|
getattr(event, "operator", None), "open_id", None
|
|
868
900
|
)
|
|
869
|
-
|
|
870
|
-
"
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
if hasattr(raw_value, "name")
|
|
880
|
-
else None,
|
|
881
|
-
),
|
|
882
|
-
raw=_coerce.obj_to_dict(data) or {},
|
|
901
|
+
payload = CardActionEvent(
|
|
902
|
+
message_id=message_id or "",
|
|
903
|
+
chat_id=chat_id or "",
|
|
904
|
+
operator=EventOperator(open_id=operator_open_id or ""),
|
|
905
|
+
action=CardActionPayload(
|
|
906
|
+
tag=tag or "",
|
|
907
|
+
value=raw_value,
|
|
908
|
+
name=getattr(raw_value, "name", None)
|
|
909
|
+
if hasattr(raw_value, "name")
|
|
910
|
+
else None,
|
|
883
911
|
),
|
|
912
|
+
raw=_coerce.obj_to_dict(data) or {},
|
|
913
|
+
)
|
|
914
|
+
# Route through safety.push_action (tier 2): dedup on a stable
|
|
915
|
+
# action identity (tag + value payload) so Feishu's at-least-once
|
|
916
|
+
# WS redelivery can't double-invoke the handler, and serialize by
|
|
917
|
+
# chat_id so two fast clicks in the same chat are processed in
|
|
918
|
+
# order. Node-SDK aligned.
|
|
919
|
+
await self._through_action_safety(
|
|
920
|
+
event_id=f"card:{payload.message_id}:{payload.operator.open_id}:"
|
|
921
|
+
f"{_card_action_identity(payload.action)}",
|
|
922
|
+
queue_scope=payload.chat_id or payload.message_id or "",
|
|
923
|
+
handler=lambda: self._invoke("cardAction", payload),
|
|
884
924
|
)
|
|
885
925
|
except Exception as e:
|
|
886
926
|
logger.exception("FeishuChannel cardAction dispatch failed: %s", e)
|
|
887
927
|
|
|
928
|
+
async def _through_action_safety(
|
|
929
|
+
self,
|
|
930
|
+
*,
|
|
931
|
+
event_id: str,
|
|
932
|
+
queue_scope: str,
|
|
933
|
+
handler: Callable[[], Any],
|
|
934
|
+
) -> None:
|
|
935
|
+
"""Run ``handler`` through the safety tier-2 gate (dedup + lock +
|
|
936
|
+
per-scope serial queue) when the pipeline exists; fall back to a
|
|
937
|
+
direct invocation when it hasn't been built yet (early events during
|
|
938
|
+
startup, unit tests that bypass ``connect``)."""
|
|
939
|
+
safety = self._safety
|
|
940
|
+
if safety is None:
|
|
941
|
+
result = handler()
|
|
942
|
+
if inspect.isawaitable(result):
|
|
943
|
+
await result
|
|
944
|
+
return
|
|
945
|
+
|
|
946
|
+
async def _run() -> None:
|
|
947
|
+
result = handler()
|
|
948
|
+
if inspect.isawaitable(result):
|
|
949
|
+
await result
|
|
950
|
+
|
|
951
|
+
await safety.push_action(event_id, queue_scope or event_id, _run)
|
|
952
|
+
|
|
953
|
+
async def _through_light_safety(
|
|
954
|
+
self,
|
|
955
|
+
*,
|
|
956
|
+
event_id: str,
|
|
957
|
+
handler: Callable[[], Any],
|
|
958
|
+
) -> None:
|
|
959
|
+
"""Tier-3 variant: dedup only (reaction add/remove). Same fallback
|
|
960
|
+
semantics as :meth:`_through_action_safety`."""
|
|
961
|
+
safety = self._safety
|
|
962
|
+
if safety is None:
|
|
963
|
+
result = handler()
|
|
964
|
+
if inspect.isawaitable(result):
|
|
965
|
+
await result
|
|
966
|
+
return
|
|
967
|
+
|
|
968
|
+
async def _run() -> None:
|
|
969
|
+
result = handler()
|
|
970
|
+
if inspect.isawaitable(result):
|
|
971
|
+
await result
|
|
972
|
+
|
|
973
|
+
await safety.push_light(event_id, _run)
|
|
974
|
+
|
|
888
975
|
async def _handle_reaction_event(self, data: Any, *, action: str) -> None:
|
|
889
976
|
cfg = self._config.inbound.reaction_notifications
|
|
890
977
|
if cfg == "off":
|
|
@@ -905,16 +992,25 @@ class FeishuChannel:
|
|
|
905
992
|
if message_id and message_id not in self._sent_messages:
|
|
906
993
|
return
|
|
907
994
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
995
|
+
direction = "added" if action == "create" else "removed"
|
|
996
|
+
payload = ReactionEvent(
|
|
997
|
+
message_id=message_id,
|
|
998
|
+
operator=EventOperator(open_id=operator_open_id or ""),
|
|
999
|
+
emoji_type=emoji_type or "",
|
|
1000
|
+
action=direction,
|
|
1001
|
+
action_time=action_time,
|
|
1002
|
+
raw=_coerce.obj_to_dict(data) or {},
|
|
1003
|
+
)
|
|
1004
|
+
# Tier 3: dedup only. Reactions are idempotent state changes so
|
|
1005
|
+
# lock / serial queue would add latency for no benefit, but
|
|
1006
|
+
# WS redelivery would double-invoke without this guard.
|
|
1007
|
+
# Node-SDK aligned (pushLight).
|
|
1008
|
+
await self._through_light_safety(
|
|
1009
|
+
event_id=(
|
|
1010
|
+
f"reaction:{message_id}:{operator_open_id or ''}:"
|
|
1011
|
+
f"{emoji_type or ''}:{direction}"
|
|
917
1012
|
),
|
|
1013
|
+
handler=lambda: self._invoke("reaction", payload),
|
|
918
1014
|
)
|
|
919
1015
|
except Exception as e:
|
|
920
1016
|
logger.exception("FeishuChannel reaction dispatch failed: %s", e)
|
|
@@ -961,6 +1057,36 @@ class FeishuChannel:
|
|
|
961
1057
|
except Exception as e:
|
|
962
1058
|
logger.exception("FeishuChannel messageRead dispatch failed: %s", e)
|
|
963
1059
|
|
|
1060
|
+
async def _handle_comment_event(self, data: Any) -> None:
|
|
1061
|
+
try:
|
|
1062
|
+
# ``CustomizedEvent.event`` is the raw inner event payload as a
|
|
1063
|
+
# plain dict; the per-event timestamp lives on the envelope
|
|
1064
|
+
# (``header.create_time`` for p2, ``ts`` for p1) — not in the
|
|
1065
|
+
# inner dict — so pass it explicitly.
|
|
1066
|
+
raw_event = getattr(data, "event", None)
|
|
1067
|
+
header = getattr(data, "header", None)
|
|
1068
|
+
envelope_ts = (
|
|
1069
|
+
getattr(header, "create_time", None) if header is not None
|
|
1070
|
+
else getattr(data, "ts", None)
|
|
1071
|
+
)
|
|
1072
|
+
normalized = normalize_comment(
|
|
1073
|
+
raw_event if raw_event is not None else data,
|
|
1074
|
+
bot_open_id=self._bot_open_id,
|
|
1075
|
+
envelope_timestamp=envelope_ts,
|
|
1076
|
+
)
|
|
1077
|
+
if normalized is None:
|
|
1078
|
+
return
|
|
1079
|
+
# Tier 2: dedup + lock + per-file_token serial queue. Multiple
|
|
1080
|
+
# comments on the same document are ordered; redeliveries of the
|
|
1081
|
+
# same comment event are dropped. Node-SDK aligned.
|
|
1082
|
+
await self._through_action_safety(
|
|
1083
|
+
event_id=f"comment:{normalized.file_token}:{normalized.comment_id}",
|
|
1084
|
+
queue_scope=normalized.file_token,
|
|
1085
|
+
handler=lambda: self._invoke("comment", normalized),
|
|
1086
|
+
)
|
|
1087
|
+
except Exception as e:
|
|
1088
|
+
logger.exception("FeishuChannel comment dispatch failed: %s", e)
|
|
1089
|
+
|
|
964
1090
|
def _notify_reconnecting(self) -> None:
|
|
965
1091
|
for h in list(self._handlers.get("reconnecting", [])):
|
|
966
1092
|
try:
|
|
@@ -1,7 +1,33 @@
|
|
|
1
|
-
"""Normalize
|
|
1
|
+
"""Normalize ``drive.notice.comment_add_v1`` events.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Wire payload (node-aligned, observed end-to-end via channel-test-harness
|
|
4
|
+
TC-317 against a real Feishu tenant 2026-04-27)::
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
"file_token": "...",
|
|
8
|
+
"file_type": "docx",
|
|
9
|
+
"comment_id": "...",
|
|
10
|
+
"reply_id": "...",
|
|
11
|
+
"is_mentioned": true,
|
|
12
|
+
"create_time": "1700000000000", # ms, string
|
|
13
|
+
"notice_meta": {
|
|
14
|
+
"from_user_id": { "open_id": "...", "user_id": "...", "union_id": "..." },
|
|
15
|
+
"to_user_id": { ... },
|
|
16
|
+
"file_token": "...",
|
|
17
|
+
"file_type": "docx",
|
|
18
|
+
"timestamp": "1700000000000",
|
|
19
|
+
"is_mentioned": true,
|
|
20
|
+
"notice_type": "comment_add"
|
|
21
|
+
},
|
|
22
|
+
// Legacy fallbacks (older p1 callbacks):
|
|
23
|
+
"user_id": { "open_id": "...", ... },
|
|
24
|
+
"is_mention": true,
|
|
25
|
+
"action_time": "1700000000000"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
The operator lives at ``notice_meta.from_user_id`` (top-level
|
|
29
|
+
``user_id`` is the legacy fallback). Whether the bot was mentioned is a
|
|
30
|
+
boolean flag (``is_mentioned``), not a probe of the mentions array.
|
|
5
31
|
"""
|
|
6
32
|
|
|
7
33
|
from dataclasses import dataclass, field
|
|
@@ -31,10 +57,23 @@ def normalize_comment(
|
|
|
31
57
|
data: Any,
|
|
32
58
|
*,
|
|
33
59
|
bot_open_id: Optional[str] = None,
|
|
60
|
+
envelope_timestamp: Optional[str] = None,
|
|
34
61
|
) -> Optional[CommentEvent]:
|
|
35
|
-
"""Flatten the raw drive.notice.comment_add_v1 payload.
|
|
62
|
+
"""Flatten the raw ``drive.notice.comment_add_v1`` payload.
|
|
63
|
+
|
|
64
|
+
Accepts either the whole envelope (with ``event`` key) or the inner
|
|
65
|
+
event dict. Returns ``None`` when the payload is malformed or missing
|
|
66
|
+
one of the required fields (``file_token`` / ``file_type`` /
|
|
67
|
+
``comment_id`` / operator open_id).
|
|
36
68
|
|
|
37
|
-
|
|
69
|
+
``envelope_timestamp`` is the ``header.create_time`` (p2) or top-level
|
|
70
|
+
``ts`` (p1) carried by the WS/HTTP envelope. The inner event payload
|
|
71
|
+
omits a per-event timestamp on the wire, so the envelope is the only
|
|
72
|
+
reliable source — pass it from the dispatcher callback.
|
|
73
|
+
|
|
74
|
+
``bot_open_id`` is unused — the bot-mention signal is sourced from the
|
|
75
|
+
payload's ``is_mentioned`` flag instead. The parameter is kept for
|
|
76
|
+
backward compatibility with the previous (broken) implementation.
|
|
38
77
|
"""
|
|
39
78
|
if not isinstance(data, dict):
|
|
40
79
|
data = _try_dict(data)
|
|
@@ -46,30 +85,50 @@ def normalize_comment(
|
|
|
46
85
|
notice_meta = event.get("notice_meta") if isinstance(event.get("notice_meta"), dict) else {}
|
|
47
86
|
|
|
48
87
|
file_token = event.get("file_token") or notice_meta.get("file_token") or ""
|
|
49
|
-
if not file_token:
|
|
50
|
-
return None
|
|
51
88
|
file_type = event.get("file_type") or notice_meta.get("file_type") or ""
|
|
52
89
|
comment_id = event.get("comment_id") or notice_meta.get("comment_id") or ""
|
|
53
|
-
|
|
90
|
+
|
|
91
|
+
# Operator: prefer notice_meta.from_user_id (current p2 wire format),
|
|
92
|
+
# fall back to top-level user_id (legacy p1 callback shape). The old
|
|
93
|
+
# path looked at ``event.operator`` / ``event.operator_id`` — neither
|
|
94
|
+
# is in the actual payload, so operator came back null.
|
|
95
|
+
user_id_obj = notice_meta.get("from_user_id") or event.get("user_id") or {}
|
|
96
|
+
if not isinstance(user_id_obj, dict):
|
|
97
|
+
user_id_obj = {}
|
|
98
|
+
operator_open_id = user_id_obj.get("open_id")
|
|
99
|
+
|
|
100
|
+
# Required-field gate (node-aligned). Missing operator open_id is a
|
|
101
|
+
# malformed payload — drop rather than deliver a half-populated event.
|
|
102
|
+
if not (file_token and file_type and comment_id and operator_open_id):
|
|
54
103
|
return None
|
|
104
|
+
|
|
55
105
|
reply_id = event.get("reply_id") or notice_meta.get("reply_id") or None
|
|
56
106
|
|
|
57
|
-
operator_raw = event.get("operator") or event.get("operator_id") or notice_meta.get("operator") or {}
|
|
58
107
|
op = CommentOperator(
|
|
59
|
-
open_id=
|
|
60
|
-
user_id=
|
|
61
|
-
union_id=
|
|
108
|
+
open_id=operator_open_id,
|
|
109
|
+
user_id=user_id_obj.get("user_id"),
|
|
110
|
+
union_id=user_id_obj.get("union_id"),
|
|
62
111
|
)
|
|
112
|
+
|
|
63
113
|
mentioned_bot_flag = bool(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
114
|
+
event.get("is_mentioned")
|
|
115
|
+
if event.get("is_mentioned") is not None
|
|
116
|
+
else (notice_meta.get("is_mentioned") or event.get("is_mention"))
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
ts_str = (
|
|
120
|
+
event.get("create_time")
|
|
121
|
+
or notice_meta.get("timestamp")
|
|
122
|
+
or event.get("action_time")
|
|
123
|
+
or event.get("event_create_time")
|
|
124
|
+
or event.get("timestamp")
|
|
125
|
+
or envelope_timestamp
|
|
68
126
|
)
|
|
69
127
|
try:
|
|
70
|
-
ts = int(
|
|
128
|
+
ts = int(ts_str) if ts_str is not None else 0
|
|
71
129
|
except (TypeError, ValueError):
|
|
72
130
|
ts = 0
|
|
131
|
+
|
|
73
132
|
return CommentEvent(
|
|
74
133
|
file_token=file_token,
|
|
75
134
|
file_type=file_type,
|
|
@@ -20,6 +20,10 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
|
20
20
|
from ..types import Mention
|
|
21
21
|
|
|
22
22
|
_PLACEHOLDER_RE = re.compile(r"@_user_\d+")
|
|
23
|
+
# Matches the ``@_all`` mention-all placeholder Feishu embeds in text content
|
|
24
|
+
# for "@所有人" messages. Needs a non-word-char (or end-of-string) boundary
|
|
25
|
+
# after ``@_all`` so we don't misfire on, say, ``@_all_employees``.
|
|
26
|
+
_AT_ALL_RE = re.compile(r"@_all(?![A-Za-z0-9_])")
|
|
23
27
|
_AT_TAG_RE = re.compile(
|
|
24
28
|
r"<at\s+([^>]*?)>(?P<name>.*?)</at>",
|
|
25
29
|
re.IGNORECASE | re.DOTALL,
|
|
@@ -27,6 +31,20 @@ _AT_TAG_RE = re.compile(
|
|
|
27
31
|
_ATTR_RE = re.compile(r'([\w_-]+)\s*=\s*"([^"]*)"')
|
|
28
32
|
|
|
29
33
|
|
|
34
|
+
def text_has_mention_all(text: Optional[str]) -> bool:
|
|
35
|
+
"""True if ``text`` contains the ``@_all`` placeholder.
|
|
36
|
+
|
|
37
|
+
Feishu does NOT populate ``event.message.mentions`` for ``@所有人``
|
|
38
|
+
messages in all cases — the only signal can be an ``@_all`` token
|
|
39
|
+
inside ``content.text``. Without detecting that, downstream policy
|
|
40
|
+
code sees ``mentioned_all=False`` and the ``mention_all_blocked``
|
|
41
|
+
gate never fires.
|
|
42
|
+
"""
|
|
43
|
+
if not text:
|
|
44
|
+
return False
|
|
45
|
+
return _AT_ALL_RE.search(text) is not None
|
|
46
|
+
|
|
47
|
+
|
|
30
48
|
# ---------------------------------------------------------------------------
|
|
31
49
|
# Data shape
|
|
32
50
|
# ---------------------------------------------------------------------------
|
|
@@ -155,6 +173,9 @@ def resolve_mentions(
|
|
|
155
173
|
return f"@{m.name}"
|
|
156
174
|
|
|
157
175
|
result = _PLACEHOLDER_RE.sub(_replace, content)
|
|
176
|
+
# Rewrite ``@_all`` placeholder to the human-visible form; without this
|
|
177
|
+
# user-visible content carries the raw token.
|
|
178
|
+
result = _AT_ALL_RE.sub("@所有人", result)
|
|
158
179
|
if strip_bot_mentions:
|
|
159
180
|
result = re.sub(r"\s{2,}", " ", result).strip()
|
|
160
181
|
return result
|
|
@@ -25,7 +25,12 @@ from ..types import (
|
|
|
25
25
|
from .dedup import Deduper
|
|
26
26
|
from .flatten import flatten
|
|
27
27
|
from .interactive import fetch_interactive
|
|
28
|
-
from .mentions import
|
|
28
|
+
from .mentions import (
|
|
29
|
+
extract_mentions,
|
|
30
|
+
parse_at_tags,
|
|
31
|
+
resolve_mentions,
|
|
32
|
+
text_has_mention_all,
|
|
33
|
+
)
|
|
29
34
|
from .merge_forward import MergeForwardExpander
|
|
30
35
|
from .registry import parse_message_content
|
|
31
36
|
|
|
@@ -155,8 +160,18 @@ class InboundPipeline:
|
|
|
155
160
|
mentions: List[Mention] = list(ext.mention_list)
|
|
156
161
|
mentioned_all = ext.mentioned_all
|
|
157
162
|
if isinstance(content, TextContent):
|
|
163
|
+
# Feishu frequently ships ``@所有人`` messages with
|
|
164
|
+
# ``mentions = null`` — the only signal is an ``@_all``
|
|
165
|
+
# placeholder in ``content.text``. Without this probe the
|
|
166
|
+
# policy gate never sees ``mentioned_all=True`` so
|
|
167
|
+
# ``respond_to_mention_all`` / ``mention_all_blocked`` go
|
|
168
|
+
# silently skipped (TC-505).
|
|
169
|
+
if not mentioned_all and text_has_mention_all(content.text):
|
|
170
|
+
mentioned_all = True
|
|
158
171
|
content.text = resolve_mentions(content.text, ext)
|
|
159
172
|
elif isinstance(content, PostContent):
|
|
173
|
+
if not mentioned_all and text_has_mention_all(content.text):
|
|
174
|
+
mentioned_all = True
|
|
160
175
|
content.text = resolve_mentions(content.text, ext)
|
|
161
176
|
at_mentions, at_all, stripped = parse_at_tags(content.text)
|
|
162
177
|
content.text = stripped
|
lark_oapi/core/const.py
CHANGED
|
@@ -10003,7 +10003,7 @@ lark_oapi/channel/__init__.py,sha256=yZeVV8Zeyv53YufjfX6lHrDblyeMmhMS2I40sHRv0kM
|
|
|
10003
10003
|
lark_oapi/channel/_api_helpers.py,sha256=mv0pNmf1X2gG0gSCNfFXOFf_Y2Bl08FhOQCATc_vzqM,8874
|
|
10004
10004
|
lark_oapi/channel/_coerce.py,sha256=nHdt-y6iHuo_Fp7ZdLFkydG1pTMM-_wQJGJbOzcUiEI,7490
|
|
10005
10005
|
lark_oapi/channel/bot_identity.py,sha256=E2zMl1AYFCENi-HXtXH47WgZrWRpPMXiXyM38cvMdrw,5488
|
|
10006
|
-
lark_oapi/channel/channel.py,sha256=
|
|
10006
|
+
lark_oapi/channel/channel.py,sha256=uZJXERef6N9QYqUXzt2PmLBTNY3ZK_gwJZeczJHB8C8,59676
|
|
10007
10007
|
lark_oapi/channel/config.py,sha256=PEYUDqG_r3sOqAF7acrA3pDgVPmk16ChWVB71l1xd_U,9877
|
|
10008
10008
|
lark_oapi/channel/driver.py,sha256=5Ij0WaQP3kEhUqZqUoREgfwv052118lyj2A6bCiIg7Y,11863
|
|
10009
10009
|
lark_oapi/channel/errors.py,sha256=MbQIH95dxt3m99ofaMp_3oM1RJVvAMxW1A3SKiavAfk,7179
|
|
@@ -10017,13 +10017,13 @@ lark_oapi/channel/auth/uat_runner.py,sha256=pt4c99AJnIJzmPwNxngI6PrsrBK5Xpt5WDh8
|
|
|
10017
10017
|
lark_oapi/channel/card/__init__.py,sha256=NqmfsU_1sWIubZ-QtrhWTlEcHY5H_THNaVPSakJsjl0,277
|
|
10018
10018
|
lark_oapi/channel/card/builder.py,sha256=KbxbIBQTd0ojN0wWplxz1DdM-BK9Twllf_tUEXqw94w,11932
|
|
10019
10019
|
lark_oapi/channel/normalize/__init__.py,sha256=8VPAoSV7LHKLKQAS7LnqgDC0jHwldykhRxnvlng70cw,940
|
|
10020
|
-
lark_oapi/channel/normalize/comment.py,sha256=
|
|
10020
|
+
lark_oapi/channel/normalize/comment.py,sha256=vmAKhCKXyzNSrB0WBvHDDDDtSKhO_zxyZSby1R-k1KQ,5054
|
|
10021
10021
|
lark_oapi/channel/normalize/dedup.py,sha256=T8c2eUynJkOc83nEsxBxTSmaj0BFw8crjjVVGOuoY5I,3250
|
|
10022
10022
|
lark_oapi/channel/normalize/flatten.py,sha256=vvvKhFFF_HXHC9fBZr_hhN6eV0DCq-vsfHXlHJrYku4,804
|
|
10023
10023
|
lark_oapi/channel/normalize/interactive.py,sha256=EqwBy5EEW-wdfze4EQpBXQ7UIqzzxJNiBj8fSdWqSbM,2705
|
|
10024
|
-
lark_oapi/channel/normalize/mentions.py,sha256=
|
|
10024
|
+
lark_oapi/channel/normalize/mentions.py,sha256=w-hoYYw8QoiFP6Gf30l1MLSMJVfm4lpRURmGbOXufYU,7716
|
|
10025
10025
|
lark_oapi/channel/normalize/merge_forward.py,sha256=F88wE2dNywQOUpPeoEJNiM4l3U5we1EtP_Cnrpa8YCs,8513
|
|
10026
|
-
lark_oapi/channel/normalize/pipeline.py,sha256=
|
|
10026
|
+
lark_oapi/channel/normalize/pipeline.py,sha256=s2UU3-BvBQaAPKHhK5nFbZzBHTVUbkdHqfWb9Td0Rpk,9623
|
|
10027
10027
|
lark_oapi/channel/normalize/registry.py,sha256=xF-35gSSxwLjoHsWS3Cjz7n7K4rDMOIFvlXfLgqrEyg,11260
|
|
10028
10028
|
lark_oapi/channel/normalize/converters/__init__.py,sha256=7hIl1hfFXkQ78EkY1WzVt4vmWThGJP2ll7ozp5jCCck,2202
|
|
10029
10029
|
lark_oapi/channel/normalize/converters/_utils.py,sha256=VvDj6v6NwTSIY39Gsudsl4s6Hcq0inuAyfR87bYGrM8,1853
|
|
@@ -10073,7 +10073,7 @@ lark_oapi/channel/safety/processing_lock.py,sha256=G4Kcm8-aRCfrIKD_6aXu-inWG28tB
|
|
|
10073
10073
|
lark_oapi/channel/safety/stale_detector.py,sha256=FKVBdYwRddpFynpRZxTyznxL3gkJVaoTXnn_iQ9uY04,691
|
|
10074
10074
|
lark_oapi/channel/safety/types.py,sha256=WaWhUleLe21-57HTFH4cOwJpZb2OiwKuDxWxPAE0OYA,2124
|
|
10075
10075
|
lark_oapi/core/__init__.py,sha256=z286YdAEQ4kRDGuKsZb0Osua5KMcg4yqBsLmKsdW1Hw,263
|
|
10076
|
-
lark_oapi/core/const.py,sha256=
|
|
10076
|
+
lark_oapi/core/const.py,sha256=6t5WIv00UT_LdpXnzL0WEpzYp1vhpQP0p2TMAwgJmnI,608
|
|
10077
10077
|
lark_oapi/core/construct.py,sha256=CKsC-5bza7R01PDE2pWbyThlXi1oLAtjR0YD3Lex_sM,1980
|
|
10078
10078
|
lark_oapi/core/enum.py,sha256=ehxuylOjD3btFHTRtpA--QvEcvUlWimuqXIqglcg4Kw,471
|
|
10079
10079
|
lark_oapi/core/env_var.py,sha256=6xonyZzC_6Jyaotazj-ip0_yzB4wpZ4xArXIdPdPTck,169
|
|
@@ -10174,8 +10174,8 @@ lark_oapi/ws/pb/google/protobuf/pyext/cpp_message.py,sha256=GCWPkLCXd0QxY3yzNN-G
|
|
|
10174
10174
|
lark_oapi/ws/pb/google/protobuf/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10175
10175
|
lark_oapi/ws/pb/google/protobuf/util/json_format_pb2.py,sha256=4QRyr6gfGO_e2MNpt3z3MT1ZHfoR5SXRpmIs4aWrv6g,6375
|
|
10176
10176
|
lark_oapi/ws/pb/google/protobuf/util/json_format_proto3_pb2.py,sha256=uA8FCLxKYYUrFOlpjSlyWQWbtpodzBMZrIxM25dyJhM,14610
|
|
10177
|
-
lark_oapi-2.0.0.
|
|
10178
|
-
lark_oapi-2.0.0.
|
|
10179
|
-
lark_oapi-2.0.0.
|
|
10180
|
-
lark_oapi-2.0.0.
|
|
10181
|
-
lark_oapi-2.0.0.
|
|
10177
|
+
lark_oapi-2.0.0.dev4.dist-info/licenses/LICENSE,sha256=N2ZgITrKZ1yufqVdfmr4ixMPp4qJ_ly-1_YeCs3w-Uk,1083
|
|
10178
|
+
lark_oapi-2.0.0.dev4.dist-info/METADATA,sha256=o25IQ1h_x-Q-Od785pKtEtIRmyxdvpIka9q5gqcN3SY,4743
|
|
10179
|
+
lark_oapi-2.0.0.dev4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10180
|
+
lark_oapi-2.0.0.dev4.dist-info/top_level.txt,sha256=sDloiIa3R4quQp19_e1mf206wAxH0Wg1bvNnKzV8QJg,10
|
|
10181
|
+
lark_oapi-2.0.0.dev4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|