Skill: jpengcheng523-netizen/quannwang-news-aggregator (version 1.0.0)

# 全网新闻聚合 Skill — News Aggregator > 聚合多个信息源，按关键词过滤，自动生成每日简报并推送到飞书。 **版本**: 1.0.0 | **作者**: 肥肥🐶 | **分类**: 效率工具 > 信息聚合 --- ## 功能特性 - 🔍 **多信源抓取**: 支持 RSS 订阅源、网页爬取（通用 URL） - 🏷️ **关键词过滤**: 精确匹配 + 模糊匹配，可配置多个关键词分类 - 📰 **每日简报**: 自动按科技/财经/行业等分类整理新闻 - 📤 **飞书推送**: 生成 Markdown 格式简报，一键推送到指定会话 - ⏰ **定时任务**: 支持 Cron 表达式配置抓取时间（默认每天 08:00 UTC） - 🔄 **去重机制**: 基于标题相似度自动去重 - 💾 **历史存储**: 本地存储已推送新闻，防止重复推送 --- ## 安装方式 ### 方式一：通过 OpenClaw Skill 市场安装（待上线） ```bash openclaw skill install news-aggregator ``` ### 方式二：手动安装 ```bash # 克隆到本地 skills 目录 git clone <repo_url> /home/gem/workspace/agent/workspace/skills/news-aggregator # 安装依赖 cd /home/gem/workspace/agent/workspace/skills/news-aggregator npm install ``` --- ## 配置说明 首次使用需配置 `config.json`： ```json { "feishu": { "chat_id": "oc_xxxxxxxx", // 推送目标群/会话 ID "receive_id_type": "chat_id" // chat_id（群聊）或 open_id（私聊） }, "keywords": { "科技": ["AI", "人工智能", "大模型", "LLM", "ChatGPT"], "财经": ["经济", "股市", "加密货币", "比特币"], "行业": ["新能源", "半导体", "自动驾驶"] }, "sources": [ { "name": "36氪", "url": "https://36kr.com/feed", "type": "rss" }, { "name": "少数派", "url": "https://sspai.com/feed", "type": "rss" }, { "name": "虎嗅", "url": "https://www.huxiu.com/rss/0.xml", "type": "rss" } ], "schedule": "0 8 * * *", "maxNewsPerCategory": 10, "historyFile": "./data/history.json" } ``` --- ## 使用方法 ### 启动定时任务 ```bash node scripts/scheduler.js ``` ### 手动触发一次抓取 ```bash node scripts/fetch.js ``` ### 手动推送简报 ```bash node scripts/push.js ``` ### 一键运行（抓取 + 生成 + 推送） ```bash node scripts/run.js ``` --- ## 脚本说明 | 脚本 | 功能 | |------|------| | `fetch.js` | 从配置的信源抓取最新新闻 | | `filter.js` | 按关键词过滤并分类新闻 | | `digest.js` | 生成每日简报 Markdown | | `push.js` | 将简报推送到飞书 | | `run.js` | 串联：抓取 → 过滤 → 生成 → 推送 | | `scheduler.js` | 基于 Cron 的定时调度器 | --- ## 数据结构 ### 新闻条目 ```json { "title": "新闻标题", "url": "https://example.com/article", "source": "信源名称", "publishedAt": "2026-04-22T10:00:00Z", "summary": "摘要内容（可选）", "category": "科技" } ``` --- ## 定价参考 | 方案 | 价格 | 功能 | |------|------|------| | 免费版 | ¥0 | 3 个信源、每日 1 次、单一分类 | | 专业版 | ¥29/月 | 10 个信源、每小时抓取、多分类 | | 企业版 | ¥99/月 | 无限信源、自定义信源、API 接口、多渠道推送 | --- ## 技术栈 - **Node.js 18+** - **RSS 解析**: `rss-parser` - **网页爬取**: `axios` + `cheerio` - **飞书 SDK**: `openclaw-lark`（内置） - **定时任务**: `node-cron` --- ## 注意事项 1. 请遵守各信源的 `robots.txt` 和使用条款 2. 建议抓取间隔不小于 30 分钟 3. 首次使用请先测试 `fetch.js` 确认信源可用 4. 飞书推送需要机器人已在目标会话中 --- ## 免责声明 本 Skill 仅供个人学习研究使用，请勿用于商业爬虫或其他违规用途。

