fix(pipeline): preserve form callbacks and verify all group response rules

Allow routed Dify form callbacks to resume without another mention or
prefix while keeping ordinary messages subject to group response rules.

Cover mention, prefix, regex, random probability, and combined rules.
This commit is contained in:
BiFangKNT
2026-10-17 20:03:24 +08:00
parent 807de8d24a
commit b3574efd95
2 changed files with 115 additions and 7 deletions
@@ -37,10 +37,13 @@ class GroupRespondRuleCheckStage(stage.PipelineStage):
if query.launcher_type.value != 'group': # 只处理群消息
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
# Verified interaction callbacks resume the original run without a new
# mention or prefix. Routing ordinary messages only selects a pipeline;
# it must not bypass that pipeline's group response rules.
if query.variables and query.variables.get('_interaction_submission'):
# Interaction submissions and routed platform form callbacks resume the
# original run without another mention or prefix. Ordinary routing only
# selects a pipeline and must still obey its group response rules.
if query.variables and (
query.variables.get('_interaction_submission')
or (query.variables.get('_routed_by_rule') and query.variables.get('_dify_form_action'))
):
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
rules = query.pipeline_config['trigger']['group-respond-rules']
+108 -3
View File
@@ -167,13 +167,118 @@ async def test_routed_group_messages_still_obey_response_rules(mock_app, sample_
@pytest.mark.asyncio
async def test_interaction_submission_does_not_require_another_mention(mock_app, sample_query):
@pytest.mark.parametrize('routed', [False, True])
@pytest.mark.parametrize(
'overrides,text,mention,draw,matches,expected_text',
[
pytest.param({'regexp': ['^hello$']}, 'hello', False, 0.9, True, 'hello', id='regexp-match'),
pytest.param({'regexp': ['^hello$']}, 'bye', False, 0.9, False, 'bye', id='regexp-miss'),
pytest.param({'regexp': ['^bye$', '^hello$']}, 'hello', False, 0.9, True, 'hello', id='second-regexp'),
pytest.param({'regexp': ['[']}, 'hello', False, 0.9, False, 'hello', id='invalid-regexp'),
pytest.param({'random': 0}, 'hello', False, 0.0, False, 'hello', id='random-disabled'),
pytest.param({'random': 1}, 'hello', False, 0.999, True, 'hello', id='random-always'),
pytest.param({'random': 0.25}, 'hello', False, 0.249, True, 'hello', id='random-hit'),
pytest.param({'random': 0.25}, 'hello', False, 0.25, False, 'hello', id='random-boundary'),
pytest.param({}, 'hello', True, 0.0, False, 'hello', id='mention-disabled'),
pytest.param({}, '!bot hello', False, 0.0, False, '!bot hello', id='prefix-disabled'),
pytest.param({'prefix': ['!other ', '!bot ']}, '!bot hello', False, 0.9, True, 'hello', id='second-prefix'),
pytest.param(
{'at': True, 'prefix': ['!bot '], 'regexp': ['^hello$'], 'random': 0.25},
'hello',
False,
0.9,
True,
'hello',
id='combined-regexp-match',
),
pytest.param(
{'at': True, 'prefix': ['!bot '], 'regexp': ['^bye$'], 'random': 0.25},
'hello',
False,
0.1,
True,
'hello',
id='combined-random-fallback',
),
pytest.param(
{'at': True, 'prefix': ['!bot '], 'regexp': ['^bye$'], 'random': 0.25},
'hello',
False,
0.9,
False,
'hello',
id='combined-all-miss',
),
],
)
async def test_group_response_rule_configuration(
mock_app, sample_query, monkeypatch, routed, overrides, text, mention, draw, matches, expected_text
):
resprule, entities, _, _ = get_modules()
random_rule = import_module('langbot.pkg.pipeline.resprule.rules.random')
monkeypatch.setattr(random_rule.random, 'random', lambda: draw)
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
sample_query.adapter.bot_account_id = '999'
sample_query.variables = {'_routed_by_rule': routed}
sample_query.pipeline_config = {
'trigger': {'group-respond-rules': {'at': False, 'prefix': [], 'regexp': [], 'random': 0, **overrides}}
}
parts = [platform_message.Plain(text=text)]
if mention:
parts.insert(0, platform_message.At(target='999'))
sample_query.message_chain = platform_message.MessageChain(parts)
stage = resprule.GroupRespondRuleCheckStage(mock_app)
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'GroupRespondRuleCheckStage')
assert result.result_type == (entities.ResultType.CONTINUE if matches else entities.ResultType.INTERRUPT)
assert str(result.new_query.message_chain) == expected_text
@pytest.mark.asyncio
@pytest.mark.parametrize('callback_key', ['_interaction_submission', '_dify_form_action'])
async def test_interaction_callbacks_do_not_require_another_mention(mock_app, sample_query, callback_key):
resprule, entities, _, _ = get_modules()
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
sample_query.variables = {'_routed_by_rule': True, '_interaction_submission': {'interaction_id': 'form-1'}}
sample_query.adapter.bot_account_id = '999'
submission = {'action_id': 'approve'}
sample_query.variables = {'_routed_by_rule': True, callback_key: submission}
sample_query.message_chain = platform_message.MessageChain([platform_message.Plain(text='[Form Action: Approve]')])
sample_query.pipeline_config = {
'trigger': {'group-respond-rules': {'at': True, 'prefix': ['!bot '], 'regexp': [], 'random': 0}}
}
stage = resprule.GroupRespondRuleCheckStage(mock_app)
stage.rule_matchers = []
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'GroupRespondRuleCheckStage')
assert result.result_type == entities.ResultType.CONTINUE
assert str(result.new_query.message_chain) == '[Form Action: Approve]'
assert result.new_query.variables[callback_key] == submission
@pytest.mark.asyncio
@pytest.mark.parametrize(
'variables',
[
{'_routed_by_rule': True, '_dify_form_action': None},
{'_routed_by_rule': True, '_dify_form_action': {}},
{'_routed_by_rule': False, '_dify_form_action': {'action_id': 'approve'}},
],
)
async def test_incomplete_form_callback_does_not_bypass_response_rules(mock_app, sample_query, variables):
resprule, entities, _, _ = get_modules()
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
sample_query.adapter.bot_account_id = '999'
sample_query.variables = variables
sample_query.message_chain = platform_message.MessageChain([platform_message.Plain(text='[Form Action: Approve]')])
sample_query.pipeline_config = {
'trigger': {'group-respond-rules': {'at': True, 'prefix': ['!bot '], 'regexp': [], 'random': 0}}
}
stage = resprule.GroupRespondRuleCheckStage(mock_app)
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'GroupRespondRuleCheckStage')
assert result.result_type == entities.ResultType.INTERRUPT