Skill: xiaohuaishu/competitor-radar (version 1.0.0)

--- name: competitor-radar description: 竞品动态监控雷达。自动抓取竞品博客RSS、GitHub Release、HackerNews讨论，用AI评分筛选重要动态，生成结构化报告。当需要了解竞品最新动向、监控行业变化时使用。 license: MIT metadata: openclaw: emoji: "🎯" requires: bins: ["python3"] --- # Competitor Radar 🎯 自动监控竞品动态，生成结构化分析报告。 ## 配置竞品 编辑 `config.yaml`，添加你要监控的竞品： ```yaml competitors: - name: "竞品名称" domain: "example.com" blog_rss: "https://example.com/rss.xml" github_org: "github-org-name" keywords: ["关键词1", "关键词2"] ``` ## 使用方法 ```bash # 扫描过去7天 python3 radar.py # 扫描过去14天，保存报告 python3 radar.py --days 14 --output report.md # 跳过AI评分（更快） python3 radar.py --no-ai # 使用自定义配置 python3 radar.py --config /path/to/my-config.yaml ``` ## 数据来源 - 官网博客 RSS Feed - GitHub Release / Tag - HackerNews 提及 ## 输出格式 Markdown 报告，按重要性分级（🔴重要 / 🟡值得关注 / ⚪一般）

path: radar.py
content: #!/usr/bin/env python3
"""
🎯 Competitor Radar — 竞品动态雷达
用法:
  python3 radar.py                    # 扫描过去7天（使用默认 config.yaml）
  python3 radar.py --days 14          # 扫描过去14天
  python3 radar.py --output report.md # 保存报告
  python3 radar.py --config my.yaml   # 自定义配置文件
"""
import sys, os, re, json, argparse, urllib.request, urllib.error
from datetime import datetime, timezone, timedelta
from pathlib import Path
from xml.etree import ElementTree as ET

SKILL_DIR = Path(__file__).parent

# ── LLM ──────────────────────────────────────────────────────────────────────

def llm_call(prompt: str, max_tokens: int = 600) -> str:
    url = "http://127.0.0.1:18790/anthropic/v1/messages"
    headers = {
        "Content-Type": "application/json",
        "x-api-key": "[REDACTED_SECRET]",
        "anthropic-version": "2023-06-01",
    }
    data = json.dumps({
        "model": "ppio/pa/claude-sonnet-4-6",
        "max_tokens": max_tokens,
        "system": "你是一个竞品分析助理，用简洁中文回答，不要废话。",
        "messages": [{"role": "user", "content": prompt}],
    }).encode()
    req = urllib.request.Request(url, data=data, headers=headers, method="POST")
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read())["content"][0]["text"]


# ── YAML 解析（极简，不依赖 pyyaml）────────────────────────────────────────

def parse_simple_yaml(text: str) -> dict:
    """极简 YAML 解析，支持本配置文件的格式"""
    result = {"competitors": []}
    current = None
    for line in text.splitlines():
        stripped = line.strip()
        if not stripped or stripped.startswith("#"):
            continue
        if stripped == "competitors:":
            continue
        if stripped.startswith("- name:"):
            if current:
                result["competitors"].append(current)
            current = {"name": stripped.split(":", 1)[1].strip().strip('"')}
        elif current and ":" in stripped:
            k, v = stripped.split(":", 1)
            k = k.strip()
            v = v.strip().strip('"')
            if k == "keywords":
                continue  # 跳过，下面单独处理
            current[k] = v
        elif current and stripped.startswith("- ") and "keywords" not in stripped:
            pass  # keyword list items — handled below
    if current:
        result["competitors"].append(current)
    # 重新解析 keywords
    comp_idx = -1
    in_kw = False
    for line in text.splitlines():
        stripped = line.strip()
        if stripped.startswith("- name:"):
            comp_idx += 1
            in_kw = False
            if "keywords" not in result["competitors"][comp_idx]:
                result["competitors"][comp_idx]["keywords"] = []
        elif stripped == "keywords:":
            in_kw = True
        elif in_kw and stripped.startswith("- ") and comp_idx >= 0:
            kw = stripped[2:].strip().strip('"')
            result["competitors"][comp_idx].setdefault("keywords", []).append(kw)
        elif ":" in stripped and not stripped.startswith("- "):
            in_kw = False
    return result