path: config.example.json
content: {
  "feishu": {
    "chat_id": "oc_xxxxxxxx",
    "receive_id_type": "chat_id"
  },
  "keywords": {
    "科技": ["AI", "人工智能", "大模型", "LLM", "ChatGPT", "机器学习"],
    "财经": ["经济", "股市", "加密货币", "比特币", "美股", "A股"],
    "行业": ["新能源", "半导体", "自动驾驶", "智能汽车"]
  },
  "sources": [
    {
      "name": "36氪",
      "url": "https://36kr.com/feed",
      "type": "rss"
    },
    {
      "name": "虎嗅",
      "url": "https://www.huxiu.com/rss/0.xml",
      "type": "rss"
    },
    {
      "name": "少数派",
      "url": "https://sspai.com/feed",
      "type": "rss"
    },
    {
      "name": "IT之家",
      "url": "https://www.ithome.com/rss/",
      "type": "rss"
    },
    {
      "name": "Solidot",
      "url": "https://www.solidot.org/index.rss",
      "type": "rss"
    }
  ],
  "schedule": "0 8 * * *",
  "maxNewsPerCategory": 10,
  "historyFile": "./data/history.json"
}


path: package-lock.json
content: {
  "name": "news-aggregator",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "news-aggregator",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "axios": "^1.6.0",
        "cheerio": "^1.0.0",
        "node-cron": "^3.0.3",
        "rss-parser": "^3.13.0"
      }
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/axios": {
      "version": "1.15.2",
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "follow-redirects": "^1.15.11",
        "form-data": "^4.0.5",
        "proxy-from-env": "^2.1.0"
      }
    },
    "node_modules/boolbase": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/cheerio": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "cheerio-select": "^2.1.0",
        "dom-serializer": "^2.0.0",
        "domhandler": "^5.0.3",
        "domutils": "^3.2.2",
        "encoding-sniffer": "^0.2.1",
        "htmlparser2": "^10.1.0",
        "parse5": "^7.3.0",
        "parse5-htmlparser2-tree-adapter": "^7.1.0",
        "parse5-parser-stream": "^7.1.2",
        "undici": "^7.19.0",
        "whatwg-mimetype": "^4.0.0"
      },
      "engines": {
        "node": ">=20.18.1"
      },
      "funding": {
        "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
      }
    },
    "node_modules/cheerio-select": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "boolbase": "^1.0.0",
        "css-select": "^5.1.0",
        "css-what": "^6.1.0",
        "domelementtype": "^2.3.0",
        "domhandler": "^5.0.3",
        "domutils": "^3.0.1"
      },
      "funding": {
        "url": "https://github.com/sponsors/fb55"
      }
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/css-select": {
      "version": "5.2.2",
      "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "boolbase": "^1.0.0",
        "css-what": "^6.1.0",
        "domhandler": "^5.0.2",
        "domutils": "^3.0.1",
        "nth-check": "^2.0.1"
      },
      "funding": {
        "url": "https://github.com/sponsors/fb55"
      }
    },
    "node_modules/css-what": {
      "version": "6.2.2",
      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">= 6"
      },
      "funding": {
        "url": "https://github.com/sponsors/fb55"
      }
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/dom-serializer": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "domelementtype": "^2.3.0",
        "domhandler": "^5.0.2",
        "entities": "^4.2.0"
      },
      "funding": {
        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
      }
    },
    "node_modules/domelementtype": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/fb55"
        }
      ],
      "license": "BSD-2-Clause"
    },
    "node_modules/domhandler": {
      "version": "5.0.3",
      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "domelementtype": "^2.3.0"
      },
      "engines": {
        "node": ">= 4"
      },
      "funding": {
        "url": "https://github.com/fb55/domhandler?sponsor=1"
      }
    },
    "node_modules/domutils": {
      "version": "3.2.2",
      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "dom-serializer": "^2.0.0",
        "domelementtype": "^2.3.0",
        "domhandler": "^5.0.3"
      },
      "funding": {
        "url": "https://github.com/fb55/domutils?sponsor=1"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/encoding-sniffer": {
      "version": "0.2.1",
      "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "iconv-lite": "^0.6.3",
        "whatwg-encoding": "^3.1.1"
      },
      "funding": {
        "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
      }
    },
    "node_modules/entities": {
      "version": "4.5.0",
      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.12"
      },
      "funding": {
        "url": "https://github.com/fb55/entities?sponsor=1"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/follow-redirects": {
      "version": "1.16.0",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/RubenVerborgh"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=4.0"
      },
      "peerDependenciesMeta": {
        "debug": {
          "optional": true
        }
      }
    },
    "node_modules/form-data": {
      "version": "4.0.5",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/htmlparser2": {
      "version": "10.1.0",
      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        "https://github.com/fb55/htmlparser2?sponsor=1",
        {
          "type": "github",
          "url": "https://github.com/sponsors/fb55"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "domelementtype": "^2.3.0",
        "domhandler": "^5.0.3",
        "domutils": "^3.2.2",
        "entities": "^7.0.1"
      }
    },
    "node_modules/htmlparser2/node_modules/entities": {
      "version": "7.0.1",
      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.12"
      },
      "funding": {
        "url": "https://github.com/fb55/entities?sponsor=1"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/node-cron": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "uuid": "8.3.2"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/nth-check": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "boolbase": "^1.0.0"
      },
      "funding": {
        "url": "https://github.com/fb55/nth-check?sponsor=1"
      }
    },
    "node_modules/parse5": {
      "version": "7.3.0",
      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "entities": "^6.0.0"
      },
      "funding": {
        "url": "https://github.com/inikulin/parse5?sponsor=1"
      }
    },
    "node_modules/parse5-htmlparser2-tree-adapter": {
      "version": "7.1.0",
      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "domhandler": "^5.0.3",
        "parse5": "^7.0.0"
      },
      "funding": {
        "url": "https://github.com/inikulin/parse5?sponsor=1"
      }
    },
    "node_modules/parse5-parser-stream": {
      "version": "7.1.2",
      "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "parse5": "^7.0.0"
      },
      "funding": {
        "url": "https://github.com/inikulin/parse5?sponsor=1"
      }
    },
    "node_modules/parse5/node_modules/entities": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.12"
      },
      "funding": {
        "url": "https://github.com/fb55/entities?sponsor=1"
      }
    },
    "node_modules/proxy-from-env": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/rss-parser": {
      "version": "3.13.0",
      "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "entities": "^2.0.3",
        "xml2js": "^0.5.0"
      }
    },
    "node_modules/rss-parser/node_modules/entities": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "funding": {
        "url": "https://github.com/fb55/entities?sponsor=1"
      }
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/sax": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BlueOak-1.0.0",
      "engines": {
        "node": ">=11.0.0"
      }
    },
    "node_modules/undici": {
      "version": "7.25.0",
      "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=20.18.1"
      }
    },
    "node_modules/uuid": {
      "version": "8.3.2",
      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "bin": {
        "uuid": "dist/bin/uuid"
      }
    },
    "node_modules/whatwg-encoding": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
      "license": "MIT",
      "dependencies": {
        "iconv-lite": "0.6.3"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/whatwg-mimetype": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/xml2js": {
      "version": "0.5.0",
      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "sax": ">=0.6.0",
        "xmlbuilder": "~11.0.0"
      },
      "engines": {
        "node": ">=4.0.0"
      }
    },
    "node_modules/xmlbuilder": {
      "version": "11.0.1",
      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=4.0"
      }
    }
  }
}


