{ "id": "DNDfetchRss0001", "name": "Daily News Digest - Fetch RSS to Web", "nodes": [ { "parameters": {}, "id": "manual-trigger", "name": "手动执行", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [240, 260] }, { "parameters": { "rule": { "interval": [ { "field": "cronExpression", "expression": "0 30 7 * * *" } ] } }, "id": "schedule-trigger", "name": "每天 07:30", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.2, "position": [240, 420] }, { "parameters": { "operation": "executeQuery", "query": "SELECT id AS source_id, name AS source_name, feed_url, region, category, priority\nFROM news_sources\nWHERE enabled = TRUE\nORDER BY priority DESC, id;", "options": {} }, "id": "load-sources", "name": "加载启用资讯源", "type": "n8n-nodes-base.postgres", "typeVersion": 2.5, "position": [500, 340] }, { "parameters": { "url": "={{ $json.feed_url }}", "options": {} }, "id": "read-rss", "name": "读取 RSS", "type": "n8n-nodes-base.rssFeedRead", "typeVersion": 1.2, "position": [760, 340], "continueOnFail": true }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const feed = $json;\nconst source = $('加载启用资讯源').item.json;\n\nconst skipped = () => ({\n json: {\n skip: true,\n source_id: source.source_id,\n title: '',\n summary: '',\n url: '',\n author: '',\n language_code: 'en',\n region: source.region || 'international',\n category: source.category || 'general',\n topics: [],\n keywords: [],\n importance_score: Number(source.priority || 50),\n published_at: null,\n raw_payload: feed\n }\n});\n\nif (feed.error) return skipped();\n\nconst stripHtml = (value) => String(value || '')\n .replace(//gi, ' ')\n .replace(//gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, String.fromCharCode(34))\n .replace(/'/gi, String.fromCharCode(39))\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst title = stripHtml(feed.title);\nconst summary = stripHtml(feed.contentSnippet || feed.content || feed.description).slice(0, 1800);\nconst url = String(feed.link || feed.guid || '').trim();\nif (!title || !url || !/^https?:\\/\\//i.test(url)) return skipped();\n\nconst publishedRaw = feed.isoDate || feed.pubDate || feed.published || feed.updated || null;\nconst publishedDate = publishedRaw ? new Date(publishedRaw) : null;\nconst publishedAt = publishedDate && !Number.isNaN(publishedDate.getTime())\n ? publishedDate.toISOString()\n : null;\nconst maxAgeMs = 14 * 24 * 60 * 60 * 1000;\nif (publishedDate && Date.now() - publishedDate.getTime() > maxAgeMs) return skipped();\n\nconst text = `${title} ${summary}`.toLowerCase();\nconst keywordGroups = {\n ai_agent: [\n 'ai', 'artificial intelligence', 'machine learning', 'deep learning',\n 'large language model', 'llm', 'agent', 'agentic', 'generative ai',\n '人工智能', '大模型', '智能体', '机器学习', '深度学习', '生成式', '具身智能'\n ],\n agri_hardware: [\n 'agricultural machinery', 'farm machinery', 'sensor', 'drone', 'robot',\n 'tractor', 'harvester', 'iot', 'satellite', 'precision equipment',\n '传感器', '无人机', '机器人', '农机', '拖拉机', '收割机', '物联网', '卫星', '智能装备'\n ],\n agri_solutions: [\n 'smart farm', 'smart farming', 'precision agriculture', 'digital agriculture',\n 'greenhouse', 'irrigation', 'traceability', 'farm management',\n '智慧农业', '智慧农场', '精准农业', '数字农业', '温室', '灌溉', '水肥一体化', '追溯',\n '解决方案', '项目落地', '示范项目'\n ],\n agri_services: [\n 'agricultural service', 'farm service', 'agronomy', 'extension service',\n 'farm saas', 'crop insurance', 'agricultural finance', 'supply chain',\n '农业服务', '农技服务', '社会化服务', '托管服务', '农业 saas', '农业保险',\n '农业金融', '供应链', '运维服务'\n ],\n policy_market: [\n 'agricultural policy', 'farm policy', 'subsidy', 'regulation', 'investment',\n 'funding', 'tender', 'procurement', 'market outlook',\n '农业政策', '补贴', '监管', '投资', '融资', '招标', '采购', '市场行情'\n ]\n};\n\nconst escapeRegex = (value) => value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\nconst containsKeyword = (keyword) => {\n if (/^[a-z0-9+#.-]+$/i.test(keyword) && keyword.length <= 3) {\n return new RegExp(`\\\\b${escapeRegex(keyword)}\\\\b`, 'i').test(text);\n }\n return text.includes(keyword.toLowerCase());\n};\n\nconst matchedByCategory = {};\nfor (const [category, words] of Object.entries(keywordGroups)) {\n matchedByCategory[category] = words.filter(containsKeyword);\n}\n\nconst sourceCategory = source.category || 'general';\nconst scores = Object.fromEntries(\n Object.entries(matchedByCategory).map(([category, words]) => [\n category,\n words.length + (sourceCategory === category ? 2 : 0)\n ])\n);\nlet category = sourceCategory;\nconst best = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];\nif (best && best[1] > 0) category = best[0];\n\nconst matchedKeywords = [...new Set(Object.values(matchedByCategory).flat())];\nconst dedicatedSource = sourceCategory !== 'general';\nif (!dedicatedSource && matchedKeywords.length === 0) return skipped();\n\nconst matchedTopics = Object.entries(matchedByCategory)\n .filter(([, words]) => words.length > 0)\n .map(([topic]) => topic);\nconst topics = [...new Set([category, source.region, ...matchedTopics])];\nconst languageCode = /[\\u3400-\\u9fff]/.test(`${title}${summary}`) ? 'zh' : 'en';\nconst importanceScore = Math.max(0, Math.min(100,\n Number(source.priority || 50) + Math.min(12, matchedKeywords.length * 2)\n));\n\nreturn {\n json: {\n skip: false,\n source_id: source.source_id,\n source_name: source.source_name,\n title,\n summary,\n url,\n author: stripHtml(feed.creator || feed.author || ''),\n language_code: languageCode,\n region: source.region,\n category,\n topics,\n keywords: matchedKeywords,\n importance_score: importanceScore,\n published_at: publishedAt,\n raw_payload: feed\n }\n};" }, "id": "normalize-classify", "name": "清洗与规则分类", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1020, 340] }, { "parameters": { "operation": "executeQuery", "query": "SELECT *\nFROM public.ingest_news_article(\n $1::bigint, $2::text, $3::text, $4::text, $5::text,\n $6::varchar, $7::varchar, $8::varchar, $9::jsonb, $10::jsonb,\n $11::integer, $12::timestamptz, $13::jsonb, $14::boolean\n);", "options": { "queryReplacement": "={{ [\n $json.source_id,\n $json.title,\n $json.summary,\n $json.url,\n $json.author,\n $json.language_code,\n $json.region,\n $json.category,\n JSON.stringify($json.topics || []),\n JSON.stringify($json.keywords || []),\n $json.importance_score,\n $json.published_at,\n JSON.stringify($json.raw_payload || {}),\n $json.skip === true\n] }}" } }, "id": "save-article", "name": "保存文章并发布日报", "type": "n8n-nodes-base.postgres", "typeVersion": 2.5, "position": [1280, 340] } ], "connections": { "手动执行": { "main": [[{"node": "加载启用资讯源", "type": "main", "index": 0}]] }, "每天 07:30": { "main": [[{"node": "加载启用资讯源", "type": "main", "index": 0}]] }, "加载启用资讯源": { "main": [[{"node": "读取 RSS", "type": "main", "index": 0}]] }, "读取 RSS": { "main": [[{"node": "清洗与规则分类", "type": "main", "index": 0}]] }, "清洗与规则分类": { "main": [[{"node": "保存文章并发布日报", "type": "main", "index": 0}]] } }, "active": false, "settings": { "timezone": "Asia/Shanghai", "executionOrder": "v1" }, "versionId": "daily-news-digest-fetch-rss-v1", "meta": { "templateCredsSetupCompleted": false }, "pinData": {}, "tags": [] }