# ── 数据源抓取 ────────────────────────────────────────────────────────────────

def fetch_url(url: str, timeout: int = 8) -> str | None:
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 CompetitorRadar/1.0"})
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return resp.read().decode("utf-8", errors="replace")
    except Exception:
        return None


def parse_rss(xml_text: str, since: datetime) -> list:
    items = []
    try:
        root = ET.fromstring(xml_text)
        ns = {"atom": "http://www.w3.org/2005/Atom"}
        # RSS 2.0
        for item in root.findall(".//item"):
            title_el = item.find("title")
            link_el  = item.find("link")
            date_el  = item.find("pubDate")
            title = title_el.text if title_el is not None else ""
            link  = link_el.text  if link_el  is not None else ""
            pub   = date_el.text  if date_el  is not None else ""
            items.append({"title": title, "link": link, "date": pub, "source": "blog_rss"})
        # Atom
        for entry in root.findall(".//atom:entry", ns):
            title_el = entry.find("atom:title", ns)
            link_el  = entry.find("atom:link", ns)
            date_el  = entry.find("atom:updated", ns)
            title = title_el.text if title_el is not None else ""
            link  = link_el.get("href", "") if link_el is not None else ""
            pub   = date_el.text if date_el is not None else ""
            items.append({"title": title, "link": link, "date": pub, "source": "blog_rss"})
    except Exception:
        pass
    return items[:10]  # 最多10条


def fetch_github_releases(org: str, since: datetime) -> list:
    url = f"https://api.github.com/orgs/{org}/repos?per_page=10&sort=pushed"
    text = fetch_url(url)
    if not text:
        return []
    items = []
    try:
        repos = json.loads(text)
        for repo in repos[:5]:
            rname = repo.get("name", "")
            rel_url = f"https://api.github.com/repos/{org}/{rname}/releases?per_page=3"
            rel_text = fetch_url(rel_url)
            if rel_text:
                releases = json.loads(rel_text)
                for r in releases:
                    items.append({
                        "title": f"[{rname}] {r.get('name') or r.get('tag_name', '')}",
                        "link":  r.get("html_url", ""),
                        "date":  r.get("published_at", ""),
                        "source": "github_release",
                    })
    except Exception:
        pass
    return items[:8]


def fetch_hn_mentions(keywords: list) -> list:
    items = []
    for kw in keywords[:2]:
        url = f"https://hn.algolia.com/api/v1/search?query={urllib.parse.quote(kw)}&tags=story&hitsPerPage=5"
        text = fetch_url(url)
        if text:
            try:
                data = json.loads(text)
                for hit in data.get("hits", []):
                    items.append({
                        "title":  hit.get("title", ""),
                        "link":   hit.get("url") or f"https://news.ycombinator.com/item?id={hit.get('objectID')}",
                        "date":   hit.get("created_at", ""),
                        "source": "hackernews",
                    })
            except Exception:
                pass
    return items[:6]


import urllib.parse


# ── AI 评分 ───────────────────────────────────────────────────────────────────

def score_items(comp_name: str, items: list) -> list:
    if not items:
        return []
    titles_text = "\n".join([f"{i+1}. [{it['source']}] {it['title']}" for i, it in enumerate(items)])
    prompt = f"""以下是关于 {comp_name} 的最新动态，请给每条打分（1-5分，5=非常重要的产品/战略动态，1=无关紧要）。
只返回 JSON 数组，格式: [{{"idx":1,"score":4,"reason":"一句话理由"}}]

{titles_text}"""
    try:
        resp = llm_call(prompt, max_tokens=400)
        # 提取 JSON
        m = re.search(r'\[.*\]', resp, re.DOTALL)
        if m:
            scores = json.loads(m.group(0))
            score_map = {s["idx"]: s for s in scores}
            for i, it in enumerate(items):
                s = score_map.get(i+1, {})
                it["score"] = s.get("score", 3)
                it["reason"] = s.get("reason", "")
    except Exception:
        for it in items:
            it.setdefault("score", 3)
            it.setdefault("reason", "")
    return items


