#!/usr/bin/env python3 """CNB CodeBuddy NPC 通用监控脚本 — 自动盯所有 open Issue + PR。 设计: - 自动列出仓库所有 open 的 Issue(含 NPC 评论摘要) - 列出所有 open PR(NPC 推的 MR) - 输出稳定(无时间戳),任何新 Issue / 新评论 / 新 PR 都会让输出变化 - 配合 cronjob monitor_script 使用:有变化才触发 agent,静默零消耗 """ import json import os import urllib.error import urllib.request API = "https://api.cnb.cool" REPO = os.environ.get("CNB_REPO", "muchen-org/download-webhook") def load_token(): """从 ~/.hermes/.env 读取 CNB_TOKEN""" env_path = os.path.expanduser("~/.hermes/.env") try: with open(env_path) as f: for line in f: line = line.strip() if line.startswith("CNB_TOKEN="): return line.split("=", 1)[1].strip() except FileNotFoundError: pass return os.environ.get("CNB_TOKEN", "") def api_get(path): req = urllib.request.Request( f"{API}{path}", headers={ "Authorization": f"Bearer {load_token()}", "accept": "application/json", }, method="GET", ) try: with urllib.request.urlopen(req, timeout=20) as resp: return json.loads(resp.read().decode()) except urllib.error.HTTPError as e: return {"error": e.code} except Exception as e: return {"error": str(e)} def get_list(payload, key="items"): """兼容 list / {items: [...]} 两种返回""" if isinstance(payload, list): return payload if isinstance(payload, dict) and key in payload: return payload[key] if isinstance(payload, dict) and "data" in payload: return payload["data"] return [] def main(): lines = [] # 1. 所有 open Issue issues = api_get(f"/{REPO}/-/issues?state=open") issue_list = get_list(issues) open_issues = [i for i in issue_list if i.get("state") == "open"] lines.append(f"Open issues: {len(open_issues)}") for issue in sorted(open_issues, key=lambda x: int(x.get("number", 0))): num = issue.get("number", "?") comments = api_get(f"/{REPO}/-/issues/{num}/comments") comment_list = get_list(comments) # 只显示 NPC 评论(作者含 npc 或模型名) npc_comments = [ c for c in comment_list if "npc" in (c.get("author", {}).get("username", "")).lower() or "npc" in (c.get("author", {}).get("nickname", "")).lower() or (c.get("author", {}).get("username", "") or "").startswith("npc/") or (c.get("author", {}).get("nickname", "") or "").startswith("npc/") or c.get("author", {}).get("username", "") == "deepseek-v4-flash" ] title = (issue.get("title") or "").replace("\n", " ") lines.append(f" #{num} open: {title[:70]}") for c in npc_comments: body = (c.get("body") or "").replace("\n", " ")[:80] lines.append(f" NPC: {body}") if not npc_comments: lines.append(f" (no NPC activity)") # 2. 所有 open PR pulls = api_get(f"/{REPO}/-/pulls?state=open") pull_list = get_list(pulls) open_pulls = [p for p in pull_list if p.get("state") == "open"] lines.append(f"Open PRs: {len(open_pulls)}") for p in sorted(open_pulls, key=lambda x: int(x.get("number", 0))): author = p.get("author", {}).get("nickname", p.get("author", {}).get("username", "?")) lines.append(f" PR#{p.get('number', '?')} [{author}]: {(p.get('title') or '')[:60]}") print("\n".join(lines)) if __name__ == "__main__": main()