path: package.json
content: {
  "name": "news-aggregator",
  "version": "1.0.0",
  "description": "全网新闻聚合 Skill — 定时抓取多信源、关键词过滤、生成每日简报并推送到飞书",
  "main": "scripts/run.js",
  "scripts": {
    "start": "node scripts/run.js",
    "fetch": "node scripts/fetch.js",
    "filter": "node scripts/filter.js",
    "digest": "node scripts/digest.js",
    "push": "node scripts/push.js",
    "schedule": "node scripts/scheduler.js"
  },
  "keywords": [
    "news",
    "aggregator",
    "rss",
    "feishu",
    "daily-digest",
    "openclaw",
    "skill"
  ],
  "author": "肥肥🐶",
  "license": "MIT",
  "dependencies": {
    "axios": "^1.6.0",
    "cheerio": "^1.0.0",
    "node-cron": "^3.0.3",
    "rss-parser": "^3.13.0"
  }
}


path: scripts/digest.js
content: /**
 * digest.js — 每日简报生成脚本
 * 将分类后的新闻渲染为 Markdown 格式的每日简报
 */

const fs = require('fs');
const path = require('path');

// 加载配置
const configPath = path.join(__dirname, '..', 'config.json');
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : {};

const CATEGORY_EMOJI = {
  '科技': '🖥️',
  '财经': '💰',
  '行业': '📊',
  '其他': '📰'
};