# ── 报告生成 ──────────────────────────────────────────────────────────────────

def build_report(results: dict, days: int) -> str:
    now = datetime.now(timezone(timedelta(hours=8)))
    lines = [
        f"# 🎯 竞品雷达报告 {now.strftime('%Y-%m-%d')}",
        f"",
        f"## 摘要",
    ]
    total = sum(len(v) for v in results.values())
    important = sum(1 for items in results.values() for it in items if it.get("score", 0) >= 4)
    lines.append(f"过去 **{days} 天**监控了 **{len(results)} 个竞品**，共收集 **{total} 条动态**，其中重要动态 **{important} 条**。")
    lines.append("")
    lines.append("---")

    for comp_name, items in results.items():
        lines.append(f"\n## {comp_name}\n")
        high   = [it for it in items if it.get("score", 0) >= 4]
        mid    = [it for it in items if it.get("score", 0) == 3]
        low    = [it for it in items if it.get("score", 0) <= 2]

        if high:
            lines.append("### 🔴 重要动态")
            for it in high:
                title = it["title"] or "(无标题)"
                link  = it["link"]
                src   = it["source"]
                reason = it.get("reason", "")
                if link:
                    lines.append(f"- **[{title}]({link})** — 来源: {src}")
                else:
                    lines.append(f"- **{title}** — 来源: {src}")
                if reason:
                    lines.append(f"  > {reason}")
        if mid:
            lines.append("\n### 🟡 值得关注")
            for it in mid:
                title = it["title"] or "(无标题)"
                link  = it["link"]
                src   = it["source"]
                if link:
                    lines.append(f"- [{title}]({link}) · {src}")
                else:
                    lines.append(f"- {title} · {src}")
        if low:
            lines.append("\n### ⚪ 一般")
            for it in low:
                title = it["title"] or "(无标题)"
                lines.append(f"- {title}")
        if not items:
            lines.append("_本周期内暂无数据（可能网络受限）_")

    lines.append(f"\n---\n*生成时间：{now.strftime('%Y-%m-%d %H:%M')} GMT+8 | 数据周期：最近 {days} 天*")
    return "\n".join(lines)


# ── 主程序 ────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="🎯 Competitor Radar — 竞品动态雷达")
    parser.add_argument("--days",   type=int, default=7,  help="监控天数（默认7）")
    parser.add_argument("--output", default="",           help="输出报告路径")
    parser.add_argument("--config", default=str(SKILL_DIR / "config.yaml"), help="配置文件路径")
    parser.add_argument("--no-ai",  action="store_true",  help="跳过 AI 评分（更快）")
    args = parser.parse_args()

    # 读取配置
    config_text = Path(args.config).read_text()
    config = parse_simple_yaml(config_text)
    competitors = config.get("competitors", [])
    since = datetime.now(timezone.utc) - timedelta(days=args.days)

    print(f"🎯 竞品雷达启动，监控 {len(competitors)} 个竞品，过去 {args.days} 天...")

    results = {}
    for comp in competitors:
        name = comp.get("name", "Unknown")
        print(f"  📡 正在抓取 {name} ...", end="", flush=True)
        items = []

        # RSS
        if comp.get("blog_rss"):
            text = fetch_url(comp["blog_rss"])
            if text:
                items.extend(parse_rss(text, since))

        # GitHub releases
        if comp.get("github_org"):
            items.extend(fetch_github_releases(comp["github_org"], since))

        # HackerNews
        if comp.get("keywords"):
            items.extend(fetch_hn_mentions(comp["keywords"]))

        # 去重（按标题）
        seen, unique = set(), []
        for it in items:
            key = it.get("title", "")[:60]
            if key not in seen and key:
                seen.add(key)
                unique.append(it)

        # AI 评分
        if not args.no_ai and unique:
            unique = score_items(name, unique)
        else:
            for it in unique:
                it.setdefault("score", 3)
                it.setdefault("reason", "")

        unique.sort(key=lambda x: x.get("score", 0), reverse=True)
        results[name] = unique
        print(f" {len(unique)} 条")

    report = build_report(results, args.days)

    if args.output:
        Path(args.output).write_text(report)
        print(f"\n✅ 报告已保存：{args.output}")
    else:
        print("\n" + "="*60)
        print(report)

    return 0


