Skip to content

Commit bb54c82

Browse files
committed
fix: dedupe mention footer and reduce duplicate dispatch triggers
1 parent bf9d6e8 commit bb54c82

File tree

7 files changed

+125
-32
lines changed

7 files changed

+125
-32
lines changed

.github/workflows/dispatch_agents.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ jobs:
1616
if: |
1717
(github.event_name == 'issue_comment' &&
1818
github.event.issue.state == 'open' &&
19+
github.event.comment.user.type != 'Bot' &&
20+
!contains(fromJson('["moderator","reviewer_a","reviewer_b","summarizer","observer","pubmed_observer","arxiv_observer","video_manim"]'), github.event.comment.user.login) &&
1921
!contains(github.event.comment.body, '[Agent:') &&
2022
(contains(github.event.comment.body, '相关人员:') || contains(github.event.comment.body, '协作请求:'))) ||
2123
(github.event_name == 'issues' &&
2224
github.event.issue.state == 'open' &&
2325
(contains(github.event.issue.body, '相关人员:') || contains(github.event.issue.body, '协作请求:')))
2426
runs-on: ubuntu-latest
25-
# 允许 Agent 回复中的 @mention 触发分发
27+
# 仅处理用户触发的受控 @mention;系统 agent 由 orchestrator 处理
2628

2729
steps:
2830
- uses: actions/checkout@v4

.github/workflows/orchestrator.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ jobs:
8383
if: |
8484
github.event_name == 'issue_comment' &&
8585
github.event.issue.state == 'open' &&
86+
github.event.comment.user.type != 'Bot' &&
87+
!contains(fromJson('["moderator","reviewer_a","reviewer_b","summarizer","observer","pubmed_observer","arxiv_observer","video_manim"]'), github.event.comment.user.login) &&
8688
!contains(github.event.comment.body, '[Agent:') &&
8789
(contains(github.event.comment.body, '相关人员:') || contains(github.event.comment.body, '协作请求:'))
8890
runs-on: ubuntu-latest