/**
 * 格式化日期为中文格式
 */
function formatDate(date) {
  const d = new Date(date);
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${year}年${month}月${day}日`;
}

/**
 * 格式化时间为相对时间
 */
function timeAgo(dateStr) {
  try {
    const diff = Date.now() - new Date(dateStr).getTime();
    const hours = Math.floor(diff / 3600000);
    if (hours < 1) return '刚刚';
    if (hours < 24) return `${hours}小时前`;
    const days = Math.floor(hours / 24);
    return `${days}天前`;
  } catch {
    return '';
  }
}

/**
 * 生成单条新闻的 Markdown 列表项
 */
function formatNewsItem(news, index) {
  const emoji = CATEGORY_EMOJI[news.category] || '📰';
  const ago = timeAgo(news.publishedAt);
  const timeStr = ago ? ` (${ago})` : '';
  const summary = news.summary ? `\n    > ${news.summary.slice(0, 100)}${news.summary.length > 100 ? '...' : ''}` : '';
  return `${index}. [${news.title}](${news.url})${timeStr}${summary}`;
}

/**
 * 生成完整简报
 */
function generateDigest(categorized, date) {
  const dateStr = formatDate(date || new Date());
  const lines = [];

  lines.push(`# 📰 每日新闻简报`);
  lines.push(`**${dateStr}** · ${new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })} UTC`);
  lines.push('');
  lines.push('---');
  lines.push('');

  let totalNews = 0;
  const categories = Object.keys(categorized).filter(cat => categorized[cat] && categorized[cat].length > 0);

  if (categories.length === 0) {
    lines.push('今日暂无符合关键词的新闻 🎉');
    lines.push('');
    return lines.join('\n');
  }

  for (const category of categories) {
    const newsList = categorized[category];
    if (!newsList || newsList.length === 0) continue;

    const emoji = CATEGORY_EMOJI[category] || '📰';
    lines.push(`## ${emoji} ${category}`);
    lines.push('');

    newsList.forEach((news, i) => {
      lines.push(formatNewsItem(news, i + 1));
      lines.push('');
    });

    totalNews += newsList.length;
  }

  lines.push('---');
  lines.push('');
  lines.push(`📌 共收录 **${totalNews}** 条新闻 | 由 **News Aggregator** 自动生成`);
  lines.push('');
  lines.push('> 💡 点击标题链接阅读原文。简报内容由算法自动筛选，内容的真实性与立场与本工具无关。');

  return lines.join('\n');
}