if __name__ == "__main__":
    sys.exit(main())


path: config.yaml
content: competitors:
  - name: "OpenAI"
    domain: "openai.com"
    blog_rss: "https://openai.com/blog/rss.xml"
    github_org: "openai"
    keywords: ["ChatGPT", "GPT-4", "OpenAI"]

  - name: "Anthropic"
    domain: "anthropic.com"
    blog_rss: "https://www.anthropic.com/rss.xml"
    github_org: "anthropic"
    keywords: ["Claude", "Anthropic"]

  - name: "Google DeepMind"
    domain: "deepmind.google"
    blog_rss: "https://deepmind.google/blog/rss.xml"
    github_org: "google-deepmind"
    keywords: ["Gemini", "DeepMind", "Google AI"]


path: _write_radar.py
content: #!/usr/bin/env python3
"""Helper script to write radar.py"""
import os

SCRIPT = r'''#!/usr/bin/env python3
"""Competitor Radar - 竞品动态监控雷达
Usage: python3 radar.py [--days 7] [--output report.md] [--config config.yaml] [--no-llm] [--verbose]
"""

import argparse, json, os, re, sys, urllib.error, urllib.parse, urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone, timedelta
from xml.etree import ElementTree as ET

# ── YAML 加载 ──────────────────────────────────────────────────────────────
try:
    import yaml
    def load_yaml(path):
        with open(path, "r", encoding="utf-8") as f:
            return yaml.safe_load(f)
    YAML_BACKEND = "pyyaml"
except ImportError:
    YAML_BACKEND = "builtin"
    def load_yaml(path):
        """极简 YAML 解析，仅支持本 skill 配置格式"""
        with open(path, "r", encoding="utf-8") as f:
            lines = f.readlines()
        result = {"competitors": []}
        current = None
        in_list_field = None
        for raw in lines:
            stripped = raw.rstrip()
            if not stripped or stripped.strip().startswith("#"):
                continue
            indent = len(raw) - len(raw.lstrip())
            content = stripped.strip()
            if content == "competitors:":
                continue
            elif indent == 2 and content.startswith("- name:"):
                current = {"name": content.split(":", 1)[1].strip().strip('"')}
                result["competitors"].append(current)
                in_list_field = None
            elif indent == 4 and current is not None:
                if content.startswith("- ") and in_list_field:
                    val = content[2:].strip().strip('"')
                    if isinstance(current.get(in_list_field), list):
                        current[in_list_field].append(val)
                elif ":" in content:
                    key, _, val = content.partition(":")
                    key = key.strip(); val = val.strip().strip('"')
                    if val.startswith("[") and val.endswith("]"):
                        inner = val[1:-1]
                        current[key] = [x.strip().strip('"\'') for x in inner.split(",") if x.strip()]
                        in_list_field = None
                    elif not val:
                        current[key] = []; in_list_field = key
                    else:
                        current[key] = val; in_list_field = None
            elif indent == 6 and current is not None and in_list_field:
                if content.startswith("- "):
                    val = content[2:].strip().strip('"')
                    if isinstance(current.get(in_list_field), list):
                        current[in_list_field].append(val)
        return result

# ── 全局配置 ──────────────────────────────────────────────────────────────
TIMEOUT = 8
LLM_URL = "http://127.0.0.1:18790/anthropic/v1/messages"
LLM_API_KEY = "[REDACTED_SECRET]"
LLM_MODEL = "ppio/pa/claude-sonnet-4-6"
VERBOSE = False

def log(m):
    if VERBOSE: print(f"[DEBUG] {m}", file=sys.stderr)
def warn(m): print(f"[WARN]  {m}", file=sys.stderr)
def info(m): print(f"[INFO]  {m}", file=sys.stderr)

# ── HTTP 工具 ─────────────────────────────────────────────────────────────
def http_get(url, extra_headers=None, timeout=TIMEOUT):
    try:
        headers = {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
            "Accept": "application/json, application/xml, text/xml, */*",
        }
        if extra_headers: headers.update(extra_headers)
        req = urllib.request.Request(url, headers=headers)
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return resp.read(), None
    except urllib.error.HTTPError as e: return None, f"HTTP {e.code}"
    except urllib.error.URLError as e: return None, f"连接失败: {e.reason}"
    except Exception as e: return None, f"{type(e).__name__}: {e}"

# ── LLM 调用 ──────────────────────────────────────────────────────────────
def llm_call(prompt, system="你是龙虾公司的信息分析助理", max_tokens=300):
    headers = {
        "Content-Type": "application/json",
        "x-api-key": LLM_API_KEY,
        "anthropic-version": "2023-06-01",
    }
    data = json.dumps({
        "model": LLM_MODEL, "max_tokens": max_tokens, "system": system,
        "messages": [{"role": "user", "content": prompt}]
    }).encode()
    req = urllib.request.Request(LLM_URL, data=data, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return json.loads(resp.read())["content"][0]["text"]
    except Exception as e:
        warn(f"LLM 失败: {e}"); return None

def score_item(title, summary, source, competitor_name):
    """对单条动态 LLM 评分，返回 (score:int, reason:str)"""
    prompt = (
        f"你是竞品情报分析师。评估以下动态重要性（AI创业公司视角）。\n\n"
        f"竞品：{competitor_name} | 来源：{source}\n"
        f"标题：{title}\n摘要：{(summary or '')[:400]}\n\n"
        f"只返回JSON（无markdown）：{{\"score\": <1-5>, \"reason\": \"<一句话核心摘要>\"}}\n\n"
        f"5=重大战略(新模型/产品/融资) 4=重要功能/合作 3=常规更新 2=次要 1=噪音"
    )
    resp = llm_call(prompt, max_tokens=150)
    if not resp: return 3, ""
    try:
        resp = resp.strip()
        if "```" in resp:
            m = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", resp, re.DOTALL)
            if m: resp = m.group(1)
        m = re.search(r"\{[^{}]+\}", resp, re.DOTALL)
        if m: resp = m.group(0)
        parsed = json.loads(resp)
        return max(1, min(5, int(parsed.get("score", 3)))), str(parsed.get("reason","")).strip()
    except Exception as e:
        log(f"评分解析失败: {e} | {resp[:80]}"); return 3, ""

# ── 日期工具 ──────────────────────────────────────────────────────────────
def parse_date(s):
    if not s: return None
    s = s.strip().replace(" GMT", " +0000").replace(" UTC", " +0000")
    fmts = [
        "%a, %d %b %Y %H:%M:%S %z", "%Y-%m-%dT%H:%M:%S%z",
        "%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S.%f%z",
        "%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d",
    ]
    for fmt in fmts:
        try:
            dt = datetime.strptime(s, fmt)
            return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
        except ValueError: continue
    return None

def strip_html(t):
    return re.sub(r"<[^>]+>", "", t or "").strip()

def fmt_date(dt):
    return dt.strftime("%Y-%m-%d") if dt else "未知"

# ── 数据源 1: RSS ─────────────────────────────────────────────────────────
def fetch_rss(competitor, cutoff):
    url = competitor.get("blog_rss")
    if not url: return [], None
    log(f"[RSS] {competitor['name']} -> {url}")
    data, err = http_get(url)
    if err: return [], f"RSS 抓取失败 ({err})"
    try: root = ET.fromstring(data)
    except ET.ParseError as e: return [], f"RSS XML 解析失败: {e}"

    items = []
    ATOM = "http://www.w3.org/2005/Atom"

    def add(title, link, summary, date_str):
        dt = parse_date(date_str)
        if dt and dt < cutoff: return
        items.append({
            "title": strip_html(title) or "无标题",
            "url": (link or "").strip(),
            "summary": strip_html(summary)[:500],
            "date": fmt_date(dt),
            "source": "官方博客",
        })

    # RSS 2.0
    for item in root.findall(".//item"):
        add(item.findtext("title",""), item.findtext("link",""),
            item.findtext("description","") or item.findtext("summary",""),
            item.findtext("pubDate","") or item.findtext("{http://purl.org/dc/elements/1.1/}date",""))

    # Atom
    for ns_pfx in [f"{{{ATOM}}}", ""]:
        for entry in root.findall(f"{ns_pfx}entry"):
            t = entry.find(f"{ns_pfx}title")
            l = entry.find(f"{ns_pfx}link")
            s = entry.find(f"{ns_pfx}summary") or entry.find(f"{ns_pfx}content")
            d = entry.find(f"{ns_pfx}updated") or entry.find(f"{ns_pfx}published")
            add(t.text if t is not None else "",
                l.get("href","") if l is not None else "",
                s.text if s is not None else "",
                d.text if d is not None else "")

    log(f"[RSS] {competitor['name']} 得 {len(items)} 条")
    return items[:20], None

# ── 数据源 2: GitHub Releases ─────────────────────────────────────────────
def fetch_github_releases(competitor, cutoff):
    org = competitor.get("github_org","")
    spec_repos = competitor.get("github_repos") or []
    if not org and not spec_repos: return [], None

    [REDACTED_SECRET]"GITHUB_TOKEN","")
    auth = {"Authorization": f"token {token}"} if token else {}

    repos = list(spec_repos)
    if not repos and org:
        data, err = http_get(
            f"https://api.github.com/orgs/{org}/repos?sort=updated&per_page=8&type=public",
            extra_headers=auth)
        if err: return [], f"GitHub org repos 失败: {err}"
        try:
            lst = json.loads(data)
            repos = [r["full_name"] for r in lst[:5]] if isinstance(lst, list) else []
        except: return [], "GitHub 响应解析失败"

    items = []
    for repo in repos[:8]:
        log(f"[GitHub] {repo}")
        data, err = http_get(
            f"https://api.github.com/repos/{repo}/releases?per_page=5",
            extra_headers=auth)
        if err:
            log(f"[GitHub] {repo} releases 失败: {err}")
            continue
        try:
            releases = json.loads(data)
            if not isinstance(releases, list): continue
            for rel in releases:
                dt = parse_date(rel.get("published_at",""))
                if dt and dt < cutoff: continue
                body = strip_html(rel.get("body",""))[:400]
                items.append({
                    "title": f"[{repo.split('/')[1]}] {rel.get('name') or rel.get('tag_name','release')}",
                    "url": rel.get("html_url",""),
                    "summary": body,
                    "date": fmt_date(dt),
                    "source": "GitHub Release",
                })
        except Exception as e:
            log(f"[GitHub] {repo} 解析失败: {e}")

    log(f"[GitHub] {competitor['name']} 得 {len(items)} 条")
    return items[:15], None

# ── 数据源 3: HackerNews ──────────────────────────────────────────────────
def fetch_hackernews(competitor, cutoff):
    keywords = competitor.get("keywords") or [competitor["name"]]
    if not keywords: return [], None

    items = []
    seen = set()

    for kw in keywords[:3]:
        encoded = urllib.parse.quote(kw)
        url = (f"https://hn.algolia.com/api/v1/search"
               f"?query={encoded}&tags=story"
               f"&numericFilters=created_at_i>{int(cutoff.timestamp())}"
               f"&hitsPerPage=5")
        log(f"[HN] 搜索 '{kw}'")
        data, err = http_get(url)
        if err:
            log(f"[HN] 搜索失败: {err}")
            continue
        try:
            result = json.loads(data)
            for hit in result.get("hits", []):
                hn_url = hit.get("url") or f"https://news.ycombinator.com/item?id={hit.get('objectID','')}"
                if hn_url in seen: continue
                seen.add(hn_url)
                title = hit.get("title","")
                if not title: continue
                dt = datetime.fromtimestamp(hit.get("created_at_i", 0), tz=timezone.utc)
                points = hit.get("points", 0) or 0
                num_comments = hit.get("num_comments", 0) or 0
                items.append({
                    "title": title, "url": hn_url,
                    "summary": f"HN讨论 | {points}分 | {num_comments}评论",
                    "date": fmt_date(dt), "source": "HackerNews",
                    "_score

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)