src/issuelab/cli/dispatch.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,15 @@ def dispatch_mentions(
366366
print("Info: No matching agents found")
367367
return {"success_count": 0, "total_count": 0, "local_agents": [], "failed_agents": []}
368368

369-
print(f"Matched {len(matched_configs)} agents")
369+
matched_all_count = len(matched_configs)
370+
matched_configs = [cfg for cfg in matched_configs if str(cfg.get("agent_type", "")).lower() != "system"]
371+
skipped_system_count = matched_all_count - len(matched_configs)
372+
if skipped_system_count > 0:
373+
print(f"Info: Skipped {skipped_system_count} system agent(s); handled by orchestrator workflow")
374+
if not matched_configs:
375+
return {"success_count": 0, "total_count": 0, "local_agents": [], "failed_agents": []}
376+
377+
print(f"Matched {len(matched_configs)} user agents")
370378

371379
client_payload: dict[str, Any] = {
372380
"source_repo": source_repo,

src/issuelab/tools/github.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import os
5+
import re
56
import subprocess
67
import tempfile
78
from typing import Literal
@@ -14,6 +15,11 @@
1415

1516
# GitHub 评论最大长度
1617
MAX_COMMENT_LENGTH = 10000
18+
_CONTROLLED_FOOTER_RE = re.compile(
19+
r"(?:\n{1,3}(?:---\s*\n)?(?:\s*相关人员:\s*(?:@[a-zA-Z0-9_](?:[a-zA-Z0-9_-]*[a-zA-Z0-9_])?\s*)+"
20+
r"|\s*协作请求:\s*(?:\n\s*-\s*@.*)+)\s*)\Z",
21+
re.MULTILINE,
22+
)
1723

1824

1925
def _load_mentions_max_count() -> int:
@@ -188,8 +194,10 @@ def post_comment(
188194
if mentions:
189195
from issuelab.mention_policy import build_mention_section
190196

197+
# 避免重复追加:如果正文尾部已有受控区,先移除再注入过滤后的单一受控区。
198+
body_no_footer = _CONTROLLED_FOOTER_RE.sub("", body.rstrip())
191199
mention_section = build_mention_section(mentions, format_type="labeled")
192-
final_body = f"{body}\n\n{mention_section}"
200+
final_body = f"{body_no_footer}\n\n{mention_section}"
193201
else:
194202
final_body = body
195203

src/issuelab/utils/mentions.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,43 +23,44 @@ def extract_github_mentions(text: str | None) -> list[str]:
2323

2424

2525
def extract_controlled_mentions(text: str | None) -> list[str]:
26-
"""Extract mentions only from controlled collaboration sections.
27-
28-
Supported formats:
29-
- `相关人员: @alice @bob`
30-
- `协作请求:` followed by bullet lines like `- @alice`
31-
"""
26+
"""Extract mentions only from the trailing controlled collaboration section."""
3227
if not text:
3328
return []
3429

30+
lines = text.splitlines()
31+
if not lines:
32+
return []
33+
34+
end = len(lines) - 1
35+
while end >= 0 and not lines[end].strip():
36+
end -= 1
37+
if end < 0:
38+
return []
39+
3540
result: list[str] = []
3641
seen: set[str] = set()
37-
in_list_section = False
3842

39-
for raw_line in text.splitlines():
40-
line = raw_line.strip()
43+
# Case 1: 文末为单行 "相关人员: @a @b"
44+
tail = lines[end].strip()
45+
if "相关人员:" in tail:
46+
suffix = tail.split("相关人员:", 1)[1]
47+
for username in extract_github_mentions(suffix):
48+
if username not in seen:
49+
seen.add(username)
50+
result.append(username)
51+
return result
4152

42-
if "相关人员:" in line:
43-
suffix = line.split("相关人员:", 1)[1]
44-
for username in extract_github_mentions(suffix):
53+
# Case 2: 文末为列表形式
54+
list_lines: list[str] = []
55+
idx = end
56+
while idx >= 0 and re.match(r"^\s*-\s+@", lines[idx]):
57+
list_lines.append(lines[idx])
58+
idx -= 1
59+
60+
if list_lines and idx >= 0 and lines[idx].strip().startswith("协作请求:"):
61+
for line in reversed(list_lines):
62+
for username in extract_github_mentions(line):
4563
if username not in seen:
4664
seen.add(username)
4765
result.append(username)
48-
in_list_section = False
49-
continue
50-
51-
if line.startswith("协作请求:"):
52-
in_list_section = True
53-
continue
54-
55-
if in_list_section:
56-
if re.match(r"^\s*-\s+@", raw_line):
57-
for username in extract_github_mentions(raw_line):
58-
if username not in seen:
59-
seen.add(username)
60-
result.append(username)
61-
continue
62-
if line and not line.startswith("-"):
63-
in_list_section = False
64-
6566
return result

tests/test_cli.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ def test_controlled_section_list_format(self):
9292
mentions = parse_github_mentions(text, controlled_section_only=True)
9393
assert mentions == ["alice", "bob"]
9494

95+
def test_controlled_section_only_reads_trailing_block(self):
96+
"""Ignore historical mentions in body, read only trailing controlled block."""
97+
text = """
98+
历史记录:
99+
相关人员: @old_agent
100+
101+
正文继续...
102+
103+
---
104+
相关人员: @alice @bob
105+
"""
106+
mentions = parse_github_mentions(text, controlled_section_only=True)
107+
assert mentions == ["alice", "bob"]
108+
95109

96110
class TestMentionsCLI:
97111
"""Tests for mentions CLI."""
@@ -276,6 +290,40 @@ def test_match_triggers(self):
276290
assert len(matched) == 1
277291
assert matched[0]["owner"] == "carol"
278292

293+
def test_dispatch_mentions_skips_system_agents(self, monkeypatch):
294+
"""System agents should be ignored by dispatch workflow."""
295+
from issuelab.cli.dispatch import dispatch_mentions
296+
297+
monkeypatch.setattr(
298+
"issuelab.cli.dispatch.load_registry",
299+
lambda _agents_dir: {
300+
"moderator": {
301+
"owner": "moderator",
302+
"repository": "gqy20/IssueLab",
303+
"agent_type": "system",
304+
},
305+
"alice": {
306+
"owner": "alice",
307+
"repository": "gqy20/IssueLab",
308+
"agent_type": "user",
309+
},
310+
},
311+
)
312+
313+
summary = dispatch_mentions(
314+
mentions=["moderator", "alice"],
315+
agents_dir="agents",
316+
source_repo="gqy20/IssueLab",
317+
issue_number=1,
318+
dry_run=True,
319+
app_id="fake_app_id",
320+
app_private_key="fake_private_key",
321+
)
322+
323+
assert summary["total_count"] == 1
324+
assert summary["success_count"] == 1
325+
assert summary["local_agents"] == ["alice"]
326+
279327

280328
class TestDispatchCLI:
281329
"""Tests for dispatch CLI mention parsing."""

tests/test_github.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,30 @@ def fake_run(cmd, capture_output, text, env):
8787
assert content.strip().splitlines()[-1] == "相关人员: @a1 @a2 @a3"
8888

8989

90+
def test_post_comment_replaces_existing_controlled_footer(monkeypatch):
91+
body = "分析正文\n\n---\n相关人员: @legacy1 @legacy2"
92+
captured = {}
93+
94+
def fake_run(cmd, capture_output, text, env):
95+
captured["cmd"] = cmd
96+
return MagicMock(returncode=0)
97+
98+
monkeypatch.setattr("issuelab.tools.github.subprocess.run", fake_run)
99+
monkeypatch.setattr("issuelab.tools.github.os.unlink", lambda _path: None)
100+
monkeypatch.setattr(
101+
"issuelab.mention_policy.filter_mentions", lambda mentions, policy=None, issue_number=None: (mentions, [])
102+
)
103+
104+
result = post_comment(1, body, mentions=["alice"])
105+
assert result is True
106+
107+
cmd = captured.get("cmd", [])
108+
body_path = cmd[cmd.index("--body-file") + 1]
109+
content = Path(body_path).read_text(encoding="utf-8")
110+
assert content.count("相关人员:") == 1
111+
assert content.strip().splitlines()[-1] == "相关人员: @alice"
112+
113+
90114
def test_post_comment_keeps_original_body_without_normalize(monkeypatch):
91115
body = """[Agent: reviewer_a]
92116

0 commit comments

Comments
 (0)