async function main() {
  const inputPath = path.join(__dirname, '..', 'data', 'filtered_news.json');
  if (!fs.existsSync(inputPath)) {
    console.error('[digest] 错误: data/filtered_news.json 不存在，请先运行 filter.js');
    return;
  }

  const categorized = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
  console.log('[digest] 生成简报...');

  const digest = generateDigest(categorized, new Date());

  const outputPath = path.join(__dirname, '..', 'data', 'daily_digest.md');
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
  fs.writeFileSync(outputPath, digest);

  console.log(`[digest] 简报已生成: ${outputPath}`);
  return digest;
}

module.exports = { main, generateDigest };

if (require.main === module) {
  main().catch(console.error);
}


path: scripts/fetch.js
content: /**
 * fetch.js — 新闻抓取脚本
 * 从配置的信源（RSS / 通用 URL）抓取最新新闻
 */

const fs = require('fs');
const path = require('path');
const axios = require('axios');
const cheerio = require('cheerio');
const Parser = require('rss-parser');

// 内置轻量 RSS 解析器（无第三方依赖时使用）
class SimpleRssParser {
  parse(xml) {
    const items = [];
    const itemMatches = xml.matchAll(/<item[^>]*>([\s\S]*?)<\/item>/gi);
    for (const match of itemMatches) {
      const itemXml = match[1];
      const getContent = (tag) => {
        const m = itemXml.match(new RegExp(`<${tag}[^>]*><!\[CDATA\[([\s\S]*?)\]\]><\/${tag}>`));
        if (m) return m[1].trim();
        const m2 = itemXml.match(new RegExp(`<${tag}[^>]*>([\s\S]*?)<\/${tag}>`));
        return m2 ? m2[1].replace(/<[^>]+>/g, '').trim() : '';
      };
      const title = getContent('title');
      const link = getContent('link');
      const pubDate = getContent('pubDate') || getContent('dc:date');
      const description = getContent('description');
      if (title && link) {
        items.push({ title, url: link, publishedAt: pubDate ? new Date(pubDate).toISOString() : new Date().toISOString(), summary: description });
      }
    }
    return items;
  }
}

const parser = new Parser({ customFields: { item: ['media:content', 'content:encoded'] } });
const simpleParser = new SimpleRssParser();

// 加载配置
const configPath = path.join(__dirname, '..', 'config.json');
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : { sources: [], maxNewsPerCategory: 10 };
const sources = config.sources || [];

