Summary
For post (rich-text) messages that mention everyone (@所有人 / @all),
InboundMessage.mentioned_all is always False. As a result PolicyGate
never emits policy_mention_all_blocked, so a bot configured with
respond_to_mention_all=False still responds to broadcast @all posts.
Plain text @all messages are handled correctly — only the post code
path is affected.
Root cause
In lark_oapi/channel/normalize/pipeline.py, the PostContent branch of
InboundPipeline.process derives mentioned_all only from:
ext.mentioned_all (the event mentions[] array), and
text_has_mention_all(content.text) / parse_at_tags(content.text)
(the rendered text).
But for a post:
- Feishu does not populate
mentions[] for @all (already acknowledged in the
code comments on the text branch).
- The
@all signal lives in the post AST as an at node with
user_id == "all" ({"tag":"at","user_id":"all"}). converters/post.py
renders that node to the literal string @所有人 / @<user_name> — not
the @_all placeholder that text_has_mention_all looks for, and not an
<at> tag that parse_at_tags matches.
So neither probe fires and mentioned_all stays False. The post AST is never
inspected for the mention-all node.
Reproduction (confirmed on 1.6.9, also reproduces on 1.6.6)
import asyncio, json
from lark_oapi.channel.normalize.pipeline import InboundPipeline, PipelineConfig, PipelineDeps
from lark_oapi.channel.config import PolicyConfig
from lark_oapi.channel.safety.policy_gate import PolicyGate
async def main():
pipe = InboundPipeline(PipelineConfig(), PipelineDeps())
# A post that @-mentions everyone, like a broadcast notice
content = {"content": [[
{"tag": "at", "user_id": "all", "user_name": "Everyone"},
{"tag": "text", "text": " heads up everyone"},
]]}
event = {"message_id": "om", "chat_id": "oc", "chat_type": "group",
"message_type": "post", "content": json.dumps(content), "mentions": []}
sender = {"sender_id": {"open_id": "ou_asker"}, "sender_type": "user"}
inbound = await pipe.process("e", event, sender)
print("post mentioned_all =", inbound.mentioned_all)
gate = PolicyGate(PolicyConfig(require_mention=False, respond_to_mention_all=False))
gate.set_bot_open_id("ou_bot")
print("post decision =", gate.evaluate(inbound))
# same intent as plain text — handled correctly
tevent = dict(event, message_type="text", content=json.dumps({"text": "@_all heads up"}))
tin = await pipe.process("e2", tevent, sender)
print("text mentioned_all =", tin.mentioned_all)
print("text decision =", gate.evaluate(tin))
asyncio.run(main())
Output:
post mentioned_all = False
post decision = PolicyDecision(allowed=True, reason=None)
text mentioned_all = True
text decision = PolicyDecision(allowed=False, reason='policy_mention_all_blocked')
Expected: the post case should match the text case
(mentioned_all = True, decision policy_mention_all_blocked).
Suggested fix
In the PostContent branch, probe the raw post AST for an at node with
user_id == "all" (mirroring is_mention_all):
elif isinstance(content, PostContent):
if not mentioned_all and text_has_mention_all(content.text):
mentioned_all = True
# post @all is an `at` node (user_id == "all") in the AST, not an
# @_all placeholder in the rendered text — probe the raw post.
if not mentioned_all and _post_has_mention_all(content.post):
mentioned_all = True
...
where _post_has_mention_all walks the post documents/paragraphs for any at
element whose user_id (or open_id) equals "all".
Environment
- lark-oapi 1.6.9 (also 1.6.6)
- Python 3.11
Possibly related: #134 (InboundMessage.mentioned_bot always False for IM
messages) — same family of post/IM mention-resolution gaps.
Summary
For post (rich-text) messages that mention everyone (
@所有人/@all),InboundMessage.mentioned_allis alwaysFalse. As a resultPolicyGatenever emits
policy_mention_all_blocked, so a bot configured withrespond_to_mention_all=Falsestill responds to broadcast@allposts.Plain text
@allmessages are handled correctly — only the post codepath is affected.
Root cause
In
lark_oapi/channel/normalize/pipeline.py, thePostContentbranch ofInboundPipeline.processderivesmentioned_allonly from:ext.mentioned_all(the eventmentions[]array), andtext_has_mention_all(content.text)/parse_at_tags(content.text)(the rendered text).
But for a post:
mentions[]for@all(already acknowledged in thecode comments on the text branch).
@allsignal lives in the post AST as anatnode withuser_id == "all"({"tag":"at","user_id":"all"}).converters/post.pyrenders that node to the literal string
@所有人/@<user_name>— notthe
@_allplaceholder thattext_has_mention_alllooks for, and not an<at>tag thatparse_at_tagsmatches.So neither probe fires and
mentioned_allstaysFalse. The post AST is neverinspected for the mention-all node.
Reproduction (confirmed on 1.6.9, also reproduces on 1.6.6)
Output:
Expected: the
postcase should match thetextcase(
mentioned_all = True, decisionpolicy_mention_all_blocked).Suggested fix
In the
PostContentbranch, probe the raw post AST for anatnode withuser_id == "all"(mirroringis_mention_all):where
_post_has_mention_allwalks the post documents/paragraphs for anyatelement whose
user_id(oropen_id) equals"all".Environment
Possibly related: #134 (
InboundMessage.mentioned_botalways False for IMmessages) — same family of post/IM mention-resolution gaps.