├── .github └── workflows │ ├── action.yml.backup │ └── cron.yml ├── .gitignore ├── README.md ├── index.js ├── package.json ├── sina.js └── template ├── index.html └── page.js /.github/workflows/action.yml.backup: -------------------------------------------------------------------------------- 1 | name: 'Sina News Feed Bot' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | - cron: ' 5 * * * * ' 9 | 10 | jobs: 11 | feed-processor: 12 | name: Sina news feed processor 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Setup Node.js 18 | uses: actions/setup-node@main 19 | with: 20 | node-version: '14' 21 | - name: Install dependencies 22 | run: npm install 23 | - name: Build RSS 24 | run: node index.js 25 | - name: Deploy to GitHub Pages 26 | uses: JamesIves/github-pages-deploy-action@3.7.1 27 | with: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | BRANCH: gh-pages # The branch the action should deploy to. 30 | FOLDER: dist # The folder the action should deploy. 31 | CLEAN: true # Automatically remove deleted files from the deploy branch 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Vercel-cron 2 | on: 3 | push: 4 | branches: 5 | - main 6 | schedule: 7 | - cron: '*/30 * * * *' 8 | jobs: 9 | cron: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Call Vercel deploy webhook 13 | run: | 14 | curl -X POST ${{ secrets.VERCEL_HOOKS }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 新浪全球实时新闻 https://sina-news.vercel.app/ 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const AbortController = require('abort-controller'); 3 | const { url } = require('./sina.js'); 4 | const Feed = require('feed').Feed; 5 | const fs = require('fs/promises'); 6 | const process = require('process'); 7 | 8 | const controller = new AbortController(); 9 | // 30 秒后取消请求 10 | const timeout = setTimeout( 11 | () => { controller.abort(); }, 12 | 30000, 13 | ); 14 | 15 | const feed = new Feed({ 16 | title: '新浪新闻', 17 | description: '新浪全球实时财经新闻直播', 18 | // link: 'https://ruanyf.github.io/sina-news/', 19 | link: 'https://sina-news.vercel.app/', 20 | language: 'zh-CN', 21 | generator: 'sina news feed generator', 22 | feedLinks: { 23 | json: 'https://sina-news.vercel.app/rss.json', 24 | rss: 'https://sina-news.vercel.app/rss.xml' 25 | }, 26 | }); 27 | 28 | 29 | const filterArr = [ 30 | '比特币', 31 | '以太坊', 32 | '莱特币', 33 | '瑞波币', 34 | '疫苗', 35 | '疫情', 36 | '新冠', 37 | '央行', 38 | '联储', 39 | '中央银行', 40 | '财长', 41 | '财政部', 42 | '参议院', 43 | '众议院', 44 | ]; 45 | 46 | async function main() { 47 | 48 | const response = await fetch(url, { 49 | headers: {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10130'}, 50 | signal: controller.signal 51 | }); 52 | 53 | if (response.status < 200 || response.status >= 300) { 54 | throw new Error('wrong status code'); 55 | } 56 | 57 | const json = await response.json(); 58 | console.log(`successfully fetch the feed.`); 59 | 60 | const result = json.result || {}; 61 | if (!result.status || result.status.code !== 0) return; 62 | const items = result.data.feed.list; 63 | console.log(`successfully parse the feed.`); 64 | 65 | items.forEach(item => { 66 | if (!item.rich_text) return; 67 | 68 | // text filter 69 | for (let i = 0; i < filterArr.length; i++) { 70 | if (item.rich_text.includes(filterArr[i])) { 71 | // console.log(item.rich_text); 72 | return; 73 | } 74 | } 75 | 76 | // tag filter 77 | if (item.tag && Array.isArray(item.tag) && item.tag.length) { 78 | const tags = item.tag; 79 | const tagFilterArr = ['5', '6', '7', '9']; 80 | for (let i = 0; i < tags.length; i++) { 81 | const tag = tags[i].id; 82 | if (tagFilterArr.includes(tag)) { 83 | // console.log(item.rich_text); 84 | return; 85 | } 86 | } 87 | } 88 | 89 | feed.addItem({ 90 | title: item.rich_text, 91 | id: item.id, 92 | link: item.docurl, 93 | content: '', 94 | date: new Date(item.create_time + '+08:00'), 95 | }); 96 | }); 97 | console.log(`successfully generating new feed.`); 98 | 99 | try { 100 | await fs.access('./dist', fs.constants.R_OK | fs.constants.W_OK); 101 | await fs.rm('./dist', { recursive: true }); 102 | console.log(`successfully deleted ./dist`); 103 | } catch { 104 | // ... 105 | } 106 | 107 | await fs.mkdir('./dist'); 108 | console.log(`successfully create ./dist`); 109 | 110 | await fs.writeFile('./dist/rss.json', feed.json1()); 111 | console.log(`successfully write rss.json`); 112 | 113 | await fs.writeFile('./dist/rss.xml', feed.rss2()); 114 | console.log(`successfully write rss.xml`); 115 | 116 | await fs.copyFile('./template/index.html', `./dist/index.html`); 117 | await fs.copyFile('./template/page.js', `./dist/page.js`); 118 | console.log(`successfully copy asset files`); 119 | 120 | } 121 | 122 | main() 123 | .catch(err => { 124 | console.log(err); 125 | process.exit(1); 126 | }) 127 | .finally(() => { 128 | clearTimeout(timeout); 129 | }); 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sina-news", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "sina.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "abort-controller": "^3.0.0", 14 | "feed": "^4.2.1", 15 | "node-fetch": "^2.6.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sina.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Sina 7x24 News 3 | * 4 | * http://finance.sina.com.cn/7x24/ 5 | * 6 | * API Endpoint:https://zhibo.sina.com.cn/api/zhibo/feed?callback=sina&page=1&page_size=30&zhibo_id=152&tag_id=0&dire=f&dpc=1&pagesize=30&type=0 7 | * 8 | * Parameters: 9 | * - callback: optional string。JSONP 回调函数的名称。如果省略,直接返回 JSON 数据。 10 | * - dire: string。含义未知,设为 f。 11 | * - page:number。当前所在的页面,默认为第1页。 12 | * - page_size: optional number, default 30。返回的条目数量。 13 | * - dpc: number。含义未知,设为1。 14 | * - type: number。含义未知,设为0。 15 | * - zhibo_id:number。固定为 152。 16 | * - tag_id:number。分类,0 表示全部,1 宏观,2 行业,3 公司,4 数据,5 市场, 6 观点, 7 央行, 8 其他,9 焦点,10 A 股,102 国际。 17 | * - id: number,optional。当前的新闻条目 id,无效果。 18 | * - _:number,optional。当前时间戳,`Date.now()`,无效果。 19 | * 20 | * Return Value 21 | * 22 | * - if with parameter `callback=fn`, return 23 | * ``` 24 | * try { 25 | * jQuery1112024942892104039993_1607943165149({ 26 | * result: { 27 | * ... 28 | * } 29 | * }); 30 | * } catch (e) {} 31 | * ; 32 | * ``` 33 | * 34 | * - if without parameter callback, return 35 | * ``` 36 | * { 37 | * result: { 38 | * data: { 39 | * feed: { 40 | * html: "", 41 | * list: [ 42 | * { 43 | * create_time: "2020-12-14 19:08:25", 44 | * creator: "wangting6@staff.sina.com.cn", 45 | * docurl: "https://finance.sina.cn/7x24/2020-12-14/detail-iiznezxs6893884.d.html", 46 | * id: 1930071, 47 | * mender: "wangting6@staff.sina.com.cn", 48 | * multimedia: "", 49 | * rich_text: " 德国卫生部发言人:重申我们预计欧洲药品管理局(EMA)将在12月底批准BIONTECH的新冠疫苗。", 50 | * tag: [{ id: "102", name: "国际" }], 51 | * type: 0, 52 | * top_value: 0, 53 | * update_time: "2020-12-14 19:08:32", 54 | * zhibo_id: 152, 55 | * }, 56 | * ], 57 | * maxid: 1930071, 58 | * min_id: 1930071, 59 | * page_info: {firstPage: 1, lastPage: 12, nextPage: 2, pName: "page", page: 1, pageSize: 1, prePage: 1, totalNum: 12, totalPage: 12}, 60 | * survey_id: [] 61 | * }, 62 | * focus: [], 63 | * top: {}, 64 | * zhibo: [] 65 | * }, 66 | * status: {code: 0, msg: "OK"}, 67 | * timestamp: "Mon Dec 14 19:10:02 +0800 2020" 68 | * } 69 | * } 70 | * ``` 71 | * 72 | */ 73 | 74 | const endpoint = 'https://zhibo.sina.com.cn/api/zhibo/feed'; 75 | const params = new URLSearchParams({ 76 | page: 1, 77 | page_size: 100, 78 | zhibo_id: 152, 79 | tag_id: 0, 80 | dire: 'f', 81 | dpc: 1, 82 | type: 0, 83 | }); 84 | 85 | module.exports = { 86 | endpoint, 87 | params, 88 | url: endpoint + '?' + params.toString(), 89 | }; 90 | 91 | -------------------------------------------------------------------------------- /template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sina News 7 | 8 | 9 | 17 | 18 | 19 |
20 |
21 |

22 | 全球实时财经新闻 23 |

24 |
    25 |
    26 |
    27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /template/page.js: -------------------------------------------------------------------------------- 1 | fetch('./rss.json') 2 | .then(async function (response) { 3 | const res = await response.json(); 4 | const items = res.items; 5 | 6 | const list = document.querySelector('.list'); 7 | const fragment = document.createDocumentFragment(); 8 | 9 | items.forEach(i => { 10 | const li = document.createElement('li'); 11 | const p = document.createElement('p'); 12 | const timeObj = new Date(i.date_modified); 13 | p.innerHTML = `${i.title} (${timeObj.toLocaleTimeString('zh-CN', {hour12: false, hour: '2-digit', minute: '2-digit'})})`; 14 | li.appendChild(p); 15 | fragment.appendChild(li); 16 | }); 17 | 18 | list.appendChild(fragment); 19 | }) 20 | 21 | --------------------------------------------------------------------------------