async function fetchRss(source) {
  try {
    const response = await axios.get(source.url, { timeout: 15000, headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NewsAggregator/1.0)' } });
    const contentType = response.headers['content-type'] || '';
    let items = [];
    if (contentType.includes('xml') || source.url.includes('rss') || source.url.includes('feed')) {
      try { items = (await parser.parseString(response.data)).items; } catch { items = simpleParser.parse(response.data); }
    } else {
      // 通用网页：尝试提取 <a> 链接
      const $ = cheerio.load(response.data);
      $('a[href]').each((_, el) => {
        const href = $(el).attr('href');
        const text = $(el).text().trim();
        if (href && text && text.length > 10 && href.startsWith('http')) {
          items.push({ title: text, url: href, publishedAt: new Date().toISOString(), summary: '' });
        }
      });
    }
    return (items || []).slice(0, 20).map(item => ({
      title: (item.title || '').replace(/<[^>]+>/g, '').trim(),
      url: item.link || item.url || '',
      source: source.name,
      publishedAt: item.pubDate || item.isoDate || new Date().toISOString(),
      summary: (item.contentSnippet || item.content || item.summary || '').replace(/<[^>]+>/g, '').slice(0, 200)
    })).filter(item => item.title && item.url);
  } catch (err) {
    console.error(`[fetch] ${source.name} 抓取失败:`, err.message);
    return [];
  }
}

async function main() {
  console.log(`[fetch] 开始抓取 ${sources.length} 个信源...`);
  const allNews = [];
  for (const source of sources) {
    const news = await fetchRss(source);
    allNews.push(...news);
    console.log(`[fetch] ${source.name}: ${news.length} 条`);
  }
  // 按时间排序，去标题重复
  allNews.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
  const seen = new Set();
  const unique = allNews.filter(n => {
    const key = n.title.slice(0, 50);
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
  const outputPath = path.join(__dirname, '..', 'data', 'raw_news.json');
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
  fs.writeFileSync(outputPath, JSON.stringify(unique, null, 2));
  console.log(`[fetch] 完成，共 ${unique.length} 条（已去重）`);
  return unique;
}

module.exports = { main, fetchRss };

if (require.main === module) {
  main().catch(console.error);
}


path: scripts/filter.js
content: /**
 * filter.js — 新闻过滤与分类脚本
 * 按关键词匹配新闻条目，归类到不同分类
 */

const fs = require('fs');
const path = require('path');

// 加载配置
const configPath = path.join(__dirname, '..', 'config.json');
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : { keywords: {} };

const keywords = config.keywords || {};

/**
 * 计算关键词匹配得分
 */
function matchScore(news, category, categoryKeywords) {
  const text = `${news.title} ${news.summary}`.toLowerCase();
  let score = 0;
  for (const kw of categoryKeywords) {
    const lower = kw.toLowerCase();
    if (text.includes(lower)) {
      // 标题匹配权重更高
      if (news.title.toLowerCase().includes(lower)) score += 2;
      else score += 1;
    }
  }
  return score;
}

/**
 * 将新闻分配到最佳分类
 */
function assignCategory(news) {
  let bestCategory = '其他';
  let bestScore = 0;
  for (const [category, kws] of Object.entries(keywords)) {
    const score = matchScore(news, category, kws);
    if (score > bestScore) {
      bestScore = score;
      bestCategory = category;
    }
  }
  return bestCategory;
}

/**
 * 过滤并分类新闻
 */
function filterAndCategorize(newsList) {
  const categorized = {};
  for (const [cat] of Object.entries(keywords)) {
    categorized[cat] = [];
  }
  categorized['其他'] = [];

  for (const news of newsList) {
    const category = assignCategory(news);
    if (categorized[category]) {
      categorized[category].push({ ...news, category });
    }
  }

  // 限制每个分类的数量
  const maxPerCategory = config.maxNewsPerCategory || 10;
  for (const cat of Object.keys(categorized)) {
    categorized[cat] = categorized[cat].slice(0, maxPerCategory);
  }

  return categorized;
}

async function main() {
  const inputPath = path.join(__dirname, '..', 'data', 'raw_news.json');
  if (!fs.existsSync(inputPath)) {
    console.error('[filter] 错误: data/raw_news.json 不存在，请先运行 fetch.js');
    return;
  }

  const rawNews = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
  console.log(`[filter] 开始过滤 ${rawNews.length} 条新闻...`);

  const categorized = filterAndCategorize(rawNews);

  let total = 0;
  for (const [cat, news] of Object.entries(categorized)) {
    console.log(`[filter] ${cat}: ${news.length} 条`);
    total += news.length;
  }

  const outputPath = path.join(__dirname, '..', 'data', 'filtered_news.json');
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
  fs.writeFileSync(outputPath, JSON.stringify(categorized, null, 2));
  console.log(`[filter] 完成，共归类 ${total} 条`);

  return categorized;
}

module.exports = { main, filterAndCategorize, assignCategory };

if (require.main === module) {
  main().catch(console.error);
}


path: scripts/push.js
content: /**
 * push.js — 飞书推送脚本
 * 将生成的每日简报推送到飞书指定会话
 */

const fs = require('fs');
const path = require('path');

// 加载配置
const configPath = path.join(__dirname, '..', 'config.json');
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : {};

const feishuConfig = config.feishu || {};
const chatId = feishuConfig.chat_id;
const receiveIdType = feishuConfig.receive_id_type || 'chat_id';

// 历史记录路径
const historyPath = path.join(__dirname, '..', 'data', 'history.json');

/**
 * 加载推送历史
 */
function loadHistory() {
  if (fs.existsSync(historyPath)) {
    return JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
  }
  return { dates: [], titles: [] };
}

/**
 * 保存推送历史
 */
function saveHistory(history) {
  fs.mkdirSync(path.dirname(historyPath), { recursive: true });
  fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
}

/**
 * 检查是否已推送过相同日期的简报
 */
function isAlreadyPushed(dateStr) {
  const history = loadHistory();
  return history.dates.includes(dateStr);
}

/**
 * 记录推送历史
 */
function recordPush(dateStr, newsCount) {
  const history = loadHistory();
  history.dates.push(dateStr);
  // 保留最近 30 天记录
  if (history.dates.length > 30) {
    history.dates = history.dates.slice(-30);
  }
  saveHistory(history);
}

/**
 * 生成飞书富文本消息
 */
function buildFeishuPost(digestMd) {
  // 简报是 Markdown 格式，飞书 post 消息支持部分 Markdown
  // 这里将 Markdown 转为飞书富文本格式
  const lines = digestMd.split('\n');
  const content = [];
  let inList = false;

  for (const line of lines) {
    if (line.startsWith('# ')) {
      content.push([{ tag: 'h1', text: { content: line.replace('# ', ''), tag: 'text' } }]);
    } else if (line.startsWith('## ')) {
      content.push([{ tag: 'h2', text: { content: line.replace('## ', ''), tag: 'text' } }]);
    } else if (line.startsWith('---')) {
      content.push([{ tag: 'hr' }]);
    } else if (line.match(/^\d+\./)) {
      const match = line.match(/^(\d+)\.\s*\[([^\]]+)\]\(([^)]+)\)/);
      if (match) {
        content.push([{ tag: 'text', text: { content: `${match[1]}. `, tag: 'text' } },
                      { tag: 'a', text: { content: match[2], href: match[3] } }]);
      } else {
        content.push([{ tag: 'text', text: { content: line, tag: 'text' } }]);
      }
    } else if (line.startsWith('> ')) {
      content.push([{ tag: 'quote', text: { content: line.replace(/^>\s?/, ''), tag: 'text' } }]);
    } else if (line.trim() === '') {
      content.push([{ tag: 'text', text: { content: ' ', tag: 'text' } }]);
    } else {
      // 替换 **bold** 为加粗
      let processed = line.replace(/\*\*([^*]+)\*\*/g, (_, t) => t);
      content.push([{ tag: 'text', text: { content: processed, tag: 'text' } }]);
    }
  }

  return {
    msg_type: 'post',
    content: {
      zh_cn: {
        title: '📰 每日新闻简报',
        content: content
      }
    }
  };
}

async function main() {
  const digestPath = path.join(__dirname, '..', 'data', 'daily_digest.md');
  if (!fs.existsSync(digestPath)) {
    console.error('[push] 错误: data/daily_digest.md 不存在，请先运行 digest.js');
    return;
  }

  const digest = fs.readFileSync(digestPath, 'utf-8');
  const today = new Date().toISOString().slice(0, 10);

  if (isAlreadyPushed(today)) {
    console.log(`[push] 今日（${today}）已推送过，跳过`);
    return;
  }

  if (!chatId) {
    console.error('[push] 错误: config.json 中未配置 feishu.chat_id');
    console.error('[push] 请在 config.json 中添加: { "feishu": { "chat_id": "oc_xxx" } }');
    return;
  }

  console.log(`[push] 推送简报到飞书会话 ${chatId}...`);

  const payload = buildFeishuPost(digest);

  try {
    // 通过 OpenClaw 内置的飞书消息接口推送
    const response = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.FEISHU_BOT_TOKEN || ''}`
      },
      body: JSON.stringify({
        receive_id: chatId,
        msg_type: 'text',
        content: JSON.stringify({ text: digest.slice(0, 4000) }) // 飞书 text 消息最多 4000 字
      })
    });

    const result = await response.json();
    if (result.code !== 0 && result.msg !== 'success') {
      // 如果没有 bot token，输出为兼容性发送（主 agent 接收）
      console.log('[push] 简报内容（请通过飞书机器人手动发送或配置 FEISHU_BOT_TOKEN）:');
      console.log(digest.slice(0, 500));
    } else {
      console.log(`[push] 推送成功: ${result.code}`);
    }
  } catch (err) {
    console.error('[push] 推送失败:', err.message);
    console.log('[push] 简报已保存到 data/daily_digest.md，可手动发送');
  }

  recordPush(today, 0);
}

module.exports = { main };

if (require.main === module) {
  main().catch(console.error);
}


path: scripts/run.js
content: /**
 * run.js — 一键运行脚本
 * 串联：抓取 → 过滤 → 生成 → 推送
 */

const fetch = require('./fetch');
const filter = require('./filter');
const digest = require('./digest');
const push = require('./push');

async function main() {
  console.log('========== 新闻聚合任务开始 ==========');
  console.log(`[run] 开始时间: ${new Date().toISOString()}`);

  try {
    // Step 1: 抓取
    console.log('\n[run] Step 1/4: 抓取新闻...');
    await fetch.main();

    // Step 2: 过滤
    console.log('\n[run] Step 2/4: 过滤与分类...');
    await filter.main();

    // Step 3: 生成简报
    console.log('\n[run] Step 3/4: 生成简报...');
    await digest.main();

    // Step 4: 推送
    console.log('\n[run] Step 4/4: 推送飞书...');
    await push.main();

    console.log('\n========== 任务完成 ==========');
  } catch (err) {
    console.error('[run] 任务失败:', err);
    process.exit(1);
  }
}

if (require.main === module) {
  main().catch(console.error);
}


path: scripts/scheduler.js
content: /**
 * scheduler.js — Cron 定时调度器
 * 按配置的时间间隔自动运行新闻聚合任务
 */

const cron = require('node-cron');
const path = require('path');

// 加载配置
const configPath = path.join(__dirname, '..', 'config.json');
const fs = require('fs');

let config = { schedule: '0 8 * * *' };
if (fs.existsSync(configPath)) {
  config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}

const schedule = config.schedule || '0 8 * * *';
const runScript = path.join(__dirname, 'run.js');

console.log(`[scheduler] 定时任务已启动`);
console.log(`[scheduler] Cron 表达式: ${schedule}`);
console.log(`[scheduler] 下次运行: ${getNextRun(schedule)}`);

if (!cron.validate(schedule)) {
  console.error(`[scheduler] 无效的 Cron 表达式: ${schedule}`);
  console.error('[scheduler] 默认使用: 0 8 * * * (每天 08:00 UTC)');
}

// 调度任务
const job = cron.schedule(schedule, async () => {
  console.log(`[scheduler] ⏰ 触发定时任务: ${new Date().toISOString()}`);
  try {
    const { main } = require(runScript);
    await main();
    console.log(`[scheduler] ✅ 任务完成，下次运行: ${getNextRun(schedule)}`);
  } catch (err) {
    console.error('[scheduler] ❌ 任务失败:', err.message);
  }
}, {
  scheduled: true,
  timezone: 'UTC'
});

/**
 * 计算下次运行时间
 */
function getNextRun(cronExpr) {
  try {
    // 简单估算：解析 cron 并计算
    const parts = cronExpr.split(' ');
    if (parts.length < 5) return '未知';
    
    // 对于日 cron (0 8 * * *)，计算明天 8:00 UTC
    const now = new Date();
    const next = new Date(now);
    next.setUTCHours(parseInt(parts[0]) || 8, parseInt(parts[1]) || 0, 0, 0);
    if (next <= now) {
      next.setUTCDate(next.getUTCDate() + 1);
    }
    return next.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
  } catch {
    return '未知';
  }
}

console.log('[scheduler] 按 Ctrl+C 停止');

// 优雅退出
process.on('SIGINT', () => {
  console.log('\n[scheduler] 停止定时任务...');
  job.stop();
  process.exit(0);
});


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