The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "BriefFeed \u2014 \u4e13\u4e1a\u60c5\u62a5\u7b80\u62a5",
"nodes": [
{
"id": "aa000001-0000-0000-0000-000000000001",
"name": "\u624b\u52a8\u89e6\u53d1",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
200,
400
],
"parameters": {}
},
{
"id": "aa000007-0000-0000-0000-000000000007",
"name": "\u8bfb\u53d6\u914d\u7f6e",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
440,
400
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// ============================================================\n// BriefFeed \u914d\u7f6e\u4e2d\u5fc3 \u2014 \u4fee\u6539\u6b64\u8282\u70b9\u6765\u8c03\u6574\u6240\u6709\u6293\u53d6\u4e0e\u8bc4\u5206\u53c2\u6570\n// rss_sources \u5bf9\u5e94 config/rss_sources.json\uff08\u4e0d\u542b template\uff09\n// \u5176\u4f59\u8bbe\u7f6e\u5bf9\u5e94 config/settings.json\uff08\u4e0d\u542b template\uff09\n// ============================================================\nconst CONFIG = {\n\n // ---------- arXiv ----------\n arxiv_rss_categories: ['cs.AI', 'cs.HC', 'cs.LG', 'cs.RO'],\n paper_keywords: [\n '(\"human computer interaction\" OR HCI) AND (\"large language model\" OR LLM OR agent)',\n '(\"computer aided design\" OR CAD) AND (\"artificial intelligence\" OR \"machine learning\")',\n '(\"multimodal\" OR \"multi-modal\") AND (\"3D\" OR \"design\" OR \"engineering\")',\n '\"design automation\" AND (\"AI\" OR \"agent\" OR \"intelligent\")',\n '\"engineering assistant\" OR \"CAD copilot\" OR \"design intelligence\"',\n ],\n\n // ---------- GitHub ----------\n github_keywords: [\n 'CAD AI assistant',\n 'engineering design agent LLM',\n 'CAD copilot',\n 'HCI design tool AI',\n 'design automation agent',\n ],\n github_quality: {\n stars_min: 10, // star \u6570\u6700\u4f4e\u95e8\u69db\uff08\u964d\u4f4e\u4ee5\u907f\u514d\u6f0f\u6389\u65b0\u5174\u9879\u76ee\uff09\n pushed_within_days: 365, // \u6700\u8fd1\u6d3b\u8dc3\u5929\u6570\uff080 = \u4e0d\u8fc7\u6ee4\uff09\n keep_top: 8, // \u6bcf\u4e2a\u5173\u952e\u8bcd\u6700\u591a\u4fdd\u7559\u51e0\u4e2a\uff08\u6309 star \u964d\u5e8f\uff09\n },\n\n // ---------- RSS \u6e90\uff08\u6765\u81ea config/rss_sources.json\uff09----------\n rss_sources: [\n { name: 'OpenAI Blog', url: 'https://openai.com/news/rss.xml', category: 'AI\u516c\u53f8\u535a\u5ba2', enabled: true },\n { name: 'Anthropic News', url: 'https://www.anthropic.com/news/rss.xml', category: 'AI\u516c\u53f8\u535a\u5ba2', enabled: true },\n { name: 'Google DeepMind Blog', url: 'https://deepmind.com/blog/feed/basic/', category: 'AI\u516c\u53f8\u535a\u5ba2', enabled: true },\n { name: 'Hugging Face Blog', url: 'https://huggingface.co/blog/feed.xml', category: 'AI\u5de5\u5177', enabled: true },\n { name: 'LangChain Blog', url: 'https://blog.langchain.dev/rss/', category: 'AI\u5de5\u5177', enabled: true },\n { name: 'LlamaIndex Blog', url: 'https://www.llamaindex.ai/blog/rss.xml', category: 'AI\u5de5\u5177', enabled: true },\n { name: 'Jina AI', url: 'https://jina.ai/news/rss.xml', category: 'AI\u5de5\u5177', enabled: true },\n { name: 'Microsoft Research Blog', url: 'https://www.microsoft.com/en-us/research/feed/', category: '\u7814\u7a76\u673a\u6784', enabled: true },\n { name: 'deeplearning.ai', url: 'https://rsshub.bestblogs.dev/deeplearning/the-batch', category: '\u7814\u7a76\u673a\u6784', enabled: true },\n { name: 'The GitHub Blog', url: 'https://github.blog/feed/', category: '\u5f00\u6e90\u793e\u533a', enabled: true },\n { name: 'Vercel News', url: 'https://vercel.com/atom', category: '\u5de5\u7a0b/AI\u4ea7\u54c1', enabled: true },\n { name: \"Simon Willison's Weblog\", url: 'https://simonwillison.net/atom/everything/', category: 'AI\u5de5\u7a0b/\u72ec\u7acb\u6280\u672f\u535a\u5ba2', enabled: true },\n { name: 'Latent Space', url: 'https://www.latent.space/feed', category: 'AI\u5de5\u7a0b/Agent', enabled: true },\n { name: 'UX Magazine', url: 'https://uxmag.com/feed/', category: 'HCI/UX', enabled: true },\n { name: 'Last Week in AI', url: 'https://lastweekin.ai/feed/', category: 'AI\u5a92\u4f53', enabled: false },\n ],\n\n // ---------- \u8bc4\u5206 & \u7b5b\u9009\uff08\u6765\u81ea config/settings.json\uff09----------\n scoring_weights: { relevance: 0.5, novelty: 0.3, transferability: 0.2 },\n max_items_per_section: 10,\n min_score_threshold: 4, // \u4e09\u7ef4\u6574\u6570\u5206\u4e4b\u548c\u9608\u503c\uff08\u6ee1\u5206 15\uff0c\u5efa\u8bae 4~6\uff09\n\n // ---------- LLM ----------\n llm_model: 'deepseek-ai/DeepSeek-V3',\n llm_batch_size: 20,\n llm_max_concurrency: 3, // \u5efa\u8bae 2~5\uff0c\u89c6 API \u9650\u6d41\u800c\u5b9a\n\n // ---------- \u6293\u53d6\u91cf ----------\n fresh_days: 7, // \u53ea\u4fdd\u7559\u6700\u8fd1 N \u5929\u5185\u5bb9\uff080 = \u4e0d\u8fc7\u6ee4\uff09\n rss_items_per_source: 10,\n arxiv_results_per_kw: 8,\n github_results_per_kw: 20, // \u6293\u53d6\u5019\u9009\u6570\uff08\u8fc7\u6ee4\u524d\uff09\uff0c\u5e94\u5927\u4e8e keep_top\n\n // ---------- \u8f93\u51fa ----------\n output_dir: '/briefeed-output',\n\n // ---------- LLM \u7cfb\u7edf\u63d0\u793a\u8bcd\uff08\u6765\u81ea config/researcher_profile.md\uff09----------\n system_prompt: 'You are filtering daily professional updates for a researcher. All your output must be in Chinese.\\n\\nThe researcher is mainly interested in:\\n1. AI for CAD and engineering design\\n2. Human-AI interaction, especially CHI/UIST-style research\\n3. Intelligent engineering assistants, CAD copilots, and design automation systems\\n4. Agentic or multi-agent workflows when they are relevant to engineering tasks\\n5. Methods, frameworks, open-source tools, and product ideas that can be transferred into real systems\\n\\nThe researcher especially values:\\n- Content that is directly useful for research or project development\\n- Methodological novelty, system design insight, and engineering applicability\\n- Work related to interactive design tools, engineering intelligence, and AI-assisted workflows\\n- Ideas that can inform the development of CAD copilot / engineering assistant / design intelligence systems\\n\\nThe researcher is less interested in:\\n- Generic AI news or hype without technical substance\\n- Marketing-heavy announcements\\n- Product updates without technical depth\\n- Content with weak relevance to design, engineering, HCI, or agent systems\\n\\nReturn ONLY a valid JSON array. For each item provide these fields:\\n- id: original index (integer)\\n- relevance: integer 0-5. How closely the topic matches the researcher interests above.\\n 0=unrelated, 1=tangential, 2=somewhat relevant, 3=relevant, 4=highly relevant, 5=core topic\\n- novelty: integer 0-5. How new, surprising, or non-obvious the contribution is.\\n 0=rehash, 1=incremental, 2=slight advance, 3=notable, 4=significant, 5=breakthrough\\n- transferability: integer 0-5. How directly the ideas/code/methods can be applied to the researcher\\'s own work.\\n 0=not applicable, 1=inspirational only, 2=partial idea, 3=useful method, 4=directly adoptable, 5=ready to use\\n- category: \"\u8bba\u6587\" | \"\u535a\u5ba2\" | \"GitHub\"\\n- direction: \"AI+CAD\" | \"HCI\" | \"\u8bbe\u8ba1\u5de5\u5177\" | \"\u591a\u6a21\u6001\" | \"Agent\u7cfb\u7edf\" | \"\u5176\u4ed6\"\\n- summary_zh: 2-3 sentence Chinese summary of what this item is\\n- value_zh: 1-2 sentence Chinese explanation of why it matters to this researcher\\n- skip: true ONLY if relevance <= 1 (clearly off-topic), otherwise false\\n\\nCRITICAL:\\n1. Output ONLY a valid JSON array. No markdown. No extra text.\\n2. ALL scores must be integers 0-5. Never use decimals.\\n3. Do NOT modify any URLs. Use URLs exactly as provided.',\n};\n\nconsole.log('\u914d\u7f6e\u52a0\u8f7d\u5b8c\u6210\uff1aRSS\u6e90 ' + CONFIG.rss_sources.filter(function(s){return s.enabled;}).length + ' \u4e2a\u5df2\u542f\u7528\uff0carxiv\u5206\u7c7b ' + CONFIG.arxiv_rss_categories.length + ' \u4e2a\uff0cgithub\u5173\u952e\u8bcd ' + CONFIG.github_keywords.length + ' \u4e2a');\nreturn [{ json: { config: CONFIG } }];"
}
},
{
"id": "aa000002-0000-0000-0000-000000000002",
"name": "\u5168\u91cf\u6570\u636e\u6293\u53d6",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
680,
400
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// \u4ece\u4e0a\u6e38\u8282\u70b9\u63a5\u6536\u914d\u7f6e\nconst CONFIG = $input.first().json.config;\n\n// ============================================================\n// \u5de5\u5177\u51fd\u6570\n// ============================================================\nfunction extractTag(xml, tag) {\n const m = xml.match(new RegExp('<' + tag + '[^>]*>([^]*?)<[/]' + tag + '>', 'i'));\n return m ? m[1].trim() : '';\n}\nfunction extractAttrHref(xml) {\n const m = xml.match(/<link[^>]+href=['\"]([^'\"]+)['\"]/i);\n return m ? m[1] : '';\n}\nfunction stripHtml(s) {\n return s\n .replace(/<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>/gi, '$1')\n .replace(/<[^>]+>/g, '')\n .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '\"').replace(/&#\\d+;/g, '')\n .trim();\n}\nfunction parseRSS(xml, sourceName, category, maxItems) {\n const items = [];\n const rx = /<(?:item|entry)[^]*?<[/](?:item|entry)>/gi;\n for (const chunk of (xml.match(rx) || []).slice(0, maxItems || 10)) {\n const title = stripHtml(extractTag(chunk, 'title'));\n const link = (extractTag(chunk, 'link') || extractAttrHref(chunk)).trim();\n const desc = stripHtml(extractTag(chunk, 'summary') || extractTag(chunk, 'description') || extractTag(chunk, 'content') || '');\n const pub = extractTag(chunk, 'pubDate') || extractTag(chunk, 'published') || extractTag(chunk, 'updated') || '';\n if (!title || !link || !link.startsWith('http')) continue;\n items.push({ title, url: link, abstract: desc.slice(0, 400), date: pub ? new Date(pub).toISOString().split('T')[0] : new Date().toISOString().split('T')[0], source: sourceName, source_type: 'blog', category });\n }\n return items;\n}\nfunction parseArxiv(xml, label) {\n const items = [];\n for (const chunk of (xml.match(/<entry[^]*?<[/]entry>/gi) || [])) {\n const title = stripHtml(extractTag(chunk, 'title')).replace(/\\s+/g, ' ').trim();\n const id = extractTag(chunk, 'id').trim();\n const summary = stripHtml(extractTag(chunk, 'summary'));\n const pub = extractTag(chunk, 'published') || '';\n if (!title || !id) continue;\n items.push({ title, url: id.replace('http://arxiv.org/abs/', 'https://arxiv.org/abs/'), abstract: summary.slice(0, 500), date: pub ? pub.split('T')[0] : new Date().toISOString().split('T')[0], source: 'arXiv (' + label + ')', source_type: 'paper', category: label });\n }\n return items;\n}\nfunction isFresh(item, cutoff) {\n if (!cutoff || !item.date) return true;\n try { return new Date(item.date).getTime() >= cutoff; } catch(e) { return true; }\n}\n\n// ============================================================\n// HTTP \u5de5\u5177\u51fd\u6570\n// ============================================================\nconst https = require('https');\nconst http = require('http');\nconst nodeUrl = require('url');\nfunction httpGet(urlStr, headers, timeoutMs) {\n return new Promise(function(resolve, reject) {\n const u = nodeUrl.parse(urlStr);\n const isHttps = u.protocol === 'https:';\n const lib = isHttps ? https : http;\n const opts = { hostname: u.hostname, port: u.port ? parseInt(u.port) : (isHttps ? 443 : 80), path: (u.pathname || '/') + (u.search || ''), method: 'GET', headers: Object.assign({ 'User-Agent': 'BriefFeed/1.0' }, headers || {}), timeout: timeoutMs || 18000 };\n const req = lib.request(opts, function(res) {\n if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {\n return resolve(httpGet(res.headers.location, headers, timeoutMs));\n }\n const chunks = [];\n res.on('data', function(c) { chunks.push(c); });\n res.on('end', function() { resolve(Buffer.concat(chunks).toString('utf-8')); });\n });\n req.on('timeout', function() { req.destroy(); reject(new Error('Request timed out')); });\n req.on('error', reject);\n req.end();\n });\n}\nfunction httpGetJson(urlStr, headers, timeoutMs) {\n return httpGet(urlStr, headers, timeoutMs).then(function(text) { return JSON.parse(text); });\n}\n\n// ============================================================\n// \u5e76\u53d1\u6293\u53d6\n// ============================================================\nconst errors = [];\nconst T = 18000;\nconst cutoff = CONFIG.fresh_days > 0 ? Date.now() - CONFIG.fresh_days * 86400000 : 0;\nconst maxRSS = CONFIG.rss_items_per_source || 10;\nconst maxArxiv = CONFIG.arxiv_results_per_kw || 8;\nconst maxGH = CONFIG.github_results_per_kw || 20;\n\n// --- 1. arXiv RSS ---\nconst arxivRssResults = await Promise.allSettled(\n CONFIG.arxiv_rss_categories.map(function(cat) {\n return httpGet('https://rss.arxiv.org/rss/' + cat, {}, T)\n .then(function(xml) {\n return parseRSS(xml, 'arXiv ' + cat, cat, maxRSS).map(function(i) { return Object.assign({}, i, { source_type: 'paper' }); });\n });\n })\n);\nconst arxivRssItems = [];\narxivRssResults.forEach(function(r, i) {\n if (r.status === 'fulfilled') { arxivRssItems.push(...r.value); }\n else { errors.push('arXiv RSS ' + CONFIG.arxiv_rss_categories[i] + ': ' + r.reason.message); }\n});\n\n// --- 2. arXiv API \u5173\u952e\u8bcd\u641c\u7d22 ---\nconst arxivApiResults = await Promise.allSettled(\n CONFIG.paper_keywords.map(function(kw) {\n const url = 'https://export.arxiv.org/api/query?search_query=' + encodeURIComponent(kw) + '&start=0&max_results=' + maxArxiv + '&sortBy=submittedDate&sortOrder=descending';\n return httpGet(url, {}, T).then(function(xml) { return parseArxiv(xml, kw.slice(0, 40)); });\n })\n);\nconst arxivApiItems = [];\narxivApiResults.forEach(function(r, i) {\n if (r.status === 'fulfilled') { arxivApiItems.push(...r.value); }\n else { errors.push('arXiv API \"' + CONFIG.paper_keywords[i].slice(0, 30) + '\": ' + r.reason.message); }\n});\n\n// --- 3. RSS \u6e90 ---\nconst enabledSources = CONFIG.rss_sources.filter(function(s) { return s.enabled; });\nconst rssResults = await Promise.allSettled(\n enabledSources.map(function(src) {\n return httpGet(src.url, { 'User-Agent': 'BriefFeed/1.0' }, T)\n .then(function(xml) { return parseRSS(xml, src.name, src.category, maxRSS); });\n })\n);\nconst rssItems = [];\nrssResults.forEach(function(r, i) {\n if (r.status === 'fulfilled') { rssItems.push(...r.value); }\n else { errors.push('RSS ' + enabledSources[i].name + ': ' + r.reason.message); }\n});\n\n// --- 4. GitHub ---\nconst ghHeaders = { 'Accept': 'application/vnd.github+json', 'User-Agent': 'BriefFeed/1.0' };\nconst ghToken = $env.GITHUB_TOKEN || '';\nif (ghToken) { ghHeaders['Authorization'] = 'token ' + ghToken; }\nconst ghQuality = CONFIG.github_quality || {};\nconst ghStarsMin = ghQuality.stars_min != null ? ghQuality.stars_min : 10;\nconst ghPushedDays = ghQuality.pushed_within_days != null ? ghQuality.pushed_within_days : 365;\nconst ghKeepTop = ghQuality.keep_top || 8;\nconst ghPushedCutoff = ghPushedDays > 0 ? Date.now() - ghPushedDays * 86400000 : 0;\nconst githubResults = await Promise.allSettled(\n CONFIG.github_keywords.map(function(kw) {\n const url = 'https://api.github.com/search/repositories?q=' + encodeURIComponent(kw) + '&sort=stars&order=desc&per_page=' + maxGH;\n return httpGetJson(url, ghHeaders, T).then(function(d) {\n if (!d || !d.items) return [];\n return d.items\n .filter(function(repo) {\n if (repo.stargazers_count < ghStarsMin) return false;\n if (ghPushedCutoff && repo.pushed_at) {\n if (new Date(repo.pushed_at).getTime() < ghPushedCutoff) return false;\n }\n return true;\n })\n .slice(0, ghKeepTop)\n .map(function(repo) {\n const pushedDate = (repo.pushed_at || '').split('T')[0];\n const createdDate = (repo.created_at || '').split('T')[0];\n return {\n title: repo.full_name,\n url: repo.html_url,\n abstract: (repo.description || '\u65e0\u63cf\u8ff0').slice(0, 300),\n date: pushedDate || new Date().toISOString().split('T')[0],\n source: 'GitHub (' + kw + ')',\n source_type: 'github',\n category: 'GitHub',\n stars: repo.stargazers_count,\n extra: '\u2b50 ' + repo.stargazers_count + ' | \ud83c\udf74 ' + repo.forks_count + ' | ' + (repo.language || '?') + ' | \u6700\u8fd1\u63a8\u9001 ' + pushedDate + ' | \u521b\u5efa ' + createdDate,\n };\n });\n });\n })\n);\nconst githubItems = [];\ngithubResults.forEach(function(r, i) {\n if (r.status === 'fulfilled') { githubItems.push(...r.value); }\n else { errors.push('GitHub \"' + CONFIG.github_keywords[i] + '\": ' + r.reason.message); }\n});\n\n// --- \u5408\u5e76 & \u65b0\u9c9c\u5ea6\u8fc7\u6ee4 ---\nconst allItems = [...arxivRssItems, ...arxivApiItems, ...rssItems, ...githubItems]\n .filter(function(item) { return isFresh(item, cutoff); });\n\nconsole.log('\u5e76\u53d1\u6293\u53d6\u5b8c\u6210\uff1a\u8bba\u6587RSS ' + arxivRssItems.length + ' | \u8bba\u6587API ' + arxivApiItems.length + ' | RSS ' + rssItems.length + ' | GitHub ' + githubItems.length + ' | \u65b0\u9c9c\u8fc7\u6ee4\u540e ' + allItems.length + ' \u6761\uff08' + CONFIG.fresh_days + ' \u5929\u5185\uff09');\n\nreturn [{ json: { items: allItems, config: CONFIG, errors: errors, fetchedAt: new Date().toISOString() } }];"
}
},
{
"id": "aa000003-0000-0000-0000-000000000003",
"name": "\u6807\u51c6\u5316\u53bb\u91cd",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
680,
400
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const data = $input.first().json;\nconst items = data.items;\nconst seen = new Set();\nconst deduped = items.filter(function(item) {\n if (!item.title || !item.url || !item.url.startsWith('http')) return false;\n const key = item.url.replace(/v\\d+$/, '').toLowerCase().replace(/\\/$/, '');\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n});\nconsole.log('\u6293\u53d6\u5b8c\u6210\uff1a\u539f\u59cb ' + items.length + ' \u6761 \u2192 \u53bb\u91cd\u540e ' + deduped.length + ' \u6761');\nif (data.errors && data.errors.length) console.log('\u8b66\u544a\uff1a' + data.errors.join(' | '));\nreturn [{ json: { items: deduped, config: data.config, errors: data.errors, fetchedAt: data.fetchedAt } }];"
}
},
{
"id": "aa000004-0000-0000-0000-000000000004",
"name": "LLM\u6279\u6b21\u8bc4\u5206",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
920,
400
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const data = $input.first().json;\nconst items = data.items;\nconst config = data.config;\n\nconst API_KEY = $env.SILICONFLOW_API_KEY;\nif (!API_KEY) throw new Error('\u8bf7\u914d\u7f6e\u73af\u5883\u53d8\u91cf SILICONFLOW_API_KEY');\n\nconst SYSTEM_PROMPT = config.system_prompt;\nif (!SYSTEM_PROMPT) throw new Error('\u914d\u7f6e\u4e2d\u7f3a\u5c11 system_prompt\uff0c\u8bf7\u68c0\u67e5\u8bfb\u53d6\u914d\u7f6e\u8282\u70b9');\n\nconst batchSize = config.llm_batch_size || 20;\nconst maxConc = config.llm_max_concurrency || 3;\n// weights \u7528\u4e8e\u5c06\u4e09\u7ef4\u6574\u6570\u5206\uff080-5\uff09\u805a\u5408\u4e3a\u5355\u4e00\u6392\u5e8f\u5206\uff080-15\uff09\uff0c\u4e0d\u518d\u7528 0-1 \u5c0f\u6570\nconst weights = config.scoring_weights || { relevance: 0.5, novelty: 0.3, transferability: 0.2 };\nconst minTotal = config.min_score_threshold || 4; // \u4e09\u7ef4\u4e4b\u548c >= 4 \u624d\u4fdd\u7559\uff0815 \u5206\u5236\uff09\nconst model = config.llm_model || 'deepseek-ai/DeepSeek-V3';\nconst https_llm = require('https');\n\nfunction callLLM(batch, batchOffset) {\n const payload = batch.map(function(item, idx) {\n return { id: batchOffset + idx, title: item.title, url: item.url, source: item.source, type: item.source_type, date: item.date, abstract: (item.abstract || '').slice(0, 400) };\n });\n const userMsg = 'Analyze the following ' + batch.length + ' items and return a JSON array:\\n\\n' + JSON.stringify(payload, null, 2) + '\\n\\nReturn ONLY the JSON array, no markdown.';\n const bodyStr = JSON.stringify({ model: model, messages: [{ role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: userMsg }], max_tokens: 4096, temperature: 0.1, stream: false });\n return new Promise(function(resolve, reject) {\n const opts = { hostname: 'api.siliconflow.cn', port: 443, path: '/v1/chat/completions', method: 'POST', headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) }, timeout: 180000 };\n const req = https_llm.request(opts, function(res) {\n const chunks = [];\n res.on('data', function(c) { chunks.push(c); });\n res.on('end', function() {\n try {\n const result = JSON.parse(Buffer.concat(chunks).toString('utf-8'));\n const content = (result.choices && result.choices[0] && result.choices[0].message) ? result.choices[0].message.content : '';\n let jsonStr = content;\n const mdMatch = content.match(/```(?:json)?\\n?([^]*?)\\n?```/);\n if (mdMatch) jsonStr = mdMatch[1];\n let evals = [];\n try { evals = JSON.parse(jsonStr.trim()); } catch(e2) {\n const arrMatch = jsonStr.match(/\\[[^]*\\]/);\n if (arrMatch) evals = JSON.parse(arrMatch[0]);\n }\n resolve(evals);\n } catch(e) { reject(e); }\n });\n });\n req.on('timeout', function() { req.destroy(); reject(new Error('LLM request timed out')); });\n req.on('error', reject);\n req.write(bodyStr);\n req.end();\n });\n}\n\nconst batches = [];\nfor (let i = 0; i < items.length; i += batchSize) {\n batches.push({ batch: items.slice(i, i + batchSize), offset: i });\n}\n\nconst scored = [];\nfor (let gi = 0; gi < batches.length; gi += maxConc) {\n const group = batches.slice(gi, gi + maxConc);\n console.log('\u5e76\u53d1\u63d0\u4ea4\u6279\u6b21 ' + (gi+1) + '~' + Math.min(gi+maxConc, batches.length) + '/' + batches.length);\n const results = await Promise.allSettled(\n group.map(function(b) { return callLLM(b.batch, b.offset); })\n );\n results.forEach(function(r, ri) {\n const b = group[ri];\n let evals = [];\n if (r.status === 'fulfilled') {\n evals = r.value;\n } else {\n console.log('\u6279\u6b21 ' + (gi+ri+1) + ' LLM \u5931\u8d25: ' + r.reason.message);\n evals = b.batch.map(function(_, idx) { return { id: b.offset+idx, relevance: 2, novelty: 2, transferability: 2, category: '\u535a\u5ba2', direction: '\u672a\u5206\u7c7b', summary_zh: '', value_zh: '\uff08LLM\u5931\u8d25\uff09', skip: false }; });\n }\n for (let ei = 0; ei < evals.length; ei++) {\n const ev = evals[ei];\n if (ev.skip) continue;\n const localIdx = ev.id - b.offset;\n if (localIdx < 0 || localIdx >= b.batch.length) continue;\n const orig = b.batch[localIdx];\n // \u6574\u6570\u5316\uff08\u9632\u6b62 LLM \u8fd4\u56de\u5c0f\u6570\uff09\n const rel = Math.round(Math.min(5, Math.max(0, ev.relevance || 0)));\n const nov = Math.round(Math.min(5, Math.max(0, ev.novelty || 0)));\n const tra = Math.round(Math.min(5, Math.max(0, ev.transferability || ev.actionability || 0)));\n const total = rel + nov + tra; // 0~15\n if (total < minTotal) continue;\n // \u52a0\u6743\u5206\uff08\u7528\u4e8e\u6392\u5e8f\uff0c\u4fdd\u6301\u5411\u540e\u517c\u5bb9 score \u5b57\u6bb5\uff09\n const wScore = rel * (weights.relevance || 0.5) + nov * (weights.novelty || 0.3) + tra * (weights.transferability || weights.actionability || 0.2);\n const merged = {};\n for (const k in orig) merged[k] = orig[k];\n merged.score = Math.round(wScore * 100) / 100; // \u52a0\u6743\u5206\uff0c\u7528\u4e8e\u6392\u5e8f\n merged.total_score = total; // \u4e09\u7ef4\u603b\u5206 /15\n merged.relevance = rel;\n merged.novelty = nov;\n merged.transferability = tra;\n merged.category_zh = ev.category || (orig.source_type === 'paper' ? '\u8bba\u6587' : orig.source_type === 'github' ? 'GitHub' : '\u535a\u5ba2');\n merged.direction = ev.direction || '\u5176\u4ed6';\n merged.summary_zh = ev.summary_zh || orig.abstract || '';\n merged.value_zh = ev.value_zh || '';\n scored.push(merged);\n }\n });\n}\n\nconsole.log('LLM\u5b8c\u6210\uff1a' + scored.length + ' \u6761\u901a\u8fc7\u9608\u503c\uff0c\u5171 ' + batches.length + ' \u6279\u6b21\uff08\u6700\u5927\u5e76\u53d1 ' + maxConc + '\uff09');\nreturn [{ json: { items: scored, config: config, errors: data.errors, fetchedAt: data.fetchedAt } }];"
}
},
{
"id": "aa000005-0000-0000-0000-000000000005",
"name": "\u751f\u6210Markdown\u62a5\u544a",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1160,
400
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const data = $input.first().json;\nconst items = data.items;\nconst config = data.config;\nconst maxN = config.max_items_per_section || 10;\n\nconst papers = items.filter(function(i){ return i.source_type === 'paper'; }).sort(function(a,b){ return b.score-a.score; }).slice(0, maxN);\nconst blogs = items.filter(function(i){ return i.source_type === 'blog'; }).sort(function(a,b){ return b.score-a.score; }).slice(0, maxN);\nconst githubs = items.filter(function(i){ return i.source_type === 'github'; }).sort(function(a,b){ return b.score-a.score; }).slice(0, maxN);\n\nconst now = new Date();\nconst pad = function(n){ return String(n).padStart(2,'0'); };\nconst dateStr = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate());\nconst timeStr = pad(now.getHours()) + ':' + pad(now.getMinutes());\nconst filename = 'briefeed_' + dateStr.replace(/-/g,'') + '_' + pad(now.getHours()) + pad(now.getMinutes()) + '.md';\n\nfunction fmt(item) {\n const scoreStr = item.total_score != null ? ' | \u8bc4\u5206 ' + item.total_score + '/15\uff08\u76f8\u5173' + (item.relevance||0) + ' \u65b0\u9896' + (item.novelty||0) + ' \u8fc1\u79fb' + (item.transferability||0) + '\uff09' : '';\n const extra = item.extra ? '\\n- **\u9644\u52a0\u4fe1\u606f**\uff1a' + item.extra : '';\n return '### ' + item.title + '\\n- **\u6765\u6e90**\uff1a' + item.source + '\\n- **\u65e5\u671f**\uff1a' + item.date + ' | **\u65b9\u5411**\uff1a' + (item.direction||'-') + scoreStr + '\\n- **\u6982\u8ff0**\uff1a' + (item.summary_zh||item.abstract||'\uff08\u65e0\u6458\u8981\uff09') + '\\n- **\u4e3a\u4ec0\u4e48\u503c\u5f97\u5173\u6ce8**\uff1a' + (item.value_zh||'-') + extra + '\\n- **\u539f\u6587**\uff1a' + item.url + '\\n\\n';\n}\n\nconst errSec = data.errors && data.errors.length ? '\\n---\\n\\n> \u26a0\ufe0f **\u6293\u53d6\u8b66\u544a**\uff08' + data.errors.length + ' \u4e2a\u6e90\u5f02\u5e38\uff09\\n' + data.errors.map(function(e){ return '> - ' + e; }).join('\\n') + '\\n' : '';\n\nconst report = '# BriefFeed \u60c5\u62a5\u7b80\u62a5 \u2014 ' + dateStr + ' ' + timeStr + '\\n\\n> \u751f\u6210\u65f6\u95f4\uff1a' + data.fetchedAt + ' | \u901a\u8fc7\u7b5b\u9009\uff1a' + items.length + ' \u6761\uff08\u8bba\u6587 ' + papers.length + '\u3001\u535a\u5ba2 ' + blogs.length + '\u3001GitHub ' + githubs.length + '\uff09\\n\\n---\\n\\n## \u4e00\u3001\u4eca\u65e5\u91cd\u70b9\u8bba\u6587\\n\\n' + (papers.length ? papers.map(fmt).join('') : '_\u4eca\u65e5\u6682\u65e0\u9ad8\u76f8\u5173\u5ea6\u8bba\u6587\u3002_\\n') + '\\n---\\n\\n## \u4e8c\u3001\u4eca\u65e5\u6280\u672f / \u535a\u5ba2\u52a8\u6001\\n\\n' + (blogs.length ? blogs.map(fmt).join('') : '_\u4eca\u65e5\u6682\u65e0\u9ad8\u76f8\u5173\u5ea6\u535a\u5ba2\u52a8\u6001\u3002_\\n') + '\\n---\\n\\n## \u4e09\u3001\u4eca\u65e5 GitHub / \u5f00\u6e90\u9879\u76ee\\n\\n' + (githubs.length ? githubs.map(fmt).join('') : '_\u4eca\u65e5\u6682\u65e0\u9ad8\u76f8\u5173\u5ea6 GitHub \u9879\u76ee\u3002_\\n') + errSec;\n\nconst base64 = Buffer.from(report, 'utf-8').toString('base64');\nreturn [{ json: { filename: filename, outputDir: config.output_dir || '/briefeed-output', reportLength: report.length }, binary: { data: { data: base64, mimeType: 'text/markdown; charset=utf-8', fileName: filename, fileExtension: 'md' } } }];"
}
},
{
"id": "aa000006-0000-0000-0000-000000000006",
"name": "\u4fdd\u5b58\u62a5\u544a",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
1400,
400
],
"parameters": {
"operation": "write",
"fileName": "={{ $json.outputDir + '/' + $json.filename }}",
"dataPropertyName": "data",
"options": {}
}
}
],
"connections": {
"\u624b\u52a8\u89e6\u53d1": {
"main": [
[
{
"node": "\u8bfb\u53d6\u914d\u7f6e",
"type": "main",
"index": 0
}
]
]
},
"\u8bfb\u53d6\u914d\u7f6e": {
"main": [
[
{
"node": "\u5168\u91cf\u6570\u636e\u6293\u53d6",
"type": "main",
"index": 0
}
]
]
},
"\u5168\u91cf\u6570\u636e\u6293\u53d6": {
"main": [
[
{
"node": "\u6807\u51c6\u5316\u53bb\u91cd",
"type": "main",
"index": 0
}
]
]
},
"\u6807\u51c6\u5316\u53bb\u91cd": {
"main": [
[
{
"node": "LLM\u6279\u6b21\u8bc4\u5206",
"type": "main",
"index": 0
}
]
]
},
"LLM\u6279\u6b21\u8bc4\u5206": {
"main": [
[
{
"node": "\u751f\u6210Markdown\u62a5\u544a",
"type": "main",
"index": 0
}
]
]
},
"\u751f\u6210Markdown\u62a5\u544a": {
"main": [
[
{
"node": "\u4fdd\u5b58\u62a5\u544a",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true
},
"tags": [],
"versionId": "briefeed-v1-1",
"meta": {
"templateCredsSetupCompleted": true
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
BriefFeed — 专业情报简报. Uses readWriteFile. Event-driven trigger; 7 nodes.
Source: https://github.com/DonkeyKing01/BriefFeed/blob/df96838ffddb05ef2723794e1de7b9008dbf1474/workflows/briefeed_main.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Perfect for disaster recovery or migrating between environments, this workflow automatically identifies your most recent FTP backup and provides a manual restore capability that intelligently excludes
This n8n workflow template uses community nodes and is only compatible with the self-hosted version of n8n.
N8N Workflow. Uses localFileTrigger, executeCommand, readWriteFile. Event-driven trigger; 16 nodes.
🎯 Purpose: Generate audio files from text scripts stored in Google Drive.
Procedimiento ETL. Uses readWriteFile, microsoftOneDrive, executeWorkflowTrigger. Event-driven trigger; 10 nodes.