From 4b8fb0d290a84ca9149a7bccb462219a494ac61b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:09:03 +0000 Subject: [PATCH 1/3] Initial plan From 2ab5a66b3002abb076d79f60e2dff9cf2519cc86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:14:33 +0000 Subject: [PATCH 2/3] Add user-perspective sections and classification helpers to build_compact_report Agent-Logs-Url: https://github.com/hondayaya123/RepoWatchDog/sessions/55babaa3-779b-4878-a8a4-3dcd0a6b4f7e Co-authored-by: hondayaya123 <50391999+hondayaya123@users.noreply.github.com> --- scripts/watch_dog.py | 232 +++++++++++++++++++++++++++++++--------- tests/test_watch_dog.py | 2 + 2 files changed, 186 insertions(+), 48 deletions(-) diff --git a/scripts/watch_dog.py b/scripts/watch_dog.py index 996a4d2..4a392ba 100644 --- a/scripts/watch_dog.py +++ b/scripts/watch_dog.py @@ -54,6 +54,21 @@ MAX_COMPACT_ITEMS = 10 +# Keywords used to classify items from a user perspective (case-insensitive substring match) +_FEATURE_KEYWORDS: tuple[str, ...] = ( + "feature:", "feat:", "feature", "feat", "new feature", "add support", + "enable", "implement", "introduce", "enhance", "enhancement", "improve", +) +_BUG_KEYWORDS: tuple[str, ...] = ( + "bug:", "bug", "broken", "fail", "crash", "not work", + "never trigger", "doesn't fire", "not firing", "incorrect", "wrong", + "sends image as text", "underestimate", +) +_UNSUPPORTED_KEYWORDS: tuple[str, ...] = ( + "not support", "unsupported", "byom", "doesn't work with", + "missing support", "no support", +) + # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- @@ -369,14 +384,125 @@ def _generate_actions( return actions[:3] +# --------------------------------------------------------------------------- +# User-perspective classification helpers +# --------------------------------------------------------------------------- + + +def _user_classify_text(title: str, labels: list[str]) -> str: + """Classify an item as 'feature', 'bug', 'unsupported', or 'other'. + + Returns one of: ``'feature'``, ``'bug'``, ``'unsupported'``, ``'other'``. + """ + text = (title + " " + " ".join(labels)).lower() + if any(kw in text for kw in _UNSUPPORTED_KEYWORDS): + return "unsupported" + if any(kw in text for kw in _FEATURE_KEYWORDS): + return "feature" + if any(kw in text for kw in _BUG_KEYWORDS): + return "bug" + return "other" + + +def _classify_for_user( + releases: list[dict], + prs: list[dict], + issues: list[dict], + max_items: int = MAX_COMPACT_ITEMS, +) -> tuple[list[dict], list[dict], list[dict]]: + """Categorize items into new features, known issues, and unsupported items. + + Returns: + A tuple of (new_features, known_issues, not_supported) where each is + a list of dicts with keys ``type`` and ``item``. + """ + new_features: list[dict] = [] + known_issues: list[dict] = [] + not_supported: list[dict] = [] + + # All releases are "new features / improvements" from a user perspective + for r in releases: + new_features.append({"type": "release", "item": r}) + + # PRs: classify by title/labels + for pr in prs: + title = pr.get("title") or "" + labels = [lbl["name"] for lbl in pr.get("labels", [])] + kind = _user_classify_text(title, labels) + if kind == "bug": + known_issues.append({"type": "pr", "item": pr}) + elif kind == "unsupported": + not_supported.append({"type": "pr", "item": pr}) + else: # feature or other PRs are improvements + new_features.append({"type": "pr", "item": pr}) + + # Issues: classify by title/labels + for issue in issues: + title = issue.get("title") or "" + labels = [lbl["name"] for lbl in issue.get("labels", [])] + kind = _user_classify_text(title, labels) + if kind == "unsupported": + not_supported.append({"type": "issue", "item": issue}) + elif kind == "feature": + new_features.append({"type": "issue", "item": issue}) + else: # bug or other issues affect user experience + known_issues.append({"type": "issue", "item": issue}) + + return ( + new_features[:max_items], + known_issues[:max_items], + not_supported[:max_items], + ) + + +def _user_item_label(item: dict, item_type: str) -> str: + """Return a display title for the given item.""" + if item_type == "release": + tag = item.get("tag_name", "") + name = item.get("name") or tag + return f"{name}" if name == tag else f"{name} ({tag})" + return item.get("title") or "" + + +def _generate_user_tips( + new_features: list[dict], + known_issues: list[dict], + not_supported: list[dict], + all_releases: list[dict], +) -> list[str]: + """Return up to 3 user-friendly tips based on this week's activity.""" + tips: list[str] = [] + + release_count = sum(1 for f in new_features if f["type"] == "release") + if release_count > 0: + tips.append(f"本週有 {release_count} 個新版本發布,建議更新以取得最新功能與修復") + + feature_issues = sum(1 for f in new_features if f["type"] == "issue") + if feature_issues > 0: + tips.append(f"有 {feature_issues} 項新功能正在規劃中,可追蹤相關 Issue 了解進展") + + if known_issues: + tips.append(f"本週有 {len(known_issues)} 個已知問題,如遇到相關錯誤可查閱上方連結確認是否為已知問題") + + if not_supported: + tips.append(f"有 {len(not_supported)} 項功能目前有限制,使用前請留意") + + if not tips: + tips.append("本週無顯著變化,持續追蹤後續更新即可") + + return tips[:3] + + def build_compact_report( watch_repos: list[dict], token: str, since: datetime, important_labels: list[str] | None = None, ) -> str: - """Build a compact Traditional-Chinese report highlighting only critical changes. + """Build a compact Traditional-Chinese user-perspective summary report. + Focuses on what's new, what's broken, and what's not supported – + written for end users rather than developers. No LLM or paid API is required – all logic is rule-based. """ now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") @@ -406,26 +532,24 @@ def build_compact_report( except requests.HTTPError as exc: print(f"⚠️ Failed to fetch {owner}/{repo}: {exc}", file=sys.stderr) - critical = filter_critical_changes(all_releases, all_prs, all_issues) + new_features, known_issues, not_supported = _classify_for_user( + all_releases, all_prs, all_issues + ) total_changes = len(all_releases) + len(all_prs) + len(all_issues) repos_str = "、".join(f"{e['owner']}/{e['repo']}" for e in watch_repos) - # One-line summary - if not critical: - summary = f"本週監測 {repos_str},共 {total_changes} 項更新,**無發現重大變更**。" + # One-line summary (user perspective) + parts: list[str] = [] + if new_features: + parts.append(f"{len(new_features)} 項新功能或改善") + if known_issues: + parts.append(f"{len(known_issues)} 個已知問題") + if not_supported: + parts.append(f"{len(not_supported)} 項功能限制") + if parts: + summary = f"本週監測 {repos_str},共 {total_changes} 項更新,包含 {'、'.join(parts)}。" else: - sev3 = sum(1 for c in critical if c["severity"] >= 3) - sev2 = sum(1 for c in critical if c["severity"] == 2) - rest = len(critical) - sev3 - sev2 - parts: list[str] = [] - if sev3: - parts.append(f"{sev3} 項安全性問題") - if sev2: - parts.append(f"{sev2} 項破壞性變更") - if rest: - parts.append(f"{rest} 項重要更新") - changes_summary = "、".join(parts) - summary = f"本週監測 {repos_str},共 {total_changes} 項更新,發現 {changes_summary},請優先處理。" + summary = f"本週監測 {repos_str},共 {total_changes} 項更新,本週無顯著變化。" sections: list[str] = [ "# 📦 RepoWatchDog 週報摘要", @@ -437,51 +561,63 @@ def build_compact_report( "", ] - # ── 🔥 重大變更 ────────────────────────────────────────────────────────── - sections.append("## 🔥 重大變更") + # ── ✨ 新功能 / 改善 ────────────────────────────────────────────────────── + sections.append("## ✨ 本週新功能 / 改善") sections.append("") - if not critical: - sections.append("_本週無重大變更。_") + if not new_features: + sections.append("_本週無新功能或改善。_") sections.append("") else: - for idx, entry in enumerate(critical, 1): + for entry in new_features: item = entry["item"] item_type = entry["type"] - severity = entry["severity"] - - title = item.get("title") or item.get("name") or item.get("tag_name") or "" + title = _user_item_label(item, item_type) url = item.get("html_url", "") repo_label = item.get("_repo", "") + icon = "🆕" if item_type == "release" else "🔧" + sections.append(f"- {icon} [{title}]({url})({repo_label})") + sections.append("") - if severity >= 3: - badge = "🔴 安全" - elif severity == 2: - badge = "🟠 破壞性" - else: - badge = "🟡 重要" - - impact, action = _get_impact_and_action(item, item_type, severity) + # ── ⚠️ 已知問題 ────────────────────────────────────────────────────────── + sections.append("## ⚠️ 已知問題(使用中可能遇到)") + sections.append("") - sections.append(f"### {idx}. [{badge}] {title}") - sections.append("") - sections.append(f"- **影響:** {impact}") - sections.append(f"- **我需要做:** {action}") - sections.append(f"- **參考連結:** [{repo_label}]({url})") - sections.append("") + if not known_issues: + sections.append("_本週無已知問題回報。_") + sections.append("") + else: + for entry in known_issues: + item = entry["item"] + item_type = entry["type"] + title = _user_item_label(item, item_type) + url = item.get("html_url", "") + repo_label = item.get("_repo", "") + sections.append(f"- ❗ [{title}]({url})({repo_label})") + sections.append("") - # ── 🛡️ 風險與注意事項 ──────────────────────────────────────────────────── - sections.append("## 🛡️ 風險與注意事項") - sections.append("") - for i, risk in enumerate(_generate_risks(critical, all_releases), 1): - sections.append(f"{i}. {risk}") + # ── 🚫 目前不支援 / 限制 ────────────────────────────────────────────────── + sections.append("## 🚫 目前不支援 / 限制") sections.append("") - # ── ✅ 建議行動 checklist ───────────────────────────────────────────────── - sections.append("## ✅ 建議行動") + if not not_supported: + sections.append("_本週無功能限制回報。_") + sections.append("") + else: + for entry in not_supported: + item = entry["item"] + item_type = entry["type"] + title = _user_item_label(item, item_type) + url = item.get("html_url", "") + repo_label = item.get("_repo", "") + sections.append(f"- 🚫 [{title}]({url})({repo_label})") + sections.append("") + + # ── 💡 本週小結 ────────────────────────────────────────────────────────── + sections.append("## 💡 本週小結") sections.append("") - for action_item in _generate_actions(critical, all_releases, all_prs, all_issues): - sections.append(f"- [ ] {action_item}") + for tip in _generate_user_tips(new_features, known_issues, not_supported, all_releases): + sections.append(f"- {tip}") sections.append("") sections.append("---") diff --git a/tests/test_watch_dog.py b/tests/test_watch_dog.py index 5e804cb..a848cb8 100644 --- a/tests/test_watch_dog.py +++ b/tests/test_watch_dog.py @@ -15,9 +15,11 @@ from watch_dog import ( _build_llm_prompt, + _classify_for_user, _compute_severity, _is_major_version_bump, _parse_dt, + _user_classify_text, build_compact_report, build_report, fetch_commit_stats, From 493f4f8a35fbaa6663456e907ade28bd1c2a9d90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:17:21 +0000 Subject: [PATCH 3/3] Make compact report fully Traditional Chinese with category labels on each item Agent-Logs-Url: https://github.com/hondayaya123/RepoWatchDog/sessions/55babaa3-779b-4878-a8a4-3dcd0a6b4f7e Co-authored-by: hondayaya123 <50391999+hondayaya123@users.noreply.github.com> --- scripts/watch_dog.py | 22 +++++--- tests/test_watch_dog.py | 117 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 124 insertions(+), 15 deletions(-) diff --git a/scripts/watch_dog.py b/scripts/watch_dog.py index 4a392ba..783446d 100644 --- a/scripts/watch_dog.py +++ b/scripts/watch_dog.py @@ -56,11 +56,11 @@ # Keywords used to classify items from a user perspective (case-insensitive substring match) _FEATURE_KEYWORDS: tuple[str, ...] = ( - "feature:", "feat:", "feature", "feat", "new feature", "add support", + "feature", "feat", "new feature", "add support", "enable", "implement", "introduce", "enhance", "enhancement", "improve", ) _BUG_KEYWORDS: tuple[str, ...] = ( - "bug:", "bug", "broken", "fail", "crash", "not work", + "bug", "broken", "fail", "crash", "not work", "never trigger", "doesn't fire", "not firing", "incorrect", "wrong", "sends image as text", "underestimate", ) @@ -575,8 +575,16 @@ def build_compact_report( title = _user_item_label(item, item_type) url = item.get("html_url", "") repo_label = item.get("_repo", "") - icon = "🆕" if item_type == "release" else "🔧" - sections.append(f"- {icon} [{title}]({url})({repo_label})") + if item_type == "release": + label = "新版本" + icon = "🆕" + elif item_type == "issue": + label = "功能規劃中" + icon = "💡" + else: + label = "功能改善" + icon = "🔧" + sections.append(f"- {icon} **【{label}】** [{title}]({url})(來源:{repo_label})") sections.append("") # ── ⚠️ 已知問題 ────────────────────────────────────────────────────────── @@ -593,7 +601,7 @@ def build_compact_report( title = _user_item_label(item, item_type) url = item.get("html_url", "") repo_label = item.get("_repo", "") - sections.append(f"- ❗ [{title}]({url})({repo_label})") + sections.append(f"- ❗ **【已知錯誤】** [{title}]({url})(來源:{repo_label})") sections.append("") # ── 🚫 目前不支援 / 限制 ────────────────────────────────────────────────── @@ -610,7 +618,7 @@ def build_compact_report( title = _user_item_label(item, item_type) url = item.get("html_url", "") repo_label = item.get("_repo", "") - sections.append(f"- 🚫 [{title}]({url})({repo_label})") + sections.append(f"- 🚫 **【功能限制】** [{title}]({url})(來源:{repo_label})") sections.append("") # ── 💡 本週小結 ────────────────────────────────────────────────────────── @@ -623,7 +631,7 @@ def build_compact_report( sections.append("---") sections.append("") sections.append( - "_Generated by [RepoWatchDog](https://github.com/hondayaya123/RepoWatchDog)_ 🐶" + "_由 [RepoWatchDog](https://github.com/hondayaya123/RepoWatchDog) 自動產生_ 🐶" ) return "\n".join(sections) diff --git a/tests/test_watch_dog.py b/tests/test_watch_dog.py index a848cb8..71a37a5 100644 --- a/tests/test_watch_dog.py +++ b/tests/test_watch_dog.py @@ -829,17 +829,20 @@ def test_build_compact_report_structure(mock_get): report = build_compact_report(watch_repos, token="fake", since=since) assert "# 📦 RepoWatchDog 週報摘要" in report - assert "## 🔥 重大變更" in report - assert "## 🛡️ 風險與注意事項" in report - assert "## ✅ 建議行動" in report - # At least one action item checkbox - assert "- [ ]" in report + assert "## ✨ 本週新功能 / 改善" in report + assert "## ⚠️ 已知問題(使用中可能遇到)" in report + assert "## 🚫 目前不支援 / 限制" in report + assert "## 💡 本週小結" in report + # Items should have Chinese category labels + assert "【新版本】" in report or "【功能改善】" in report or "【功能規劃中】" in report # Summary line should be in Traditional Chinese assert "本週監測" in report + # Footer should be in Chinese + assert "自動產生" in report @patch("watch_dog.requests.get") -def test_build_compact_report_no_critical_changes(mock_get): +def test_build_compact_report_no_activity(mock_get): since = datetime(2024, 6, 1, 0, 0, 0, tzinfo=timezone.utc) watch_repos = [{"owner": "example", "repo": "repo", "description": "Test repo"}] @@ -854,6 +857,104 @@ def test_build_compact_report_no_critical_changes(mock_get): report = build_compact_report(watch_repos, token="fake", since=since) - assert "本週無重大變更" in report - assert "## 🔥 重大變更" in report + assert "## ✨ 本週新功能 / 改善" in report + assert "## ⚠️ 已知問題(使用中可能遇到)" in report + assert "_本週無已知問題回報。_" in report + + +# --------------------------------------------------------------------------- +# _user_classify_text +# --------------------------------------------------------------------------- + + +def test_user_classify_text_feature(): + assert _user_classify_text("FEATURE: Subagent skills", []) == "feature" + + +def test_user_classify_text_feature_via_label(): + assert _user_classify_text("Some PR", ["enhancement"]) == "feature" + + +def test_user_classify_text_bug(): + assert _user_classify_text("BUG: Task tool crashes", []) == "bug" + + +def test_user_classify_text_bug_label(): + assert _user_classify_text("Something is broken", ["bug"]) == "bug" + + +def test_user_classify_text_unsupported(): + assert _user_classify_text("view tool doesn't work with BYOM", []) == "unsupported" + + +def test_user_classify_text_unsupported_keyword(): + assert _user_classify_text("Feature not support for this provider", []) == "unsupported" + + +def test_user_classify_text_other(): + assert _user_classify_text("Update README", []) == "other" + + +def test_user_classify_text_unsupported_wins_over_feature(): + # "unsupported" is checked before "feature" so that titles like + # "Add support for BYOM – not support yet" correctly land in the + # limitations bucket rather than the new-features bucket. + assert _user_classify_text("Add support for BYOM – not support yet", []) == "unsupported" + + +# --------------------------------------------------------------------------- +# _classify_for_user +# --------------------------------------------------------------------------- + + +def test_classify_for_user_releases_are_new_features(): + features, issues, unsupported = _classify_for_user( + releases=[MOCK_RELEASE], prs=[], issues=[] + ) + assert len(features) == 1 + assert features[0]["type"] == "release" + assert issues == [] + assert unsupported == [] + + +def test_classify_for_user_bug_issue_goes_to_known_issues(): + bug_issue = {**MOCK_ISSUE, "title": "BUG: Something crashes", "labels": [{"name": "bug"}]} + features, issues, unsupported = _classify_for_user( + releases=[], prs=[], issues=[bug_issue] + ) + assert len(issues) == 1 + assert features == [] + assert unsupported == [] + + +def test_classify_for_user_unsupported_issue(): + ns_issue = {**MOCK_ISSUE, "title": "view tool doesn't work with BYOM", "labels": []} + features, issues, unsupported = _classify_for_user( + releases=[], prs=[], issues=[ns_issue] + ) + assert len(unsupported) == 1 + assert features == [] + assert issues == [] + + +def test_classify_for_user_feature_issue(): + feat_issue = {**MOCK_ISSUE, "title": "FEATURE: Add new skill system", "labels": []} + features, issues, unsupported = _classify_for_user( + releases=[], prs=[], issues=[feat_issue] + ) + assert len(features) == 1 + assert features[0]["type"] == "issue" + assert issues == [] + + +def test_classify_for_user_respects_max_items(): + many_issues = [ + {**MOCK_ISSUE, "number": i, "title": f"BUG: crash #{i}", "labels": [{"name": "bug"}]} + for i in range(15) + ] + features, issues, unsupported = _classify_for_user( + releases=[], prs=[], issues=many_issues, max_items=5 + ) + assert len(issues) == 5 +