├── rssant
├── __init__.py
├── helper
│ ├── __init__.py
│ └── content_hash.py
├── middleware
│ ├── __init__.py
│ ├── timer.py
│ └── message_storage.py
├── allauth_providers
│ ├── __init__.py
│ ├── github
│ │ ├── __init__.py
│ │ ├── urls.py
│ │ └── provider.py
│ ├── oauth2
│ │ └── __init__.py
│ └── helper.py
├── settings
│ └── __init__.py
├── tests
│ └── email_template_tests.py
├── templates
│ └── email
│ │ └── recall.html
├── wsgi.py
└── auth_serializer.py
├── tests
├── __init__.py
├── common
│ ├── __init__.py
│ └── test_rss_proxy.py
├── feedlib
│ ├── __init__.py
│ ├── processor
│ │ └── __init__.py
│ ├── testdata
│ │ ├── parser
│ │ │ ├── failed
│ │ │ │ ├── jsonfeed-failed-no-title-items.json
│ │ │ │ ├── http-blog-hexun-com-group-getrssresouce-aspx-classid-74.xml
│ │ │ │ ├── https-egoist-moe-atom-xml.xml
│ │ │ │ ├── rssant-manifest.json
│ │ │ │ ├── http-www-yuming-in-feed.xml
│ │ │ │ ├── http-www-ziyouren888-com-feed.xml
│ │ │ │ ├── http-porn191-com-index-php-feed.xml
│ │ │ │ ├── jsonfeed-failed-syntax.json
│ │ │ │ ├── v2ex-jsonfeed-no-storys.json
│ │ │ │ ├── http-www-m1927-com-feed-php.xml
│ │ │ │ └── http-www-chongdiantou-com-feed.xml
│ │ │ ├── warn
│ │ │ │ ├── https-tmioe-com-feed.xml
│ │ │ │ └── v2ex-jsonfeed-warning.json
│ │ │ └── well
│ │ │ │ ├── jsonfeed-example.json
│ │ │ │ ├── v2ex-jsonfeed.json
│ │ │ │ └── v2ex-jsonfeed.xml
│ │ ├── encoding
│ │ │ ├── chardet
│ │ │ │ ├── big5.xml
│ │ │ │ ├── euc-jp.xml
│ │ │ │ ├── koi8-r.xml
│ │ │ │ ├── shift-jis.xml
│ │ │ │ ├── tis-620.xml
│ │ │ │ ├── euc-kr:cp949.xml
│ │ │ │ ├── gb2312:gb18030.xml
│ │ │ │ ├── gbk:gb18030.json
│ │ │ │ ├── windows-1255.xml
│ │ │ │ ├── utf-8.xml
│ │ │ │ └── utf-8.json
│ │ │ ├── xml
│ │ │ │ ├── http_text_xml_charset_overrides_encoding_2.xml
│ │ │ │ ├── http_text_xml_default.xml
│ │ │ │ ├── http_application_xml_default.xml
│ │ │ │ ├── http_text_rss_xml_default.xml
│ │ │ │ ├── http_application_rss_xml_default.xml
│ │ │ │ ├── http_text_rss_xml_encoding.xml
│ │ │ │ ├── http_application_xml_encoding.xml
│ │ │ │ ├── http_text_atom_xml_default.xml
│ │ │ │ ├── http_application_rss_xml_encoding.xml
│ │ │ │ ├── http_application_xml_dtd_default.xml
│ │ │ │ ├── http_application_atom_xml_default.xml
│ │ │ │ ├── http_text_xml_charset_overrides_encoding.xml
│ │ │ │ ├── http_text_atom_xml_encoding.xml
│ │ │ │ ├── http_text_xml_epe_default.xml
│ │ │ │ ├── http_application_xml_charset_overrides_encoding.xml
│ │ │ │ ├── http_text_rss_xml_charset_overrides_encoding.xml
│ │ │ │ ├── http_application_xml_dtd_encoding.xml
│ │ │ │ ├── http_application_atom_xml_encoding.xml
│ │ │ │ ├── http_application_xml_epe_default.xml
│ │ │ │ ├── http_text_xml_epe_encoding.xml
│ │ │ │ ├── http_application_rss_xml_charset_overrides_encoding.xml
│ │ │ │ ├── http_text_atom_xml_charset_overrides_encoding.xml
│ │ │ │ ├── http_application_xml_epe_encoding.xml
│ │ │ │ ├── http_application_xml_dtd_charset_overrides_encoding.xml
│ │ │ │ ├── http_application_atom_xml_charset_overrides_encoding.xml
│ │ │ │ ├── http_application_atom_xml_gb2312_encoding.xml
│ │ │ │ ├── http_text_xml_epe_charset_overrides_encoding.xml
│ │ │ │ ├── http_application_xml_epe_charset_overrides_encoding.xml
│ │ │ │ └── http_application_atom_xml_gb2312_charset_overrides_encoding.xml
│ │ │ └── mixed
│ │ │ │ ├── http_application_rss_xml_charset_overrides_encoding.xml
│ │ │ │ └── http_application_atom_xml_charset_overrides_encoding.xml
│ │ ├── fulltext
│ │ │ ├── hackernews1_rss.html
│ │ │ ├── juejin1_rss.html
│ │ │ ├── juejin2_rss.html
│ │ │ ├── hackernews2_rss.html
│ │ │ ├── xkcd_rss.html
│ │ │ └── martinfowler_rss.html
│ │ ├── feed_type
│ │ │ └── html
│ │ │ │ ├── http-blog-hexun-com-group-getrssresouce-aspx-classid-74.xml
│ │ │ │ └── http-www-yuming-in-feed.xml
│ │ └── processor
│ │ │ ├── html_redirect
│ │ │ ├── test_html_redirect_1.html
│ │ │ ├── test_html_redirect_3.html
│ │ │ └── test_html_redirect_2.html
│ │ │ ├── test_iframe.html
│ │ │ └── test_iframe_link.html
│ ├── test_cacert.py
│ ├── test_response.py
│ └── test_response_file.py
├── feedserver
│ ├── __init__.py
│ └── main.py
├── models
│ ├── __init__.py
│ ├── test_story_key.py
│ ├── test_story_unique_ids.py
│ └── test_story_data.py
├── test_arxiv_results.json
├── fixtures
│ └── screenshots
│ │ ├── test_error.png
│ │ ├── test_error_20251120082830.png
│ │ ├── test_error_20251120082907.png
│ │ └── test_error_20251120082943.png
├── test_config.py
├── conftest.py
├── test_changelog.py
└── sample
│ ├── inoreader.xml
│ └── stringer.opml
├── rssant_api
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── duplicate_feed_detector_tests.py
│ ├── story_storage_tests.py
│ └── reverse_url_tests.py
├── views
│ ├── __init__.py
│ ├── common.py
│ ├── errors.py
│ └── ezrevenue.py
├── migrations
│ ├── __init__.py
│ ├── 0025_auto_20200516_0959.py
│ ├── 0002_auto_20190317_1020.py
│ ├── 0015_story_has_mathjax.py
│ ├── 0029_userfeed_group.py
│ ├── 0023_feed_response_status.py
│ ├── 0019_feed_use_proxy.py
│ ├── 0030_storyinfo_sentence_count.py
│ ├── 0031_story_sentence_count.py
│ ├── 0020_feed_checksum_data.py
│ ├── 0022_feed_warnings.py
│ ├── 0007_userfeed_is_from_bookmark.py
│ ├── 0004_rawfeed_is_gzipped.py
│ ├── 0028_auto_20200619_0906.py
│ ├── 0018_feed_freeze_level.py
│ ├── 0056_add_arxiv_report_json_data.py
│ ├── 0024_auto_20200510_1008.py
│ ├── 0032_auto_20201227_0636.py
│ ├── 0016_auto_20191105_1247.py
│ ├── 0044_add_citation_fields.py
│ ├── 0047_add_ai_entertainment_refs.py
│ ├── 0021_auto_20200418_0512.py
│ ├── 0051_auto_20251118_1322.py
│ ├── 0014_auto_20191027_0558.py
│ ├── 0017_auto_20191217_1253.py
│ ├── 0006_auto_20190418_0945.py
│ ├── 0012_auto_20191025_1526.py
│ ├── 0048_add_separate_ai_entertainment_reports.py
│ ├── 0009_auto_20190518_0659.py
│ ├── 0026_feedstorystat.py
│ ├── 0011_auto_20190714_0550.py
│ └── 0003_auto_20190327_1304.py
├── models
│ ├── story_storage
│ │ ├── common
│ │ │ └── __init__.py
│ │ ├── postgres
│ │ │ ├── __init__.py
│ │ │ └── postgres_sharding.py
│ │ └── __init__.py
│ ├── errors.py
│ └── registery.py
├── apps.py
├── resources
│ └── opml.mako
├── admin.py
└── urls.py
├── rssant_cli
├── __init__.py
├── user.py
└── scripts
│ └── fix_user_story_feed_id.sql
├── scripts
├── __init__.py
├── dev
│ ├── quick_check.sh
│ ├── rundev-api.sh
│ ├── rundev-scheduler.sh
│ ├── rundev-worker.sh
│ ├── start-all-services.sh
│ └── check-services.sh
├── tools
│ ├── diagnose_subscription_issue.py
│ └── pip-compile.sh
├── run-asyncapi.py
├── run-scheduler.py
├── django_db_init.py
├── setup.sh
├── stop_rsshub.sh
├── postgres_start.sh
├── django_pre_migrate.py
├── migrate_story_v0_1_0.py
├── check_rsshub.sh
├── pg_count.py
├── docker
│ └── start-docker.sh
└── README_FIX_LOGIN.md
├── constraint.txt
├── rssant_asyncapi
├── __init__.py
├── app.py
├── views.py
└── main.py
├── rssant_common
├── __init__.py
├── chnlist
│ ├── __init__.py
│ ├── chnlist.txt.gz
│ └── chnlist.py
├── timezone.py
├── hashid.py
├── base64.py
├── blacklist.py
├── standby_domain.py
├── django_setup.py
├── health.py
├── signature.py
├── requests_helper.py
├── resources
│ └── changelog.atom.mako
├── attrdict.py
├── _proxy_helper.py
└── throttle.py
├── rssant_harbor
└── __init__.py
├── rssant_scheduler
├── __init__.py
├── views.py
└── main.py
├── rssant_worker
└── __init__.py
├── requirements-build.txt
├── box
├── rssant.env
├── bin
│ ├── start-nginx.sh
│ ├── start-api.sh
│ ├── start-asyncapi.sh
│ ├── start-worker.sh
│ ├── start-scheduler.sh
│ ├── wait-initdb.sh
│ ├── start-initdb.sh
│ └── start-postgres.sh
├── initdb.sql
├── build-all.sh
├── test.sh
├── setup-container.sh
├── build.sh
├── run.sh
├── push-all.sh
└── build-and-restart.sh
├── rssant_config
└── __init__.py
├── requirements-pip.txt
├── rssant_feedlib
├── useragent.py
├── dotwhat_data
│ ├── other.txt
│ └── font.txt
├── cacert
│ ├── __main__.py
│ ├── __init__.py
│ └── cli.py
├── __init__.py
└── helper.py
├── .mise.toml
├── unmaintain
├── runlocust.sh
├── benchmark
│ ├── benchmark_story_data.py
│ └── benchmark_asyncio_postgres.py
└── convert_qrcode.py
├── frontend
├── postcss.config.js
├── tsconfig.node.json
├── start-dev.sh
├── src
│ ├── main.tsx
│ ├── components
│ │ ├── ui
│ │ │ ├── skeleton.tsx
│ │ │ ├── label.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── input.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── badge.tsx
│ │ │ └── avatar.tsx
│ │ ├── FeedCardSkeleton.tsx
│ │ └── PageTransition.tsx
│ ├── pages
│ │ └── publish
│ │ │ └── PublishContext.tsx
│ └── store
│ │ ├── useServiceClientStore.ts
│ │ └── useThemeStore.ts
├── index.html
├── .gitignore
├── tsconfig.json
└── dev-update.sh
├── cloudflare_worker
└── rssant
│ ├── cloudflare-worker.png
│ ├── .prettierrc
│ ├── wrangler.toml
│ ├── .gitignore
│ ├── package-lock.json
│ └── package.json
├── cursor
└── docs
│ ├── GBF框架应用说明.md
│ ├── 文档索引.md
│ ├── 项目概述.md
│ ├── 项目结构说明.md
│ ├── 外部依赖说明.md
│ ├── 文章导出到飞书-技术方案设计.md
│ ├── GitHub-技术方案设计.md
│ ├── 调度与任务-技术方案设计.md
│ ├── HackerNews-技术方案设计.md
│ ├── 业务流程说明.md
│ ├── RSSHub集成-技术方案设计.md
│ ├── ArXiv-技术方案设计.md
│ ├── 发布能力-技术方案设计.md
│ └── AI娱乐与AIGC-技术方案设计.md
├── .isort.cfg
├── .flake8
├── .env.backup
├── .travis.yml
├── deploy
├── rssant_server
│ ├── build.sh
│ ├── deploy-api.sh
│ ├── deploy-api-prod.sh
│ ├── deploy-worker.sh
│ ├── deploy-worker-prod.sh
│ └── Dockerfile
└── rssant_asyncapi
│ ├── build.sh
│ ├── pyinstaller_build.sh
│ ├── deploy.sh
│ ├── deploy-prod.sh
│ └── Dockerfile
├── etc
├── resolv.conf
└── apt-sources.list
├── requirements-dev.txt
├── pytest.ini
├── .coveragerc
├── prompts
├── github_openai_prompt.txt
├── github_ollama_prompt.txt
├── hacker_news_daily_report_openai_prompt.txt
└── hacker_news_hours_topic_openai_prompt.txt
├── .env.example
├── manage.py
├── .pre-commit-config.yaml
├── runserver.py
└── cleanup_sensitive_data.sh
/rssant/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_cli/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/constraint.txt:
--------------------------------------------------------------------------------
1 | cython<3
2 |
--------------------------------------------------------------------------------
/rssant/helper/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_api/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_api/views/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_asyncapi/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_common/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_harbor/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_scheduler/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_worker/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/dev/quick_check.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/dev/rundev-api.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/common/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/feedlib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/feedserver/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant/middleware/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_api/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/dev/rundev-scheduler.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/dev/rundev-worker.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant/allauth_providers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/dev/start-all-services.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/feedlib/processor/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements-build.txt:
--------------------------------------------------------------------------------
1 | pyinstaller==4.7
2 |
--------------------------------------------------------------------------------
/rssant/allauth_providers/github/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant/allauth_providers/oauth2/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/tools/diagnose_subscription_issue.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_api/models/story_storage/common/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rssant_api/models/story_storage/postgres/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/box/rssant.env:
--------------------------------------------------------------------------------
1 | 致命错误:路径 'box/rssant.env' 在磁盘上,但是不在 'HEAD' 中
2 |
--------------------------------------------------------------------------------
/rssant_config/__init__.py:
--------------------------------------------------------------------------------
1 | from .env import CONFIG, MAX_FEED_COUNT
2 |
--------------------------------------------------------------------------------
/requirements-pip.txt:
--------------------------------------------------------------------------------
1 | pip==20.3.4
2 | wheel==0.36.2
3 | setuptools==51.3.3
4 |
--------------------------------------------------------------------------------
/rssant/settings/__init__.py:
--------------------------------------------------------------------------------
1 | from .settings import * # noqa: F401,F403
2 |
--------------------------------------------------------------------------------
/rssant_common/chnlist/__init__.py:
--------------------------------------------------------------------------------
1 | from .chnlist import CHINA_WEBSITE_LIST
2 |
--------------------------------------------------------------------------------
/rssant_feedlib/useragent.py:
--------------------------------------------------------------------------------
1 | from rssant_common.useragent import * # noqa
2 |
--------------------------------------------------------------------------------
/.mise.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | python = "3.8.12"
3 |
4 | [env]
5 | _.python.venv = ".venv"
6 |
--------------------------------------------------------------------------------
/rssant_feedlib/dotwhat_data/other.txt:
--------------------------------------------------------------------------------
1 | .css - css
2 | .js - javascript
3 | .ico - favicon
4 |
--------------------------------------------------------------------------------
/box/bin/start-nginx.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | /usr/sbin/nginx -g 'daemon off;'
6 |
--------------------------------------------------------------------------------
/rssant_feedlib/cacert/__main__.py:
--------------------------------------------------------------------------------
1 | from .cli import cli
2 |
3 | if __name__ == "__main__":
4 | cli()
5 |
--------------------------------------------------------------------------------
/unmaintain/runlocust.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | locust -f unmaintain/locustfile.py --host 'http://127.0.0.1:6788'
3 |
--------------------------------------------------------------------------------
/box/initdb.sql:
--------------------------------------------------------------------------------
1 | CREATE ROLE rssant LOGIN SUPERUSER PASSWORD 'rssant';
2 | CREATE DATABASE rssant OWNER = rssant;
3 |
--------------------------------------------------------------------------------
/tests/test_arxiv_results.json:
--------------------------------------------------------------------------------
1 | {
2 | "total": 11,
3 | "passed": 11,
4 | "failed": 0,
5 | "errors": []
6 | }
7 |
--------------------------------------------------------------------------------
/rssant_feedlib/cacert/__init__.py:
--------------------------------------------------------------------------------
1 | from .cacert import CacertHelper as _CacertHelper
2 |
3 | where = _CacertHelper().where
4 |
--------------------------------------------------------------------------------
/rssant_api/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class RssantApiConfig(AppConfig):
5 | name = 'rssant_api'
6 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/rssant_common/chnlist/chnlist.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/rssant_common/chnlist/chnlist.txt.gz
--------------------------------------------------------------------------------
/scripts/run-asyncapi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from rssant_asyncapi.main import main
3 |
4 | if __name__ == '__main__':
5 | main()
6 |
--------------------------------------------------------------------------------
/scripts/run-scheduler.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from rssant_scheduler.main import main
3 |
4 | if __name__ == '__main__':
5 | main()
6 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/jsonfeed-failed-no-title-items.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "items": []
4 | }
--------------------------------------------------------------------------------
/tests/fixtures/screenshots/test_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/fixtures/screenshots/test_error.png
--------------------------------------------------------------------------------
/cloudflare_worker/rssant/cloudflare-worker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/cloudflare_worker/rssant/cloudflare-worker.png
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/big5.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/chardet/big5.xml
--------------------------------------------------------------------------------
/cursor/docs/GBF框架应用说明.md:
--------------------------------------------------------------------------------
1 | # GBF框架应用说明
2 |
3 | - 项目未使用 GBF 框架。
4 | - 架构类型为 Django + DRF + 自研调度与多角色运行模型,领域模型以 Django ORM 为核心,聚合模型 `UnionFeed/UnionStory` 为用户视角的数据访问层。
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/euc-jp.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/chardet/euc-jp.xml
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/koi8-r.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/chardet/koi8-r.xml
--------------------------------------------------------------------------------
/.isort.cfg:
--------------------------------------------------------------------------------
1 | [settings]
2 | multi_line_output=VERTICAL_HANGING_INDENT
3 | include_trailing_comma=true
4 | sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
5 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/shift-jis.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/chardet/shift-jis.xml
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/tis-620.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/chardet/tis-620.xml
--------------------------------------------------------------------------------
/cloudflare_worker/rssant/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "all",
5 | "tabWidth": 2,
6 | "printWidth": 80
7 | }
8 |
--------------------------------------------------------------------------------
/cloudflare_worker/rssant/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "rssant"
2 | account_id = "8772320df1662c1375bb57a344a1f78e"
3 | workers_dev = true
4 | compatibility_date = "2024-01-08"
5 |
--------------------------------------------------------------------------------
/cloudflare_worker/rssant/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /dist
3 | **/*.rs.bk
4 | Cargo.lock
5 | bin/
6 | pkg/
7 | wasm-pack.log
8 | worker/
9 | node_modules/
10 | .cargo-ok
11 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/euc-kr:cp949.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/chardet/euc-kr:cp949.xml
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/gb2312:gb18030.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/chardet/gb2312:gb18030.xml
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/gbk:gb18030.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/chardet/gbk:gb18030.json
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/windows-1255.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/chardet/windows-1255.xml
--------------------------------------------------------------------------------
/tests/fixtures/screenshots/test_error_20251120082830.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/fixtures/screenshots/test_error_20251120082830.png
--------------------------------------------------------------------------------
/tests/fixtures/screenshots/test_error_20251120082907.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/fixtures/screenshots/test_error_20251120082907.png
--------------------------------------------------------------------------------
/tests/fixtures/screenshots/test_error_20251120082943.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/fixtures/screenshots/test_error_20251120082943.png
--------------------------------------------------------------------------------
/tests/feedlib/testdata/fulltext/hackernews1_rss.html:
--------------------------------------------------------------------------------
1 | Comments on Hackernews: https://news.ycombinator.com/item?id=25684838
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/warn/https-tmioe-com-feed.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/parser/warn/https-tmioe-com-feed.xml
--------------------------------------------------------------------------------
/tests/feedlib/testdata/fulltext/juejin1_rss.html:
--------------------------------------------------------------------------------
1 | Flutter是一个优秀的UI框架,借助它开箱即用的Widgets我们能够构建出漂亮和高性能的用户界面。那这些Widgets到底是如何工作的又是如何完成渲染的。 在本文中呢,我们就来探析Widgets背后的故事-Flutter渲染机制之三棵树。 在Flutter中和Widget…
--------------------------------------------------------------------------------
/tests/feedlib/testdata/fulltext/juejin2_rss.html:
--------------------------------------------------------------------------------
1 | 上篇文章简单介绍了以下webpack内部WDS的使用,这节讨论一下WDS内部proxy的配置方式。 webpack-devServer,一般简称WDS,是 webpack 内置的用于开发环境的服务器配置。webpack本身提供三种方式用于开发环境修改代码以后自动编译,以提高开发…
2 |
--------------------------------------------------------------------------------
/box/bin/start-api.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | /app/box/bin/wait-initdb.sh
6 |
7 | export RSSANT_ROLE=api
8 | export RSSANT_BIND_ADDRESS=0.0.0.0:6788
9 | /app/runserver.py
10 |
--------------------------------------------------------------------------------
/box/bin/start-asyncapi.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | /app/box/bin/wait-initdb.sh
6 |
7 | export RSSANT_ROLE=asyncapi
8 | export RSSANT_BIND_ADDRESS=0.0.0.0:6786
9 | /app/runserver.py
10 |
--------------------------------------------------------------------------------
/box/bin/start-worker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | /app/box/bin/wait-initdb.sh
6 |
7 | export RSSANT_ROLE=worker
8 | export RSSANT_BIND_ADDRESS=0.0.0.0:6793
9 | /app/runserver.py
10 |
--------------------------------------------------------------------------------
/box/bin/start-scheduler.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | /app/box/bin/wait-initdb.sh
6 |
7 | export RSSANT_ROLE=scheduler
8 | export RSSANT_BIND_ADDRESS=0.0.0.0:6790
9 | /app/runserver.py
10 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/fulltext/hackernews2_rss.html:
--------------------------------------------------------------------------------
1 |
Comments: https://news.ycombinator.com/item?id=25730145
2 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore=
3 | E203 E251 W503 E704
4 | per-file-ignores=
5 | */__init__.py:F401
6 | max-line-length=120
7 | exclude=
8 | rssant_api/migrations/
9 | static/
10 | data/
11 |
--------------------------------------------------------------------------------
/scripts/django_db_init.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from rssant_harbor.django_service import django_run_db_init
4 |
5 | LOG = logging.getLogger(__name__)
6 |
7 |
8 | def run():
9 | django_run_db_init()
10 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_xml_charset_overrides_encoding_2.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/encoding/xml/http_text_xml_charset_overrides_encoding_2.xml
--------------------------------------------------------------------------------
/scripts/tools/pip-compile.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | export PIP_CONSTRAINT="constraint.txt"
6 | pip-compile \
7 | --no-emit-index-url \
8 | --output-file requirements.txt \
9 | requirements.in
10 |
--------------------------------------------------------------------------------
/rssant/allauth_providers/github/urls.py:
--------------------------------------------------------------------------------
1 | from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
2 |
3 | from .provider import RssantGitHubProvider
4 |
5 | urlpatterns = default_urlpatterns(RssantGitHubProvider)
6 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/feed_type/html/http-blog-hexun-com-group-getrssresouce-aspx-classid-74.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/feed_type/html/http-blog-hexun-com-group-getrssresouce-aspx-classid-74.xml
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/http-blog-hexun-com-group-getrssresouce-aspx-classid-74.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VanGongwanxiaowan/rss-aigc/HEAD/tests/feedlib/testdata/parser/failed/http-blog-hexun-com-group-getrssresouce-aspx-classid-74.xml
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_xml_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/rssant/allauth_providers/github/provider.py:
--------------------------------------------------------------------------------
1 | from allauth.socialaccount.providers.github.provider import GitHubProvider
2 |
3 |
4 | class RssantGitHubProvider(GitHubProvider):
5 | """RssantGitHubProvider"""
6 |
7 |
8 | provider_classes = [RssantGitHubProvider]
9 |
--------------------------------------------------------------------------------
/rssant_api/models/story_storage/__init__.py:
--------------------------------------------------------------------------------
1 | from .common.story_data import StoryData
2 | from .common.story_key import StoryId, StoryKey, hash_feed_id
3 | from .postgres.postgres_story import PostgresStoryStorage
4 | from .postgres.postgres_client import PostgresClient
5 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_xml_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_rss_xml_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/rssant_api/models/story_storage/postgres/postgres_sharding.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | VOLUME_SIZE = 64 * 1024
4 |
5 |
6 | def sharding_for(feed_id: int) -> int:
7 | """
8 | 数据分片算法,按 FeedID 范围分片。
9 | 每卷存储 64K 订阅的故事数据,大约64GB,1千万行记录。
10 | """
11 | return feed_id // VOLUME_SIZE
12 |
--------------------------------------------------------------------------------
/box/build-all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | VERSION=$1
6 | if [ -z "$VERSION" ]; then
7 | VERSION='latest'
8 | fi
9 | echo "*** Build guyskk/rssant:$VERSION ***"
10 |
11 | bash box/build.sh \
12 | --platform linux/amd64,linux/arm64 \
13 | -t "guyskk/rssant:$VERSION"
14 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/utf-8.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 | 《11月的蕭邦》是台灣歌手周杰倫發行第六張國語專輯
8 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_rss_xml_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/start-dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # 启动前端开发服务器脚本
3 | # 确保使用正确的 Node.js 版本
4 |
5 | cd "$(dirname "$0")"
6 |
7 | # 加载 nvm
8 | export NVM_DIR="$HOME/.nvm"
9 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
10 |
11 | # 使用 Node.js 18
12 | nvm use 18
13 |
14 | # 启动开发服务器
15 | npm run dev
16 |
--------------------------------------------------------------------------------
/rssant_cli/user.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import click
4 |
5 | import rssant_common.django_setup # noqa:F401
6 |
7 | LOG = logging.getLogger(__name__)
8 |
9 |
10 | @click.group()
11 | def main():
12 | """User Commands"""
13 |
14 |
15 | if __name__ == "__main__":
16 | main()
17 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_rss_xml_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/.env.backup:
--------------------------------------------------------------------------------
1 | # RSS订阅服务配置文件
2 |
3 | # AI分析功能配置(智谱AI)
4 | # AI模型配置:格式 model_id,model_name
5 | RSSANT_AI_MODEL_CONFIG=glm-z1-flash,glm-z1-flash
6 |
7 | # AI API基础URL(智谱AI)
8 | RSSANT_AI_API_BASE_URL=https://open.bigmodel.cn/api/paas/v4
9 |
10 | # AI API密钥(智谱AI)
11 | RSSANT_AI_API_KEY=YOUR_AI_API_KEY
12 |
--------------------------------------------------------------------------------
/rssant_api/resources/opml.mako:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Feeds from RSS-AIGC
5 |
6 |
7 | % for feed in feeds:
8 |
9 | % endfor
10 |
11 |
--------------------------------------------------------------------------------
/scripts/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | pip install -r requirements-pip.txt
6 | PIP_CONSTRAINT=constraint.txt pip install \
7 | -r requirements.txt \
8 | -r requirements-dev.txt \
9 | -r requirements-build.txt
10 |
11 | pre-commit install
12 | pre-commit run --all-files
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | os: linux
2 | dist: xenial
3 | language: shell
4 | services:
5 | - docker
6 | install:
7 | - bash ./box/build.sh \
8 | --build-arg PYPI_MIRROR=https://pypi.org/simple/ \
9 | --build-arg NPM_REGISTERY="--registry=https://registry.npmjs.org"
10 | script:
11 | - bash ./box/test.sh
12 |
--------------------------------------------------------------------------------
/deploy/rssant_server/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # shellcheck disable=SC2068
6 | ezfaas build \
7 | --repository registry.cn-zhangjiakou.aliyuncs.com/rssant/rssant-server \
8 | --dockerfile deploy/rssant_server/Dockerfile \
9 | --build-platform linux/amd64 \
10 | $@
11 |
--------------------------------------------------------------------------------
/deploy/rssant_asyncapi/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # shellcheck disable=SC2068
6 | ezfaas build \
7 | --repository registry.cn-zhangjiakou.aliyuncs.com/rssant/rssant-asyncapi \
8 | --dockerfile deploy/rssant_asyncapi/Dockerfile \
9 | --build-platform linux/amd64 \
10 | $@
11 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_xml_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_atom_xml_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedserver/main.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, redirect
2 |
3 |
4 | app = Flask(__name__)
5 |
6 |
7 | @app.route('/feed/302')
8 | def do_redirect():
9 | target = 'http://blog.guyskk.com/feed.xml'
10 | return redirect(target, 302)
11 |
12 |
13 | if __name__ == "__main__":
14 | app.run()
15 |
--------------------------------------------------------------------------------
/etc/resolv.conf:
--------------------------------------------------------------------------------
1 | nameserver 223.5.5.5
2 | nameserver 223.6.6.6
3 | nameserver 114.114.114.114
4 | nameserver 114.114.115.115
5 | nameserver 180.76.76.76
6 | nameserver 119.29.29.29
7 | nameserver 8.8.8.8
8 | nameserver 8.8.4.4
9 | nameserver 2400:3200::1
10 | nameserver 2400:3200:baba::1
11 | nameserver 2400:da00::6666
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_rss_xml_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/cursor/docs/文档索引.md:
--------------------------------------------------------------------------------
1 | # 文档索引
2 |
3 | - 项目概述:`cursor/docs/项目概述.md`
4 | - 领域模型说明:`cursor/docs/领域模型说明.md`
5 | - 接口文档:`cursor/docs/接口文档.md`
6 | - 业务流程说明:`cursor/docs/业务流程说明.md`
7 | - 项目结构说明:`cursor/docs/项目结构说明.md`
8 | - 外部依赖说明:`cursor/docs/外部依赖说明.md`
9 | - GBF框架应用说明:`cursor/docs/GBF框架应用说明.md`
10 | - 项目梳理文档:`cursor/docs/项目梳理文档.md`
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_xml_dtd_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_atom_xml_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_xml_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/deploy/rssant_asyncapi/pyinstaller_build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | pyinstaller ../scripts/run-asyncapi.py \
4 | --exclude-module django \
5 | --exclude-module lxml \
6 | --exclude-module gevent \
7 | --hidden-import gunicorn.glogging \
8 | --collect-data rssant_common \
9 | -d noarchive \
10 | --noconfirm
11 |
--------------------------------------------------------------------------------
/deploy/rssant_asyncapi/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # shellcheck disable=SC2068
6 | ezfaas deploy-aliyun \
7 | --repository registry.cn-zhangjiakou.aliyuncs.com/rssant/rssant-asyncapi \
8 | --dockerfile deploy/rssant_asyncapi/Dockerfile \
9 | --function rssant-asyncapi-qa \
10 | --build-id \
11 | $@
12 |
--------------------------------------------------------------------------------
/tests/feedlib/test_cacert.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from rssant_feedlib import cacert
3 | from rssant_feedlib.cacert import _CacertHelper
4 |
5 |
6 | def test_cacert():
7 | assert cacert.where()
8 |
9 |
10 | @pytest.mark.xfail(run=False, reason='depends on test network')
11 | def test_cacert_update():
12 | _CacertHelper.update()
13 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_atom_xml_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/deploy/rssant_asyncapi/deploy-prod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # shellcheck disable=SC2068
6 | ezfaas deploy-aliyun \
7 | --repository registry.cn-zhangjiakou.aliyuncs.com/rssant/rssant-asyncapi \
8 | --dockerfile deploy/rssant_asyncapi/Dockerfile \
9 | --function rssant-asyncapi \
10 | --build-id \
11 | $@
12 |
--------------------------------------------------------------------------------
/rssant_common/timezone.py:
--------------------------------------------------------------------------------
1 | from datetime import date, datetime, time, timedelta, timezone # noqa: F401
2 |
3 | UTC = timezone.utc
4 | CST = timezone(timedelta(hours=8), name='Asia/Shanghai')
5 |
6 |
7 | def now() -> datetime:
8 | """
9 | >>> now().tzinfo == UTC
10 | True
11 | """
12 | return datetime.now(timezone.utc)
13 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_xml_epe_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/box/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | bash ./box/run.sh
6 | sleep 3
7 | docker ps --latest
8 | docker logs --tail 1000 rssant
9 |
10 | docker run -ti guyskk/rssant:latest pytest -m 'not dbtest'
11 |
12 | docker exec -ti rssant bash box/bin/wait-initdb.sh
13 | docker exec -ti rssant pytest -m 'dbtest'
14 | docker rm -f rssant || true
15 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 | import 'highlight.js/styles/github.css'
6 |
7 | ReactDOM.createRoot(document.getElementById('root')!).render(
8 |
9 |
10 | ,
11 | )
12 |
13 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_xml_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_rss_xml_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/box/bin/wait-initdb.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | wait_file() {
4 | local file="$1"; shift
5 | # 180 seconds as default timeout
6 | local wait_seconds="${1:-180}"; shift
7 | until test $((wait_seconds--)) -eq 0 -o -f "$file" ; do sleep 1; done
8 | ((++wait_seconds))
9 | }
10 |
11 | wait_file "/app/data/initdb.ready"
12 | echo initdb ready!
13 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_xml_dtd_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/processor/html_redirect/test_html_redirect_1.html:
--------------------------------------------------------------------------------
1 | https://blog.example.com/html-redirect/
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_atom_xml_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_xml_epe_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_xml_epe_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
17 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_rss_xml_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/fulltext/xkcd_rss.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/https-egoist-moe-atom-xml.xml:
--------------------------------------------------------------------------------
1 | EGOIST v1-legacy
2 |
3 | See ya! I no longer publish stuff here (I've never published any meaningful stuff anyways), subscribe my Telegram Channel instead.
4 |
5 | 感觉写不出什么有意义的文章,所以此博客已被我的 Telegram 频道 取代。
6 |
--------------------------------------------------------------------------------
/rssant_common/hashid.py:
--------------------------------------------------------------------------------
1 | """
2 | >>> k = HASH_ID.encode(123)
3 | >>> len(k)
4 | 6
5 | >>> HASH_ID.decode(k)[0]
6 | 123
7 | """
8 | from hashids import Hashids
9 | from rssant_config import CONFIG
10 | from .unionid import UNION_ID_CHARS
11 |
12 | HASH_ID = Hashids(
13 | salt=CONFIG.hashid_salt,
14 | min_length=6,
15 | alphabet=UNION_ID_CHARS.decode('utf-8'),
16 | )
17 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/processor/test_iframe.html:
--------------------------------------------------------------------------------
1 | 《怪物猎人 世界:冰原世纪》x《生化危机2 重制版》联动任务
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_atom_xml_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/rssant-manifest.json:
--------------------------------------------------------------------------------
1 | {"name":"RSS-AIGC","short_name":"RSS-AIGC","theme_color":"#f9f9f9","icons":[{"src":"/img/icons/android-chrome-192x192.png?v=2020032001","sizes":"192x192","type":"image/png"},{"src":"/img/icons/android-chrome-512x512.png?v=2020032001","sizes":"512x512","type":"image/png"}],"start_url":"/","display":"standalone","background_color":"#ffffff"}
--------------------------------------------------------------------------------
/deploy/rssant_server/deploy-api.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # shellcheck disable=SC2068
6 | ezfaas deploy-aliyun \
7 | --repository registry.cn-zhangjiakou.aliyuncs.com/rssant/rssant-server \
8 | --dockerfile deploy/rssant_server/Dockerfile \
9 | --function rssant-api-qa \
10 | --envfile "$RSSANT_ENV_DIR/rssant-api-qa.env" \
11 | --build-id \
12 | $@
13 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_xml_epe_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | 测试配置文件
5 | 包含所有测试共用的常量配置
6 | """
7 | import os
8 |
9 | # 测试环境配置
10 | FRONTEND_URL = os.getenv('FRONTEND_URL', 'http://localhost:5173')
11 | WEBHOOK_URL = os.getenv('WEBHOOK_URL', '')
12 | TEST_USERNAME = os.getenv('TEST_USERNAME', 'admin')
13 | TEST_PASSWORD = os.getenv('TEST_PASSWORD', 'admin')
14 |
15 |
--------------------------------------------------------------------------------
/deploy/rssant_server/deploy-api-prod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # shellcheck disable=SC2068
6 | ezfaas deploy-aliyun \
7 | --repository registry.cn-zhangjiakou.aliyuncs.com/rssant/rssant-server \
8 | --dockerfile deploy/rssant_server/Dockerfile \
9 | --function rssant-api \
10 | --envfile "$RSSANT_ENV_DIR/rssant-api-prod.env" \
11 | --build-id \
12 | $@
13 |
--------------------------------------------------------------------------------
/deploy/rssant_server/deploy-worker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # shellcheck disable=SC2068
6 | ezfaas deploy-aliyun \
7 | --repository registry.cn-zhangjiakou.aliyuncs.com/rssant/rssant-server \
8 | --dockerfile deploy/rssant_server/Dockerfile \
9 | --function rssant-worker-qa \
10 | --envfile "$RSSANT_ENV_DIR/rssant-worker-qa.env" \
11 | --build-id \
12 | $@
13 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | # django
2 | pytest-django==3.10.0
3 |
4 | # test
5 | pytest==5.4.1
6 | pytest-cov==2.8.1
7 | pytest-httpserver==0.3.4
8 | flake8==3.7.9
9 | pycodestyle==2.5.0
10 | isort==5.10.1
11 | coverage==4.5.4
12 | flask==2.0.2
13 | autopep8==1.5
14 | pre-commit==2.3.0
15 | locust==1.4.1
16 | geventhttpclient==1.5.3
17 |
18 | # package
19 | pip-tools==5.5.0
20 | pipdeptree==0.13.2
21 |
--------------------------------------------------------------------------------
/deploy/rssant_server/deploy-worker-prod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # shellcheck disable=SC2068
6 | ezfaas deploy-aliyun \
7 | --repository registry.cn-zhangjiakou.aliyuncs.com/rssant/rssant-server \
8 | --dockerfile deploy/rssant_server/Dockerfile \
9 | --function rssant-worker \
10 | --envfile "$RSSANT_ENV_DIR/rssant-worker-prod.env" \
11 | --build-id \
12 | $@
13 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_xml_dtd_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/http-www-yuming-in-feed.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | norecursedirs=data dist build static .vscode .venv
3 | addopts=--doctest-modules -v --cov . -W all --ignore=data --ignore=box --ignore=unmaintain --ignore=build --ignore=dist --ignore=.venv
4 | doctest_encoding=UTF-8
5 | python_files=test_*.py *_test.py tests.py *_tests.py
6 | DJANGO_SETTINGS_MODULE=rssant.settings
7 | markers=
8 | dbtest: mark a test which need access django database.
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_atom_xml_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/feed_type/html/http-www-yuming-in-feed.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_atom_xml_gb2312_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/http-www-ziyouren888-com-feed.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import pytest
3 | import rssant_common.django_setup # noqa:F401
4 |
5 | from .rss_proxy_server import * # noqa:F401,F403
6 |
7 |
8 | @pytest.fixture(scope="session")
9 | def event_loop():
10 | # Fix pytest-asyncio ResourceWarning: unclosed socket
11 | loop = asyncio.get_event_loop()
12 | try:
13 | yield loop
14 | finally:
15 | loop.close()
16 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/mixed/http_application_rss_xml_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | 《11月的蕭邦》是台灣歌手周杰倫發行第六張國語專輯
9 |
10 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_text_xml_epe_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/processor/html_redirect/test_html_redirect_3.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | The Tudors
4 |
5 |
6 |
7 | This page has moved to a
8 | theTudors.example.com .
9 |
10 |
--------------------------------------------------------------------------------
/rssant_feedlib/__init__.py:
--------------------------------------------------------------------------------
1 | from .parser import FeedParser, FeedResult
2 | from .raw_parser import RawFeedParser, RawFeedResult, FeedParserError
3 | from .feed_checksum import FeedChecksum
4 | from .finder import FeedFinder
5 | from .reader import FeedReader
6 | from .async_reader import AsyncFeedReader
7 | from .response import FeedResponse, FeedContentType, FeedResponseStatus
8 | from .response_builder import FeedResponseBuilder
9 |
--------------------------------------------------------------------------------
/rssant_asyncapi/app.py:
--------------------------------------------------------------------------------
1 | from aiohttp import web
2 |
3 | from rssant_config import CONFIG
4 | from rssant_common.logger import configure_logging
5 |
6 | from .views import routes
7 |
8 |
9 | def create_app():
10 | configure_logging(level=CONFIG.log_level)
11 | api = web.Application()
12 | api.router.add_routes(routes)
13 | app = web.Application()
14 | app.add_subapp('/api/v1', api)
15 | return app
16 |
--------------------------------------------------------------------------------
/rssant_feedlib/cacert/cli.py:
--------------------------------------------------------------------------------
1 | import click
2 | from rssant_common.logger import configure_logging
3 | from .cacert import CacertHelper
4 |
5 |
6 | @click.group()
7 | def cli():
8 | """cacert commands"""
9 |
10 |
11 | @click.option('--debug', is_flag=True)
12 | @cli.command()
13 | def update(debug):
14 | """update cacert"""
15 | if debug:
16 | configure_logging(level='DEBUG')
17 | CacertHelper.update()
18 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | .venv/*
4 | */migrations/*
5 | */tests/*
6 | */test_*.py
7 | manage.py
8 | */settings/*
9 | */__init__.py
10 | source = .
11 | branch = True
12 |
13 | [report]
14 | exclude_lines =
15 | pragma: no cover
16 | def __repr__
17 | raise AssertionError
18 | raise NotImplementedError
19 | if __name__ == .__main__.:
20 | if TYPE_CHECKING:
21 | @abstractmethod
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | AIGC商业新闻平台
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_xml_epe_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/rssant_common/base64.py:
--------------------------------------------------------------------------------
1 | import base64
2 |
3 |
4 | class UrlsafeBase64:
5 |
6 | @classmethod
7 | def encode(cls, data: bytes) -> str:
8 | if not data:
9 | return ''
10 | return base64.urlsafe_b64encode(data).decode('ascii')
11 |
12 | @classmethod
13 | def decode(cls, data: str) -> bytes:
14 | if not data:
15 | return b''
16 | return base64.urlsafe_b64decode(data)
17 |
--------------------------------------------------------------------------------
/frontend/src/pages/publish/PublishContext.tsx:
--------------------------------------------------------------------------------
1 | import { Feed, UserPublish } from '@/lib/api'
2 | import { useOutletContext } from 'react-router-dom'
3 |
4 | export interface PublishOutletContext {
5 | unionId: string
6 | info: UserPublish | null
7 | feeds: Feed[]
8 | reload: () => Promise
9 | loading: boolean
10 | error?: string | null
11 | }
12 |
13 | export const usePublishContext = () => useOutletContext()
14 |
15 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/mixed/http_application_atom_xml_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | 《11月的蕭邦》是台灣歌手周杰倫發行第六張國語專輯
9 |
10 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/xml/http_application_atom_xml_gb2312_charset_overrides_encoding.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/processor/html_redirect/test_html_redirect_2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | The Tudors
4 |
5 |
6 |
7 | This page has moved to a
8 | theTudors.example.com .
9 |
10 |
--------------------------------------------------------------------------------
/cloudflare_worker/rssant/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rssant",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "prettier": {
8 | "version": "1.19.1",
9 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
10 | "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
11 | "dev": true
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0025_auto_20200516_0959.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.12 on 2020-05-16 09:59
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0024_auto_20200510_1008'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveIndex(
14 | model_name='story',
15 | name='rssant_api__feed_id_1bed8a_idx',
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/cloudflare_worker/rssant/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "rssant",
4 | "version": "1.0.0",
5 | "description": "RSSAnt Proxy",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "format": "prettier --write '**/*.{js,css,json,md}'"
10 | },
11 | "author": "guyskk ",
12 | "license": "MIT",
13 | "devDependencies": {
14 | "prettier": "^1.18.2"
15 | },
16 | "dependencies": {}
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # Environment variables
27 | .env
28 | .env.local
29 | .env.production.local
30 | .env.development.local
31 |
32 |
--------------------------------------------------------------------------------
/prompts/github_openai_prompt.txt:
--------------------------------------------------------------------------------
1 | 你接下来收到的都是开源项目的最新进展。
2 |
3 | 你根据进展,总结成一个中文的报告,以 项目名称和日期 开头,包含:新增功能、主要改进,修复问题等章节。
4 |
5 | 参考示例如下:
6 |
7 | # LangChain 项目进展
8 |
9 | ## 时间周期:2024-08-13至2024-08-18
10 |
11 | ## 新增功能
12 | - langchain-box: 添加langchain box包和DocumentLoader
13 | - 添加嵌入集成测试
14 |
15 | ## 主要改进
16 | - 将@root_validator用法升级以与pydantic 2保持一致
17 | - 将根验证器升级为与pydantic 2兼容
18 |
19 | ## 修复问题
20 | - 修复Azure的json模式问题
21 | - 修复Databricks Vector Search演示笔记本问题
22 | - 修复Microsoft Azure Cosmos集成测试中的连接字符串问题
23 |
24 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0002_auto_20190317_1020.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.7 on 2019-03-17 10:20
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='feed',
15 | name='dt_updated',
16 | field=models.DateTimeField(help_text='更新时间'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/rssant_cli/scripts/fix_user_story_feed_id.sql:
--------------------------------------------------------------------------------
1 | --- 修复feed merge导致user story数据不一致的问题
2 | WITH target AS (
3 | SELECT us.id AS id, us.feed_id AS old_feed_id, uf.feed_id AS new_feed_id
4 | FROM rssant_api_userstory AS us
5 | JOIN rssant_api_userfeed AS uf ON us.user_feed_id = uf.id
6 | WHERE us.feed_id != uf.feed_id
7 | )
8 | UPDATE rssant_api_userstory AS us
9 | SET feed_id=target.new_feed_id
10 | FROM target WHERE us.id=target.id
11 | RETURNING target.id, us.user_id, target.old_feed_id, target.new_feed_id;
12 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/well/jsonfeed-example.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "My Example Feed",
4 | "home_page_url": "https://example.org/",
5 | "feed_url": "https://example.org/feed.json",
6 | "items": [
7 | {
8 | "id": "2",
9 | "content_text": "This is a second item.",
10 | "url": "https://example.org/second-item"
11 | },
12 | {
13 | "id": "1",
14 | "content_html": "Hello, world!
",
15 | "url": "https://example.org/initial-post"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/prompts/github_ollama_prompt.txt:
--------------------------------------------------------------------------------
1 | 你是一个热爱开源社区的技术爱好者,经常关注 GitHub 上热门开源项目的进展。
2 |
3 | 任务:
4 | 1.你收到的开源项目 Closed issues 分类整理为:新增功能、主要改进,修复问题等。
5 | 2.将1中的整理结果生成一个中文报告,符合以下的参考格式
6 |
7 | 格式:
8 | # {repo} 项目进展
9 |
10 | ## 时间周期:{date}
11 |
12 | ## 新增功能
13 | - langchain-box: 添加langchain box包和DocumentLoader
14 | - 添加嵌入集成测试
15 |
16 | ## 主要改进
17 | - 将@root_validator用法升级以与pydantic 2保持一致
18 | - 将根验证器升级为与pydantic 2兼容
19 |
20 | ## 修复问题
21 | - 修复Azure的json模式问题
22 | - 修复Databricks Vector Search演示笔记本问题
23 | - 修复Microsoft Azure Cosmos集成测试中的连接字符串问题
24 |
25 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0015_story_has_mathjax.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2019-10-30 15:09
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0014_auto_20191027_0558'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='story',
15 | name='has_mathjax',
16 | field=models.BooleanField(blank=True, default=False, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/box/bin/start-initdb.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | rm -f /app/data/initdb.ready
6 |
7 | # wait postgres ready
8 | su - postgres -c 'while true; do /usr/bin/pg_isready; status=$?; if [[ $status -eq 0 ]]; then break; fi; sleep 1; done;'
9 |
10 | # execute initdb.sql
11 | su - postgres -c 'psql -f /app/box/initdb.sql'
12 |
13 | # django migrate & initdb
14 | python manage.py runscript django_pre_migrate
15 | python manage.py migrate
16 | python manage.py runscript django_db_init
17 |
18 | touch /app/data/initdb.ready
19 | exit 0
20 |
--------------------------------------------------------------------------------
/rssant/tests/email_template_tests.py:
--------------------------------------------------------------------------------
1 | from rssant.email_template import EMAIL_CONFIRM_TEMPLATE
2 | from rssant_config import CONFIG
3 |
4 |
5 | def test_render_email_confirm():
6 | ctx = {
7 | "link": 'https://rss.anyant.com/',
8 | "username": 'guyskk@localhost.com',
9 | 'rssant_url': CONFIG.root_url,
10 | 'rssant_email': CONFIG.smtp_username,
11 | }
12 | html = EMAIL_CONFIRM_TEMPLATE.render_html(**ctx)
13 | for key, value in ctx.items():
14 | assert value in html, f'{key} {value!r} not rendered'
15 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0029_userfeed_group.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.13 on 2020-11-01 11:18
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0028_auto_20200619_0906'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='userfeed',
15 | name='group',
16 | field=models.CharField(blank=True, help_text='用户设置的分组', max_length=200, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0023_feed_response_status.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.12 on 2020-04-28 12:31
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0022_feed_warnings'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feed',
15 | name='response_status',
16 | field=models.IntegerField(blank=True, help_text='response status code', null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/scripts/stop_rsshub.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # RSSHub 停止脚本
4 |
5 | set -e
6 |
7 | # 获取脚本所在目录
8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9 | cd "$SCRIPT_DIR/.."
10 |
11 | # 使用 docker compose 或 docker-compose
12 | if docker compose version &> /dev/null; then
13 | DOCKER_COMPOSE="docker compose"
14 | else
15 | DOCKER_COMPOSE="docker-compose"
16 | fi
17 |
18 | # 配置文件路径
19 | COMPOSE_FILE="docker-compose.rsshub.yml"
20 |
21 | echo "停止 RSSHub 服务..."
22 | $DOCKER_COMPOSE -f "$COMPOSE_FILE" down
23 |
24 | echo "✓ RSSHub 服务已停止"
25 |
26 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/encoding/chardet/utf-8.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "My Example Feed",
4 | "home_page_url": "https://example.org/",
5 | "feed_url": "https://example.org/feed.json",
6 | "items": [
7 | {
8 | "id": "2",
9 | "content_text": "This is a second item.",
10 | "url": "https://example.org/second-item"
11 | },
12 | {
13 | "id": "1",
14 | "content_html": "Hello, world!
",
15 | "url": "https://example.org/initial-post"
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0019_feed_use_proxy.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.12 on 2020-04-02 14:57
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0018_feed_freeze_level'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feed',
15 | name='use_proxy',
16 | field=models.BooleanField(blank=True, default=False, help_text='use proxy or not', null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0030_storyinfo_sentence_count.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.13 on 2020-11-22 06:59
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0029_userfeed_group'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='storyinfo',
15 | name='sentence_count',
16 | field=models.IntegerField(blank=True, help_text='sentence count', null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0031_story_sentence_count.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.13 on 2020-11-22 11:18
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0030_storyinfo_sentence_count'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='story',
15 | name='sentence_count',
16 | field=models.IntegerField(blank=True, help_text='sentence count', null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0020_feed_checksum_data.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.12 on 2020-04-16 11:17
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0019_feed_use_proxy'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feed',
15 | name='checksum_data',
16 | field=models.BinaryField(blank=True, help_text='feed checksum data', max_length=4096, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0022_feed_warnings.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.12 on 2020-04-18 09:54
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0021_auto_20200418_0512'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feed',
15 | name='warnings',
16 | field=models.TextField(blank=True, help_text='warning messages when processing the feed', null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0007_userfeed_is_from_bookmark.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.7 on 2019-04-19 14:28
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0006_auto_20190418_0945'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='userfeed',
15 | name='is_from_bookmark',
16 | field=models.BooleanField(blank=True, default=False, help_text='是否从书签导入', null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/rssant_api/views/common.py:
--------------------------------------------------------------------------------
1 | import secrets
2 |
3 | from rest_framework.permissions import BasePermission
4 |
5 | from rssant_config import CONFIG
6 |
7 |
8 | class AllowServiceClient(BasePermission):
9 | def has_permission(self, request, view):
10 | secret = request.META.get('HTTP_X_RSSANT_SERVICE_SECRET') or ''
11 | expected_secret = CONFIG.service_secret or ''
12 | return secrets.compare_digest(secret, expected_secret)
13 |
14 | def has_object_permission(self, request, view, obj):
15 | return self.has_permission(request, view)
16 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0004_rawfeed_is_gzipped.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.7 on 2019-03-27 13:54
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0003_auto_20190327_1304'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='rawfeed',
15 | name='is_gzipped',
16 | field=models.BooleanField(blank=True, default=False, help_text='is content gzip compressed', null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/box/setup-container.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | mkdir -p logs/
6 | mkdir -p data/
7 | chmod a+x box/bin/*
8 |
9 | # config postgres
10 | sed -ri "s!^#?(listen_addresses)\s*=\s*\S+.*!\1 = '*'!" /etc/postgresql/11/main/postgresql.conf
11 | echo 'host all all all md5' >> /etc/postgresql/11/main/pg_hba.conf
12 | mv /var/lib/postgresql/11/main /var/lib/postgresql/11/init
13 | mkdir /var/lib/postgresql/11/main
14 |
15 | # config supervisor
16 | cat box/supervisord.conf > /etc/supervisord.conf
17 |
18 | # config nginx
19 | cat box/nginx/nginx.conf > /etc/nginx/nginx.conf
20 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0028_auto_20200619_0906.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.12 on 2020-06-19 09:06
2 |
3 | from django.db import migrations, models
4 | import django.utils.timezone
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('rssant_api', '0027_storyinfo'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='storyinfo',
16 | name='dt_created',
17 | field=models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/rssant_api/views/errors.py:
--------------------------------------------------------------------------------
1 | from rest_framework.exceptions import APIException, ErrorDetail
2 | from rest_framework.status import HTTP_400_BAD_REQUEST
3 |
4 |
5 | class RssantAPIException(APIException):
6 |
7 | status_code = HTTP_400_BAD_REQUEST
8 | default_detail = 'Invalid request'
9 | default_code = 'invalid'
10 |
11 | def __init__(self, detail=None, code=None):
12 | if detail is None:
13 | detail = self.default_detail
14 | if code is None:
15 | code = self.default_code
16 | self.detail = ErrorDetail(detail, code)
17 |
--------------------------------------------------------------------------------
/box/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | current_repo=$(git config --get remote.origin.url)
6 | web_repo=$(python -c '
7 | import sys;
8 | p=sys.argv[1].rsplit("/",1);
9 | w=p[1].replace("rssant","rssant-web");
10 | print(p[0]+"/"+w)
11 | ' "$current_repo")
12 | echo "rssant-web: $web_repo"
13 |
14 | if [ ! -d "box/web" ]; then
15 | git clone "$web_repo" box/web
16 | fi
17 | pushd box/web || exit 1
18 | git fetch
19 | git checkout master
20 | git pull --ff-only
21 | popd || exit 1
22 |
23 | # shellcheck disable=SC2068
24 | docker build --progress plain -f box/Dockerfile . $@
25 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0018_feed_freeze_level.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.10 on 2020-03-26 16:40
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0017_auto_20191217_1253'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feed',
15 | name='freeze_level',
16 | field=models.IntegerField(blank=True, default=1, help_text='freeze level, 1: normal, N: slow down N times', null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/http-porn191-com-index-php-feed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Contact Support
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/models/test_story_key.py:
--------------------------------------------------------------------------------
1 | from rssant_api.models.story_storage.common.story_key import StoryId, hash_feed_id
2 |
3 |
4 | def test_hash_feed_id():
5 | for i in [0, 1, 2, 7, 1024, 2**31, 2**32 - 1]:
6 | val = hash_feed_id(i)
7 | assert val >= 0 and val < 2**32
8 |
9 |
10 | def test_story_id():
11 | cases = [
12 | (123, 10, 0x7b000000a0),
13 | (123, 1023, 0x7b00003ff0),
14 | ]
15 | for feed_id, offset, story_id in cases:
16 | assert StoryId.encode(feed_id, offset) == story_id
17 | assert StoryId.decode(story_id) == (feed_id, offset)
18 |
--------------------------------------------------------------------------------
/rssant/helper/content_hash.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import hashlib
3 |
4 | from rssant.settings import RSSANT_CONTENT_HASH_METHOD
5 |
6 |
7 | def compute_hash(*fields):
8 | """bytes -> bytes"""
9 | h = hashlib.new(RSSANT_CONTENT_HASH_METHOD)
10 | for content in fields:
11 | if isinstance(content, str):
12 | content = content.encode('utf-8')
13 | h.update(content or b'')
14 | return h.digest()
15 |
16 |
17 | def compute_hash_base64(*fields):
18 | """bytes -> base64 string"""
19 | value = compute_hash(*fields)
20 | return base64.b64encode(value).decode()
21 |
--------------------------------------------------------------------------------
/scripts/postgres_start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker network create rssant || true
4 | docker volume create rssant_postgres
5 | docker rm -f rssant-postgres
6 | docker run -d \
7 | --name rssant-postgres \
8 | --log-driver json-file --log-opt max-size=50m --log-opt max-file=10 \
9 | --restart unless-stopped \
10 | --memory=500M \
11 | --cpus=0.5 \
12 | --network rssant \
13 | -p 127.0.0.1:5432:5432 \
14 | -e "POSTGRES_USER=rssant" \
15 | -e "POSTGRES_PASSWORD=rssant" \
16 | -e "POSTGRES_DB=rssant" \
17 | -v rssant_postgres:/var/lib/postgresql/data \
18 | postgres:11.7
19 |
--------------------------------------------------------------------------------
/rssant_api/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from . import models
4 |
5 |
6 | def _register(model):
7 | if not hasattr(model, 'Admin'):
8 | return admin.site.register(m)
9 |
10 | class RssantModelAdmin(admin.ModelAdmin):
11 | if hasattr(model.Admin, 'display_fields'):
12 | list_display = tuple(['id'] + model.Admin.display_fields)
13 | if hasattr(model.Admin, 'search_fields'):
14 | search_fields = model.Admin.search_fields
15 |
16 | return admin.site.register(m, RssantModelAdmin)
17 |
18 |
19 | for m in models.__models__:
20 | _register(m)
21 |
--------------------------------------------------------------------------------
/rssant_scheduler/views.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from aiohttp import web
4 |
5 | from rssant_common.health import health_info
6 |
7 | routes = web.RouteTableDef()
8 |
9 |
10 | def JsonResponse(data: dict):
11 | text = json.dumps(data, ensure_ascii=False, indent=4)
12 | return web.json_response(text=text)
13 |
14 |
15 | @routes.get('/')
16 | async def index(request):
17 | return JsonResponse({'message': '你好,RSS-AIGC Async API!'})
18 |
19 |
20 | @routes.get('/health')
21 | async def health(request):
22 | result = health_info()
23 | result.update(role='scheduler')
24 | return JsonResponse(result)
25 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0056_add_arxiv_report_json_data.py:
--------------------------------------------------------------------------------
1 | # Generated manually
2 |
3 | from django.db import migrations
4 | import django.contrib.postgres.fields.jsonb
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('rssant_api', '0055_create_feishu_bot_send_history'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='arxivreport',
16 | name='report_json_data',
17 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='报告JSON数据(论文列表及AI分析)', null=True),
18 | ),
19 | ]
20 |
21 |
--------------------------------------------------------------------------------
/rssant/middleware/timer.py:
--------------------------------------------------------------------------------
1 | import time
2 | import logging
3 | from django.http import HttpResponse, HttpRequest
4 |
5 |
6 | LOG = logging.getLogger(__name__)
7 |
8 |
9 | class RssantTimerMiddleware:
10 | def __init__(self, get_response):
11 | self.get_response = get_response
12 |
13 | def __call__(self, request: HttpRequest) -> HttpResponse:
14 | t_begin = time.monotonic()
15 | response = self.get_response(request)
16 | cost_ms = round((time.monotonic() - t_begin) * 1000)
17 | LOG.info(f'X-Time: {cost_ms}ms')
18 | response['X-Time'] = f'{cost_ms}ms'
19 | return response
20 |
--------------------------------------------------------------------------------
/tests/test_changelog.py:
--------------------------------------------------------------------------------
1 | from rssant_common.changelog import ChangeLog, ChangeLogList
2 |
3 |
4 | def test_changelog():
5 | changelog = ChangeLog.from_path('docs/changelog/1.0.0.md')
6 | assert changelog.version == '1.0.0'
7 | assert changelog.date
8 | assert changelog.title
9 | assert changelog.html
10 |
11 |
12 | def test_changelog_list():
13 | changelog_list = ChangeLogList(
14 | title='RSS-AIGC 更新日志',
15 | link='https://rss.anyant.com/changelog',
16 | directory='docs/changelog',
17 | )
18 | assert changelog_list.items
19 | assert changelog_list.to_atom()
20 | assert changelog_list.to_html()
21 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/jsonfeed-failed-syntax.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "JSON Feed",
4 | "home_page_url": "https://www.v2ex.com/go/jsonfeed",
5 | "feed_url": "https://www.v2ex.com/feed/jsonfeed.json",
6 | "icon": "https://cdn.v2ex.com/navatar/db57/6a7d/1054_large.png?m=1567059308",
7 | "favicon": "https://cdn.v2ex.com/navatar/db57/6a7d/1054_normal.png?m=1567059308",
8 | "items": [
9 | {
10 | "author": {
11 | "url": "https://www.v2ex.com/member/DEVN",
12 | "name": "DEVN",
13 | "avatar": "https://cdn.v2ex.com/avatar/6fc1/cc76/467193_large.png?m=1583239760"
14 | },
15 | }
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # RSSant 配置文件
2 | # 复制此文件为 .env 并填入实际的配置值
3 |
4 | # ============================================
5 | # AI API 配置
6 | # ============================================
7 | # AI API 密钥(智谱AI或OpenRouter API Key)
8 | RSSANT_AI_API_KEY=YOUR_AI_API_KEY
9 |
10 | # AI 模型配置(格式:model_id,model_name)
11 | RSSANT_AI_MODEL_CONFIG=glm-z1-flash,glm-z1-flash
12 |
13 | # ============================================
14 | # AI 搜索和娱乐信息 API 配置
15 | # ============================================
16 | # Tavily API Key(用于AI影视和AIGC信息搜索)
17 | RSSANT_TAVILY_API_KEY=YOUR_TAVILY_API_KEY
18 |
19 | # 智谱AI API Key(如果与ai_api_key不同可单独配置)
20 | # 如果不配置,将使用 RSSANT_AI_API_KEY
21 | RSSANT_ZHIPU_API_KEY=YOUR_ZHIPU_API_KEY
22 |
--------------------------------------------------------------------------------
/tests/sample/inoreader.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Subscriptions from Inoreader [https://www.inoreader.com]
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/rssant_common/blacklist.py:
--------------------------------------------------------------------------------
1 | import re
2 | from urllib.parse import urlparse
3 |
4 |
5 | def _parse_blacklist(text):
6 | lines = set()
7 | for line in text.strip().splitlines():
8 | if line.strip():
9 | lines.add(line.strip())
10 | items = []
11 | for line in list(sorted(lines)):
12 | items.append(r'((.*\.)?{})'.format(line))
13 | pattern = re.compile('|'.join(items), re.I)
14 | return pattern
15 |
16 |
17 | def compile_url_blacklist(text):
18 | black_re = _parse_blacklist(text)
19 |
20 | def is_in_blacklist(url):
21 | url = urlparse(url)
22 | return black_re.fullmatch(url.netloc) is not None
23 |
24 | return is_in_blacklist
25 |
--------------------------------------------------------------------------------
/etc/apt-sources.list:
--------------------------------------------------------------------------------
1 | deb https://mirrors.aliyun.com/debian/ bullseye main non-free contrib
2 | deb-src https://mirrors.aliyun.com/debian/ bullseye main non-free contrib
3 | deb https://mirrors.aliyun.com/debian-security/ bullseye-security main
4 | deb-src https://mirrors.aliyun.com/debian-security/ bullseye-security main
5 | deb https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib
6 | deb-src https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib
7 | # 阿里云镜像暂未提供 bullseye-backports Release,禁用避免 apt 失败
8 | # deb https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib
9 | # deb-src https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib
10 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 |
4 | import rssant_common.django_setup # noqa:F401
5 | from rssant_config import CONFIG
6 | from rssant_common.logger import configure_logging
7 |
8 |
9 | if __name__ == '__main__':
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | configure_logging(level=CONFIG.log_level)
19 | execute_from_command_line(sys.argv)
20 |
--------------------------------------------------------------------------------
/rssant/allauth_providers/helper.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from rssant_common import _proxy_helper
4 | from rssant_common.rss_proxy import RSSProxyClient, ProxyStrategy
5 |
6 |
7 | LOG = logging.getLogger(__name__)
8 |
9 |
10 | def _proxy_strategy(url):
11 | if 'github.com' in url:
12 | return ProxyStrategy.DIRECT_FIRST
13 | else:
14 | return ProxyStrategy.DIRECT
15 |
16 |
17 | def oauth_api_request(method, url, **kwargs):
18 | """
19 | when network error, fallback to use rss proxy
20 | """
21 | options = _proxy_helper.get_proxy_options(url=url)
22 | client = RSSProxyClient(**options, proxy_strategy=_proxy_strategy)
23 | return client.request(method, url, **kwargs)
24 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0024_auto_20200510_1008.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.12 on 2020-05-10 10:08
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0023_feed_response_status'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feed',
15 | name='reverse_url',
16 | field=models.TextField(blank=True, help_text='倒转URL', null=True),
17 | ),
18 | migrations.AddIndex(
19 | model_name='feed',
20 | index=models.Index(fields=['reverse_url'], name='rssant_api__reverse_ddd20d_idx'),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/rssant_common/standby_domain.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpRequest
2 | from rssant_config import CONFIG
3 |
4 |
5 | def get_request_domain(request: HttpRequest) -> str:
6 | request_host = request and request.headers.get('x-rssant-host')
7 | if not request_host:
8 | request_host = request and request.get_host()
9 | if request_host:
10 | request_domain = request_host.split(':')[0]
11 | if request_domain in CONFIG.standby_domain_set:
12 | return request_domain
13 | return CONFIG.root_domain
14 |
15 |
16 | def get_request_root_url(request: HttpRequest) -> str:
17 | domain = get_request_domain(request)
18 | return CONFIG.root_url.replace(CONFIG.root_domain, domain)
19 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/processor/test_iframe_link.html:
--------------------------------------------------------------------------------
1 | 邱瑞讲租房经历这段太有意思了,头一次感觉数学知识能这么好笑
(视频 )
喷嚏网官方App :【安卓】在 豌豆荚 、360手机助手、小米应用商店,搜索:喷嚏阅读;【ios】App store里搜索:喷嚏网官方阅读; 喷嚏网官方网站:http://dapenti.com (海外访问:https://dapenti.com) 每天网络精华尽在【喷嚏图卦 】 喷嚏网官方新浪围脖
--------------------------------------------------------------------------------
/rssant_api/models/errors.py:
--------------------------------------------------------------------------------
1 | from .helper import ConcurrentUpdateError
2 |
3 |
4 | class RssantModelError(Exception):
5 | """Base exception for rssant API models"""
6 |
7 |
8 | class FeedExistError(RssantModelError):
9 | """Feed already exists"""
10 |
11 |
12 | class FeedNotFoundError(RssantModelError):
13 | """Feed not found"""
14 |
15 |
16 | class StoryNotFoundError(RssantModelError):
17 | """Story not found"""
18 |
19 |
20 | class FeedStoryOffsetError(RssantModelError):
21 | """Feed story_offset error"""
22 |
23 |
24 | __all__ = (
25 | 'ConcurrentUpdateError',
26 | 'RssantModelError',
27 | 'FeedExistError',
28 | 'FeedNotFoundError',
29 | 'StoryNotFoundError',
30 | 'FeedStoryOffsetError',
31 | )
32 |
--------------------------------------------------------------------------------
/rssant/middleware/message_storage.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.contrib.messages.storage.base import BaseStorage
3 | from django.contrib.messages.storage.cookie import CookieStorage
4 |
5 |
6 | class FakeMessageStorage(BaseStorage):
7 | """A fake messge storage to disable messages middleware"""
8 |
9 | def _get(self, *args, **kwargs):
10 | return [], True
11 |
12 | def _store(self, messages, response, *args, **kwargs):
13 | # see also: CookieStorage._store
14 | cookie_name = CookieStorage.cookie_name
15 | data = self.request.COOKIES.get(cookie_name)
16 | if data:
17 | response.delete_cookie(
18 | cookie_name, domain=settings.SESSION_COOKIE_DOMAIN)
19 | return []
20 |
--------------------------------------------------------------------------------
/rssant_feedlib/dotwhat_data/font.txt:
--------------------------------------------------------------------------------
1 | .FNT - Font FileVery Common
2 | .FON - Font FileVery Common
3 | .OTF - OpenType Font FormatVery Common
4 | .TTF - TrueType FontVery Common
5 | .WOFF - Web Open Font Format FileVery Common
6 | .TTC - TrueType Font Collection FileCommon
7 | .ACFM - Adobe Font Metrics FileAverage
8 | .GDF - PHP GD Library Font FileAverage
9 | .GTF - German Type Foundry FontAverage
10 | .MMM - Adobe Type Manager Multiple-Master Metrics FontAverage
11 | .PFA - PostScript Printer Font ASCII FileAverage
12 | .AMFM - Adobe Multiple Font Metrics Filenot specified
13 | .DFONT - Mac OS X Data Fork Fontnot specified
14 | .EOT - Embedded OpenType Fontnot specified
15 | .GDR - Symbian OS Font Filenot specified
16 | .TPF - Downloadable PCL Soft font filenot specified
17 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0032_auto_20201227_0636.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.13 on 2020-12-27 06:36
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0031_story_sentence_count'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feedcreation',
15 | name='group',
16 | field=models.CharField(blank=True, help_text='用户设置的分组', max_length=200, null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='feedcreation',
20 | name='title',
21 | field=models.CharField(blank=True, help_text='用户设置的标题', max_length=200, null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0016_auto_20191105_1247.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2019-11-05 12:47
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0015_story_has_mathjax'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feed',
15 | name='dryness',
16 | field=models.IntegerField(blank=True, default=0, help_text='Dryness of the feed', null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='feed',
20 | name='dt_first_story_published',
21 | field=models.DateTimeField(blank=True, help_text='最老的story发布时间', null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/v2ex-jsonfeed-no-storys.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "JSON Feed",
4 | "description": "\u7c7b\u4f3c RSS \u7684\u7ad9\u70b9\u5185\u5bb9\u4fe1\u606f\u6d41 JSON \u683c\u5f0f\u3002\u8fd9\u91cc\u8ba8\u8bba JSON Feed \u7684\u5b9e\u73b0\u53ca\u9605\u8bfb\u5668\u652f\u6301\u3002",
5 | "home_page_url": "https://www.v2ex.com/go/jsonfeed",
6 | "feed_url": "https://www.v2ex.com/feed/jsonfeed.json",
7 | "icon": "https://cdn.v2ex.com/navatar/db57/6a7d/1054_large.png?m=1567059308",
8 | "favicon": "https://cdn.v2ex.com/navatar/db57/6a7d/1054_normal.png?m=1567059308",
9 | "items": [
10 | {
11 | "date_published": "2020-01-31T15:05:16+00:00",
12 | "content_html": "2020-01-31"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/tests/models/test_story_unique_ids.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from rssant_api.models.story_unique_ids import StoryUniqueIdsData
3 |
4 |
5 | CASES = {
6 | 'empty': (0, []),
7 | 'one': (1, [
8 | '93C07B6C-D848-4405-A349-07A3775FA0A9',
9 | ]),
10 | 'two': (3, [
11 | 'https://www.example.com/2.html',
12 | 'https://www.example.com/3.html',
13 | ])
14 | }
15 |
16 |
17 | @pytest.mark.parametrize('case_name', list(CASES))
18 | def test_encode_decode(case_name):
19 | begin_offset, unique_ids = CASES[case_name]
20 | data = StoryUniqueIdsData(begin_offset, unique_ids=unique_ids)
21 | data_bytes = data.encode()
22 | got = StoryUniqueIdsData.decode(data_bytes)
23 | assert got.begin_offset == begin_offset
24 | assert got.unique_ids == unique_ids
25 |
--------------------------------------------------------------------------------
/cursor/docs/项目概述.md:
--------------------------------------------------------------------------------
1 | # 项目概述
2 |
3 | ## 项目背景
4 |
5 | - RSS-AIGC 系统,结合 AI 报告、飞书机器人等业务能力,支持本地与容器化部署。
6 |
7 | ## 项目目标
8 |
9 | - 提供稳定的 RSS 订阅与全文阅读能力,支持跨平台阅读、收藏与已读管理。
10 | - 集成 AI 行业搜索与报告生成、飞书机器人定时推送等增值功能。
11 |
12 | ## 功能概述
13 |
14 | - 订阅源管理、文章读取与状态管理(收藏、已读)。
15 | - 行业资讯聚合(Hacker News/GitHub/ArXiv)与 AI 内容加工。
16 | - AI 娱乐与 AIGC 报告生成与导出(例如导出到飞书文档)。
17 | - 飞书机器人配置管理与定时推送。
18 | - 前后端分离,后端提供 REST API 与 Swagger 文档。
19 |
20 | ## 技术栈
21 |
22 | - 后端:Django、DRF、AllAuth、Whitenoise、PostgreSQL、supervisor。
23 | - 前端:React 18、TypeScript、Vite、Tailwind、Radix UI/shadcn、Zustand。
24 | - 调度与多角色:`api/worker/scheduler` 多角色运行,`aiohttp` 服务(rssant_scheduler)。
25 | - 容器与部署:Docker 多阶段构建、supervisord 管理进程、脚本化部署。
26 |
27 | ## 架构类型
28 |
29 | - 未使用 GBF 框架;后端采用 Django + DRF 的分层组织,领域模型以 Django ORM 模型为核心,配合 `UnionFeed/UnionStory` 聚合访问层与服务层。
--------------------------------------------------------------------------------
/rssant_api/migrations/0044_add_citation_fields.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.28 on 2025-11-17 02:08
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0043_add_arxiv_multidimensional_scores'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='arxivpaper',
15 | name='citation_count',
16 | field=models.IntegerField(blank=True, help_text='引用数(来自 Semantic Scholar)', null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='arxivpaper',
20 | name='dt_citation_updated',
21 | field=models.DateTimeField(blank=True, help_text='引用数更新时间', null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0047_add_ai_entertainment_refs.py:
--------------------------------------------------------------------------------
1 | # Generated manually
2 |
3 | from django.db import migrations
4 | import jsonfield.fields
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('rssant_api', '0046_add_ai_entertainment_report'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='aientertainmentreport',
16 | name='ai_entertainment_refs',
17 | field=jsonfield.fields.JSONField(default=list, help_text='AI影视参考链接列表'),
18 | ),
19 | migrations.AddField(
20 | model_name='aientertainmentreport',
21 | name='aigc_refs',
22 | field=jsonfield.fields.JSONField(default=list, help_text='AIGC参考链接列表'),
23 | ),
24 | ]
25 |
26 |
--------------------------------------------------------------------------------
/frontend/src/store/useServiceClientStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { persist, createJSONStorage } from 'zustand/middleware'
3 |
4 | interface ServiceClientState {
5 | serviceSecret: string
6 | setServiceSecret: (secret: string) => void
7 | clearServiceSecret: () => void
8 | }
9 |
10 | export const useServiceClientStore = create()(
11 | persist(
12 | (set) => ({
13 | serviceSecret: '',
14 | setServiceSecret: (secret: string) => {
15 | set({ serviceSecret: (secret || '').trim() })
16 | },
17 | clearServiceSecret: () => {
18 | set({ serviceSecret: '' })
19 | },
20 | }),
21 | {
22 | name: 'service-client-store',
23 | storage: createJSONStorage(() => localStorage),
24 | },
25 | ),
26 | )
27 |
28 |
--------------------------------------------------------------------------------
/tests/sample/stringer.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Feeds from Stringer
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as LabelPrimitive from '@radix-ui/react-label'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 | import { cn } from '@/lib/utils'
5 |
6 | const labelVariants = cva(
7 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
8 | )
9 |
10 | const Label = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef &
13 | VariantProps
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | Label.displayName = LabelPrimitive.Root.displayName
22 |
23 | export { Label }
24 |
25 |
--------------------------------------------------------------------------------
/rssant/templates/email/recall.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Hi {{ username }} 好久不见!
4 |
5 |
是否还记得一年以前,系统刚刚上线时的模样?
6 |
7 |
在这一年多时间里,我一直在不断完善系统的功能和体验,如今系统已焕然一新!
8 |
9 |
10 | 系统依旧是无广告,无推荐,专注阅读的 RSS 阅读器。
11 | 全新界面设计,现已适配桌面端,也支持添加到手机主屏。
12 | 支持全文阅读,图片代理,播客等等,全球 RSS 均可订阅。
13 |
14 |
15 |
如果你需要一款好用的 RSS 阅读器,轻松订阅博客和资讯,欢迎回来看看!
16 |
17 |
18 |
使用中如有任何问题,欢迎随时联系我!
19 |
20 |
guyskk
21 |
系统开发者
22 |
--------------------------------------------------------------------------------
/rssant_common/django_setup.py:
--------------------------------------------------------------------------------
1 | """
2 | https://stackoverflow.com/questions/39704298/how-to-call-django-setup-in-console-script
3 | """
4 | import os
5 | from pathlib import Path
6 | import django
7 | from django.apps import apps
8 | from django.conf import settings
9 | from django.utils.autoreload import autoreload_started
10 |
11 |
12 | _root_dir = Path(__file__).parent.parent
13 |
14 |
15 | def _watch_changelog(sender, **kwargs):
16 | sender.watch_dir(_root_dir / 'docs/changelog', '*')
17 | sender.watch_dir(_root_dir / 'rssant_common/resources', '*')
18 |
19 |
20 | def _django_setup():
21 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rssant.settings')
22 | if not apps.ready and not settings.configured:
23 | django.setup()
24 | autoreload_started.connect(_watch_changelog)
25 |
26 |
27 | _django_setup()
28 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 |
23 | /* Path aliases */
24 | "baseUrl": ".",
25 | "paths": {
26 | "@/*": ["./src/*"]
27 | }
28 | },
29 | "include": ["src"],
30 | "references": [{ "path": "./tsconfig.node.json" }]
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/tests/feedlib/test_response.py:
--------------------------------------------------------------------------------
1 | from rssant_feedlib.response import FeedResponseStatus, FeedResponse, FeedContentType
2 |
3 |
4 | def test_response_status():
5 | status = FeedResponseStatus(-200)
6 | assert status == -200
7 | assert status in (-200, -300)
8 | assert FeedResponseStatus.name_of(200) == 'OK'
9 | assert FeedResponseStatus.name_of(600) == 'HTTP_600'
10 | assert FeedResponseStatus.name_of(-200) == 'FEED_CONNECTION_ERROR'
11 | assert FeedResponseStatus.is_need_proxy(-200)
12 |
13 |
14 | def test_response_repr():
15 | response = FeedResponse(
16 | content=b'123456',
17 | url='https://example.com/feed.xml',
18 | feed_type=FeedContentType.XML,
19 | )
20 | assert repr(response)
21 | response = FeedResponse(
22 | url='https://example.com/feed.xml',
23 | )
24 | assert repr(response)
25 |
--------------------------------------------------------------------------------
/rssant_common/health.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from rssant_common import timezone
4 | from rssant_common.network_helper import LOCAL_IP_LIST
5 |
6 | UPTIME_BEGIN = timezone.now()
7 |
8 |
9 | def _get_uptime(now: timezone.datetime):
10 | uptime_seconds = round((now - UPTIME_BEGIN).total_seconds())
11 | uptime = str(timezone.timedelta(seconds=uptime_seconds))
12 | return uptime
13 |
14 |
15 | def health_info():
16 | build_id = os.getenv("EZFAAS_BUILD_ID")
17 | commit_id = os.getenv("EZFAAS_COMMIT_ID")
18 | now = timezone.now()
19 | uptime = _get_uptime(now)
20 | ip_list = [ip for _, ip in LOCAL_IP_LIST]
21 | result = dict(
22 | build_id=build_id,
23 | commit_id=commit_id,
24 | now=now.isoformat(),
25 | uptime=uptime,
26 | pid=os.getpid(),
27 | ip_list=ip_list,
28 | )
29 | return result
30 |
--------------------------------------------------------------------------------
/rssant_common/signature.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from validr import T
3 |
4 |
5 | def get_params(f):
6 | sig = inspect.signature(f)
7 | params_schema = {}
8 | for name, p in list(sig.parameters.items())[1:]:
9 | if p.default is not inspect.Parameter.empty:
10 | raise ValueError('You should not set default in schema annotation!')
11 | if p.annotation is inspect.Parameter.empty:
12 | raise ValueError(f'Missing annotation in parameter {name}!')
13 | params_schema[name] = p.annotation
14 | if params_schema:
15 | return T.dict(params_schema).__schema__
16 | return None
17 |
18 |
19 | def get_returns(f):
20 | sig = inspect.signature(f)
21 | if sig.return_annotation is not inspect.Signature.empty:
22 | schema = sig.return_annotation
23 | return T(schema).__schema__
24 | return None
25 |
--------------------------------------------------------------------------------
/box/bin/start-postgres.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | /usr/lib/postgresql/11/bin/postgres --version
6 |
7 | if [ ! "$(ls -A /var/lib/postgresql/11/main)" ]; then
8 | echo 'copy postgresql init data to mounted volume'
9 | cp -a /var/lib/postgresql/11/init/. /var/lib/postgresql/11/main
10 | fi
11 |
12 | # the uid:gid will change after upgrade docker image
13 | # volume permission may mismatch and need fix
14 | # eg: when upgrade docker image from debian-9 to debian-10
15 | chown -R postgres:postgres /var/lib/postgresql/11/main
16 | chmod 700 /var/lib/postgresql/11/main
17 | chown -R postgres:postgres /var/log/postgresql
18 | chmod 700 /var/log/postgresql
19 |
20 | rm -f /var/lib/postgresql/11/main/postmaster.pid
21 | su -c "/usr/lib/postgresql/11/bin/postgres \
22 | -D /var/lib/postgresql/11/main \
23 | -c config_file=/etc/postgresql/11/main/postgresql.conf" \
24 | postgres
25 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cn } from '@/lib/utils'
3 |
4 | export interface TextareaProps
5 | extends React.TextareaHTMLAttributes {}
6 |
7 | const Textarea = React.forwardRef(
8 | ({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | }
20 | )
21 | Textarea.displayName = 'Textarea'
22 |
23 | export { Textarea }
24 |
25 |
--------------------------------------------------------------------------------
/rssant_common/requests_helper.py:
--------------------------------------------------------------------------------
1 | from requests import Response
2 | from requests.exceptions import ChunkedEncodingError
3 |
4 |
5 | def requests_check_incomplete_response(response: Response):
6 | """
7 | Check that we have read all the data as the requests library does not
8 | currently enforce this.
9 | https://blog.petrzemek.net/2018/04/22/on-incomplete-http-reads-and-the-requests-library-in-python/
10 | """
11 | expected_length = response.headers.get('Content-Length')
12 | if expected_length is not None:
13 | actual_length = response.raw.tell()
14 | expected_length = int(expected_length)
15 | if actual_length < expected_length:
16 | msg = 'incomplete response ({} bytes read, {} more expected)'.format(
17 | actual_length, expected_length - actual_length)
18 | raise ChunkedEncodingError(msg, response=response)
19 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
3 | import { cn } from '@/lib/utils'
4 |
5 | const Separator = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(
9 | (
10 | { className, orientation = 'horizontal', decorative = true, ...props },
11 | ref
12 | ) => (
13 |
24 | )
25 | )
26 | Separator.displayName = SeparatorPrimitive.Root.displayName
27 |
28 | export { Separator }
29 |
30 |
--------------------------------------------------------------------------------
/rssant_api/views/ezrevenue.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import AbstractUser
2 | from rest_framework.response import Response
3 |
4 | from django_rest_validr import RestRouter, T
5 | from rssant_common.ezrevenue import EZREVENUE_CLIENT
6 |
7 | EzrevenueView = RestRouter()
8 |
9 |
10 | @EzrevenueView.post('ezrevenue/customer.info')
11 | def ezrevenue_customer_info(
12 | request,
13 | include_balance: T.bool.default(True),
14 | ) -> T.dict:
15 | if not EZREVENUE_CLIENT:
16 | return Response(status=501)
17 | user: AbstractUser = request.user
18 | params = dict(
19 | paywall_alias='paywall_vip',
20 | customer=dict(
21 | external_id=user.id,
22 | nickname=user.username,
23 | external_dt_created=user.date_joined.isoformat(),
24 | ),
25 | include_balance=include_balance,
26 | )
27 | return EZREVENUE_CLIENT.call('customer.info', params)
28 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0021_auto_20200418_0512.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.12 on 2020-04-18 05:12
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0020_feed_checksum_data'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='story',
15 | name='audio_url',
16 | field=models.TextField(blank=True, help_text='播客音频链接', null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='story',
20 | name='iframe_url',
21 | field=models.TextField(blank=True, help_text='视频iframe链接', null=True),
22 | ),
23 | migrations.AddField(
24 | model_name='story',
25 | name='image_url',
26 | field=models.TextField(blank=True, help_text='图片链接', null=True),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/tests/models/test_story_data.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import pytest
3 | from rssant_api.models.story_storage import StoryData
4 |
5 |
6 | def test_encode_decode_json():
7 | dt = datetime.datetime(2020, 5, 23, 12, 12, 12, tzinfo=datetime.timezone.utc)
8 | base = {
9 | 'key': 'value',
10 | 'text': '你好',
11 | 'number': 123,
12 | }
13 | value = {**base, 'datetime': dt}
14 | expect = {**base, 'datetime': '2020-05-23T12:12:12.000000Z'}
15 | data = StoryData.encode_json(value)
16 | got = StoryData.decode_json(data)
17 | assert got == expect
18 |
19 |
20 | SAMPLE_TEXT = 'hello world\n你好世界\n'
21 |
22 |
23 | @pytest.mark.parametrize('length', [0, 255, 1024, 16 * 1024, 64 * 1024])
24 | def test_encode_decode_text(length):
25 | text = (SAMPLE_TEXT * (length // len(SAMPLE_TEXT)))[:length]
26 | data = StoryData.encode_text(text)
27 | got = StoryData.decode_text(data)
28 | assert got == text
29 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cn } from '@/lib/utils'
3 |
4 | export interface InputProps
5 | extends React.InputHTMLAttributes {}
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | )
20 | }
21 | )
22 | Input.displayName = 'Input'
23 |
24 | export { Input }
25 |
26 |
--------------------------------------------------------------------------------
/rssant/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for rssant project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
8 | """
9 |
10 | from django.core.wsgi import get_wsgi_application as _get_app
11 |
12 | import rssant_common.django_setup # noqa:F401
13 | from rssant_common.helper import is_main_or_wsgi
14 | from rssant_common.logger import configure_logging
15 | from rssant_config import CONFIG
16 | from rssant_worker.worker_service import WORKER_SERVICE
17 |
18 |
19 | def get_wsgi_application():
20 | configure_logging(level=CONFIG.log_level)
21 | if not CONFIG.is_role_api:
22 | WORKER_SERVICE.start_dns_refresh_thread()
23 | return _get_app()
24 |
25 |
26 | if is_main_or_wsgi(__name__):
27 | print(f'* WSGI application role={CONFIG.role} started')
28 | application = get_wsgi_application()
29 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Toast,
3 | ToastClose,
4 | ToastDescription,
5 | ToastProvider,
6 | ToastTitle,
7 | ToastViewport,
8 | } from '@/components/ui/toast'
9 | import { useToast } from '@/components/ui/use-toast'
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 |
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 |
19 |
20 | {title && {title} }
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 |
28 | )
29 | })}
30 |
31 |
32 | )
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/rssant_common/resources/changelog.atom.mako:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${ title }
4 | % if link:
5 |
6 |
7 | ${ link }/favicon.ico
8 | % endif
9 | ${ format_date(updated) }
10 | % if link:
11 | ${ link }/changelog
12 | % endif
13 | %for item in changelogs:
14 |
15 | ${ item.version }: ${ item.title }
16 | % if link:
17 |
18 | ${ link }/changelog?version=${ item.version }
19 | % endif
20 | ${ format_date(item.date) }
21 |
22 |
23 |
24 |
25 | % endfor
26 |
--------------------------------------------------------------------------------
/box/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 |
4 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5 | RSSANT_IMAGE="${RSSANT_IMAGE:-rssant:custom}"
6 |
7 | for volume in rssant-data rssant-postgres-data rssant-postgres-logs; do
8 | docker volume create "$volume" >/dev/null 2>&1 || true
9 | done
10 |
11 | if docker ps -a --format '{{.Names}}' | grep -q '^rssant$'; then
12 | docker stop -t 3 rssant >/dev/null 2>&1 || true
13 | docker rm -f rssant >/dev/null 2>&1 || true
14 | fi
15 |
16 | # shellcheck disable=SC2068
17 | docker run -ti --name rssant -d \
18 | -p 6789:80 \
19 | --env-file "$SCRIPT_DIR/rssant.env" \
20 | -v rssant-data:/app/data \
21 | -v rssant-postgres-data:/var/lib/postgresql/11/main \
22 | -v rssant-postgres-logs:/var/log/postgresql \
23 | --log-driver json-file --log-opt max-size=50m --log-opt max-file=10 \
24 | --restart unless-stopped \
25 | "$RSSANT_IMAGE" $@
26 |
27 | # docker logs --tail 1000 -f rssant
28 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/http-www-m1927-com-feed-php.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/FeedCardSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardHeader } from '@/components/ui/card'
2 | import { Skeleton } from '@/components/ui/skeleton'
3 |
4 | export function FeedCardSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0051_auto_20251118_1322.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.28 on 2025-11-18 13:22
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0050_create_aigc_report'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='aientertainmentonlyreport',
15 | name='use_tavily',
16 | field=models.BooleanField(default=False, help_text='是否使用Tavily搜索'),
17 | ),
18 | migrations.AlterField(
19 | model_name='aientertainmentreport',
20 | name='use_tavily',
21 | field=models.BooleanField(default=False, help_text='是否使用Tavily搜索'),
22 | ),
23 | migrations.AlterField(
24 | model_name='aigcreport',
25 | name='use_tavily',
26 | field=models.BooleanField(default=False, help_text='是否使用Tavily搜索'),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/fulltext/martinfowler_rss.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
A misleading title to draw readers into an occasionally true story
4 |
5 |
A couple of weeks ago Cindy was woken in the wee hours by sounds of
6 | animals fighting in our garden. As she investigated, she saw two coyotes
7 | run off, leaving our cat's body behind. A state of nature is a state of
8 | violence, and our feline predator was quickly turned into prey. Yet our
9 | garden has high fences all around, making it an unlikely spot for coyotes
10 | to explore. So is there more to that night than a simple act of
11 | nature?
12 |
13 |
more…
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/failed/http-www-chongdiantou-com-feed.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/prompts/hacker_news_daily_report_openai_prompt.txt:
--------------------------------------------------------------------------------
1 | 你是一个关注 Hacker News 的技术专家,擅于洞察技术热点和发展趋势。
2 |
3 | 任务:
4 | 1.你的技术经验分类整理 Hacker News 所有热点话题,
5 | 2.根据话题出现次数,总结今天最热门的 Top 3 技术趋势,并保留原始链接。
6 | 3.报告格式参考下面示例。
7 |
8 | 格式:
9 | # 【Hacker News 前沿技术趋势】
10 |
11 | 时间: {日期}
12 |
13 | ## Top 1:Rust 编程语言引发热门讨论
14 |
15 | 关于 Rust 的多个讨论,尤其是关于小字符串处理和安全垃圾回收技术的文章,显示出 Rust 语言在现代编程中的应用迅速增长,开发者对其性能和安全特性的兴趣不断上升。
16 |
17 | 详细内容见相关链接:
18 | - https://fasterthanli.me/articles/small-strings-in-rust
19 | - https://kyju.org/blog/rust-safe-garbage-collection/
20 |
21 | ### Top 2: Nvidia 在 AI 领域中的强大竞争力
22 |
23 | 有关于 Nvidia 的四个未知客户,每个人购买价值超过 3 亿美元的讨论,显示出 N 维达在 AI 领域中的强大竞争力。
24 |
25 | 详细内容见相关链接:
26 | - https://fortune.com/2024/08/29/nvidia-jensen-huang-ai-customers/
27 |
28 | ### Top 3:Bubbletea 的应用性和可能性
29 |
30 | 有关于构建 Bubbletea 程序的讨论,展示了 Bubbletea 在开发中的应用性和可能性。
31 |
32 | 详细内容见相关链接:
33 | - https://leg100.github.io/en/posts/building-bubbletea-programs/
34 | - https://www.sfchronicle.com/crime/article/tesla-sentry-mode-police-evidence-19731000.php
35 |
36 |
37 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0014_auto_20191027_0558.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2019-10-27 05:58
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0013_auto_20191027_0517'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='imageinfo',
15 | name='referer',
16 | field=models.TextField(blank=True, help_text='the referer used to request sample image', null=True),
17 | ),
18 | migrations.AlterField(
19 | model_name='imageinfo',
20 | name='sample_url',
21 | field=models.TextField(blank=True, help_text='sample image url', null=True),
22 | ),
23 | migrations.AlterField(
24 | model_name='imageinfo',
25 | name='user_agent',
26 | field=models.TextField(blank=True, help_text='the user-agent used to request sample image', null=True),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/scripts/dev/check-services.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # 检查服务状态脚本
3 |
4 | echo "=== RSS订阅服务状态 ==="
5 | echo ""
6 |
7 | # 检查端口
8 | check_port() {
9 | local port=$1
10 | local name=$2
11 | if lsof -i :$port > /dev/null 2>&1; then
12 | echo "✓ $name (端口 $port): 运行中"
13 | lsof -i :$port | grep LISTEN | head -1
14 | else
15 | echo "✗ $name (端口 $port): 未运行"
16 | fi
17 | }
18 |
19 | check_port 6788 "API服务"
20 | check_port 6793 "Worker服务"
21 | check_port 5173 "前端服务"
22 |
23 | echo ""
24 | echo "=== Scheduler 服务 ==="
25 | if pgrep -f "scripts/run-scheduler.py\|run-scheduler.py" > /dev/null; then
26 | echo "✓ Scheduler: 运行中"
27 | pgrep -f "scripts/run-scheduler.py\|run-scheduler.py" | xargs ps -p
28 | else
29 | echo "✗ Scheduler: 未运行"
30 | fi
31 |
32 | echo ""
33 | echo "=== 查看日志 ==="
34 | echo "API: tail -f /tmp/rssant-api.log"
35 | echo "Worker: tail -f /tmp/rssant-worker.log"
36 | echo "Scheduler: tail -f /tmp/rssant-scheduler.log"
37 | echo "Frontend: tail -f /tmp/rssant-frontend.log"
38 |
39 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | exclude: '^(data/|unmaintain/|static/|tests/feedlib/testdata/)'
2 | repos:
3 | - repo: git://github.com/pre-commit/pre-commit-hooks
4 | rev: v2.4.0
5 | hooks:
6 | - id: trailing-whitespace
7 | files: \.(py|pyx|sh|txt|in|ini|json|yaml|yml)$
8 | - id: end-of-file-fixer
9 | files: \.(py|pyx|sh|md|txt|in|ini|json|yaml|yml)$
10 | - id: check-byte-order-marker
11 | files: \.(py|pyx)$
12 | - id: check-case-conflict
13 | - id: check-added-large-files
14 | args:
15 | - '--maxkb=2000'
16 | - id: check-merge-conflict
17 | - id: check-symlinks
18 | - id: check-json
19 | - id: check-yaml
20 | - id: debug-statements
21 | - id: check-docstring-first
22 | files: \.(py|pyx)$
23 | - id: fix-encoding-pragma
24 | files: \.(py|pyx)$
25 | args:
26 | - '--remove'
27 | - id: flake8
28 | exclude: '^rssant_api/migrations/'
29 | - repo: https://github.com/Trim21/find-trailing-comma
30 | rev: v0.0.1
31 | hooks:
32 | - id: find-trailing-comma
33 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0017_auto_20191217_1253.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2019-12-17 12:53
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0016_auto_20191105_1247'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feed',
15 | name='retention_offset',
16 | field=models.IntegerField(blank=True, default=0, help_text='stale story == offset < retention_offset', null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='story',
20 | name='is_user_marked',
21 | field=models.BooleanField(blank=True, default=False, help_text='is user favorited or watched ever', null=True),
22 | ),
23 | migrations.AlterField(
24 | model_name='story',
25 | name='has_mathjax',
26 | field=models.BooleanField(blank=True, default=False, help_text='has MathJax', null=True),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/box/push-all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | VERSION=$1
6 | if [ -z "$VERSION" ]; then
7 | VERSION='latest'
8 | fi
9 |
10 | if [ $VERSION != 'latest' ]; then
11 | echo "*** Push guyskk/rssant:$VERSION-* ***"
12 | docker push guyskk/rssant:$VERSION-arm64
13 | docker push guyskk/rssant:$VERSION-amd64
14 | echo "*** Push manifest guyskk/rssant:$VERSION ***"
15 | docker manifest rm guyskk/rssant:$VERSION || true
16 | docker manifest create guyskk/rssant:$VERSION \
17 | --amend guyskk/rssant:$VERSION-arm64 \
18 | --amend guyskk/rssant:$VERSION-amd64
19 | docker manifest push guyskk/rssant:$VERSION
20 | fi
21 |
22 | echo "*** Push guyskk/rssant:latest-* ***"
23 | docker push guyskk/rssant:latest-arm64
24 | docker push guyskk/rssant:latest-amd64
25 | echo "*** Push manifest guyskk/rssant:latest ***"
26 | docker manifest rm guyskk/rssant:latest || true
27 | docker manifest create guyskk/rssant:latest \
28 | --amend guyskk/rssant:latest-arm64 \
29 | --amend guyskk/rssant:latest-amd64
30 | docker manifest push guyskk/rssant:latest
31 |
--------------------------------------------------------------------------------
/cursor/docs/项目结构说明.md:
--------------------------------------------------------------------------------
1 | # 项目结构说明
2 |
3 | ## 模块划分
4 |
5 | - 后端核心:`rssant/` 项目入口与配置;`rssant_api/` 业务应用与视图;`rssant_scheduler/` 调度服务。
6 | - 前端:`frontend/` React + Vite 应用。
7 | - 容器与部署:`box/`(Dockerfile、supervisord、Nginx、Postgres 初始化)、`deploy/`(构建与部署脚本)。
8 | - 代理与集成:`cloudflare_worker/rssant/` RSS 代理;`feishu/` 飞书脚本。
9 | - 配置与数据:`etc/` OPML 分类源与系统配置;`scripts/` 开发与运维脚本;`tests/` 测试数据与配置。
10 |
11 | ## 代码组织结构与分层
12 |
13 | - 表现层:DRF 视图(`rssant_api/views/*`),前端页面(`frontend/src/pages/*`)。
14 | - 领域层:Django ORM 模型与聚合(`rssant_api/models/*`)。
15 | - 服务层:搜索、报告生成、RSSHub 客户端等(`rssant_api/services/*`)。
16 | - 基础设施层:配置、日志、调度、容器启动与进程管理。
17 |
18 | ## 关键包说明
19 |
20 | - `rssant/settings/settings.py`:Django 设置与中间件、数据库配置、REST 框架(rssant/settings/settings.py:1)。
21 | - `rssant/urls.py`:路由注册与文档入口(rssant/urls.py:33)。
22 | - `rssant_api/urls.py`:按角色注册各业务视图(rssant_api/urls.py:23)。
23 | - `rssant_scheduler/main.py`:调度服务入口(rssant_scheduler/main.py:1)。
24 | - `box/Dockerfile` 与 `box/supervisord.conf`:容器构建与进程编排(box/Dockerfile:1)。
25 |
26 | ## 分层架构说明
27 |
28 | - 视图层调用聚合与服务层完成业务,领域模型负责数据一致性与约束,服务层封装跨模块逻辑(例如全文抓取、AI 报告生成、飞书推送)。
--------------------------------------------------------------------------------
/rssant_api/migrations/0006_auto_20190418_0945.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.7 on 2019-04-18 09:45
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0005_auto_20190416_1616'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveIndex(
14 | model_name='userstory',
15 | name='rssant_api__user_id_c419be_idx',
16 | ),
17 | migrations.AddField(
18 | model_name='userstory',
19 | name='offset',
20 | field=models.IntegerField(default=-1, help_text='Story在Feed中的位置'),
21 | preserve_default=False,
22 | ),
23 | migrations.AddIndex(
24 | model_name='userstory',
25 | index=models.Index(fields=['user_feed', 'offset'], name='rssant_api__user_fe_cfb4de_idx'),
26 | ),
27 | migrations.AddIndex(
28 | model_name='userstory',
29 | index=models.Index(fields=['user', 'story'], name='rssant_api__user_id_3bc826_idx'),
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/cursor/docs/外部依赖说明.md:
--------------------------------------------------------------------------------
1 | # 外部依赖说明
2 |
3 | ## 下游服务与外部系统
4 |
5 | - 数据库:PostgreSQL 11(容器中安装,初始化脚本 `box/initdb.sql`)。
6 | - Web 服务器:Nginx(容器内,静态与前端构建产物托管)。
7 | - Cloudflare Worker(可选):RSS 代理(`cloudflare_worker/rssant/README.md:1`)。
8 | - RSSHub(可选):公共或自部署实例(示例变量见 `box/rssant.env:33`)。
9 | - GitHub OAuth:`allauth.socialaccount` 与配置(`rssant/settings/settings.py:218`)。
10 | - SMTP 邮件(可选):通知与注册邮件(`rssant/settings/settings.py:228`)。
11 | - 飞书开放平台:飞书文档导出与机器人推送(`rssant_api/views/story.py:336`、`rssant_api/views/feishu_bot.py:35`)。
12 |
13 | ## 调用关系图(概览)
14 |
15 | ```mermaid
16 | flowchart TD
17 | FE[前端] --> API[后端API]
18 | API --> DB[(PostgreSQL)]
19 | API --> NG[Nginx静态]
20 | API --> CFW[Cloudflare Worker RSS代理]
21 | API --> RSSHub[RSSHub (可选)]
22 | API --> GH[GitHub OAuth]
23 | API --> SMTP[SMTP (可选)]
24 | API --> Feishu[飞书开放平台]
25 | ```
26 |
27 | ## 配置与环境变量
28 |
29 | - 示例参考:`box/rssant.env:1`。
30 | - 重要变量:`RSSANT_SECRET_KEY`、`RSSANT_ROOT_URL`、`RSSANT_CHECK_FEED_MINUTES`、`RSSANT_PG_*`、`RSSANT_ROLE`、`RSSANT_BIND_ADDRESS`。
31 | - 可选变量:RSS 代理与 RSSHub、SMTP、GitHub OAuth、飞书 App ID/Secret。
32 | - 安全要求:真实密钥与凭据不得提交到仓库。
--------------------------------------------------------------------------------
/unmaintain/benchmark/benchmark_story_data.py:
--------------------------------------------------------------------------------
1 | import os
2 | import random
3 | import timeit
4 | import pyinstrument
5 | import rssant_common.django_setup
6 |
7 | from rssant_api.models.story_storage import StoryData
8 |
9 |
10 | def random_content():
11 | return os.urandom(random.randint(1, 10) * 1024)
12 |
13 |
14 | def main():
15 | p = pyinstrument.Profiler()
16 | p.start()
17 | t_lz4 = timeit.timeit(lambda: StoryData.decode(StoryData(
18 | random_content(), version=StoryData.VERSION_LZ4).encode()), number=10000)
19 | print(t_lz4)
20 | p.stop()
21 | html = p.output_html()
22 | with open('benchmark_story_data_lz4.html', 'w') as f:
23 | f.write(html)
24 |
25 | p = pyinstrument.Profiler()
26 | p.start()
27 | t_gzip = timeit.timeit(lambda: StoryData.decode(StoryData(
28 | random_content(), version=StoryData.VERSION_GZIP).encode()), number=10000)
29 | print(t_gzip)
30 | p.stop()
31 | html = p.output_html()
32 | with open('benchmark_story_data_gzip.html', 'w') as f:
33 | f.write(html)
34 |
35 |
36 | if __name__ == "__main__":
37 | main()
38 |
--------------------------------------------------------------------------------
/rssant_common/attrdict.py:
--------------------------------------------------------------------------------
1 | """
2 | Simplified https://github.com/bcj/AttrDict
3 |
4 | >>> d = AttrDict(x=1)
5 | >>> d.x == 1
6 | True
7 | >>> d.y = 2
8 | >>> d.y == 2
9 | True
10 | >>> del d.y
11 | >>> d.y
12 | Traceback (most recent call last):
13 | ...
14 | AttributeError: 'AttrDict' object has no attribute 'y'
15 | >>> del d.y
16 | Traceback (most recent call last):
17 | ...
18 | AttributeError: 'AttrDict' object has no attribute 'y'
19 | """
20 | from typing import Any
21 |
22 |
23 | class AttrDict(dict):
24 |
25 | def __getattr__(self, name: str) -> Any:
26 | try:
27 | return self[name]
28 | except KeyError:
29 | raise AttributeError(_no_attr_msg(name)) from None
30 |
31 | def __setattr__(self, name: str, value: Any) -> None:
32 | self[name] = value
33 |
34 | def __delattr__(self, name: str) -> None:
35 | try:
36 | del self[name]
37 | except KeyError:
38 | raise AttributeError(_no_attr_msg(name)) from None
39 |
40 |
41 | def _no_attr_msg(name: str) -> str:
42 | return f"{AttrDict.__name__!r} object has no attribute {name!r}"
43 |
--------------------------------------------------------------------------------
/cursor/docs/文章导出到飞书-技术方案设计.md:
--------------------------------------------------------------------------------
1 | # 技术方案设计文档:文章导出到飞书
2 |
3 | ## 文档信息
4 | - 作者:系统生成
5 | - 版本:v1.0
6 | - 日期:2025-11-20
7 | - 状态:已确认
8 | - 架构类型:非GBF框架
9 |
10 | # 一、名词解释
11 | | 术语 | 解释 |
12 | |------|------|
13 | | FeishuService | 调用飞书API创建文档与内容写入的服务 |
14 | | folder_token | 目标文档目录标识(可选) |
15 |
16 | # 二、领域模型
17 | - Story 与其 AI 分析(辅助导出内容组装)。
18 |
19 | # 三、应用调用关系
20 | ```mermaid
21 | flowchart TD
22 | U[用户] --> SV[StoryView]
23 | SV --> FS[FeishuService]
24 | FS --> Feishu[飞书开放平台]
25 | ```
26 |
27 | # 四、详细方案设计
28 | ## 架构选型
29 | - Controller(StoryView)→ Service(FeishuService)。
30 |
31 | ### 分层架构说明
32 | - 视图:`rssant_api/views/story.py:334-382`(导出接口)。
33 | - 服务:`rssant_api/feishu_service.py`(创建与写入文档)。
34 |
35 | ## 接口与设计
36 | - 导出文章到飞书文档:`POST /api/v1/story.export_to_feishu`(`rssant_api/views/story.py:334-382`)
37 | - 参数:文章列表(feed_id+offset)、`folder_token`、`app_id/app_secret`(可覆盖后端配置)。
38 | - 行为:校验配置→创建服务→拉取文章详情→组装内容→创建文档并返回结果列表。
39 |
40 | ## 关键规则
41 | - 配置优先级:前端传入覆盖后端环境变量(`RSSANT_FEISHU_APP_ID/SECRET`)。
42 | - 错误处理:单篇错误不影响整体导出;结果列表中标注失败原因。
43 |
44 | ## 接口改动点
45 | - 当前无协议字段变更;若支持模板化导出,需要新增模板参数并在接口文档中体现。
46 |
47 | ## 数据库变更
48 | - 无;导出行为不持久化,仅依赖内容读取与外部接口调用。
--------------------------------------------------------------------------------
/frontend/dev-update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 前端开发模式快速更新脚本
4 | # 用于快速查看代码修改效果
5 |
6 | echo "=========================================="
7 | echo "前端开发服务器"
8 | echo "=========================================="
9 | echo ""
10 |
11 | cd "$(dirname "$0")"
12 |
13 | # 检查Node.js环境
14 | if ! command -v node &> /dev/null; then
15 | echo "❌ 未找到 Node.js"
16 | echo "请先安装 Node.js 18+"
17 | exit 1
18 | fi
19 |
20 | # 检查是否有开发服务器在运行
21 | if lsof -Pi :5173 -sTCP:LISTEN -t >/dev/null 2>&1; then
22 | echo "✅ 开发服务器已在运行 (端口 5173)"
23 | echo ""
24 | echo "📝 代码修改会自动热更新,刷新浏览器即可看到效果"
25 | echo ""
26 | echo "🌐 访问地址:"
27 | echo " - 本地: http://localhost:5173"
28 | echo " - 远程: http://192.168.191.85:5173"
29 | echo ""
30 | echo "如需重启开发服务器,请先停止当前进程:"
31 | echo " pkill -f vite"
32 | exit 0
33 | fi
34 |
35 | echo "启动前端开发服务器..."
36 | echo ""
37 |
38 | # 加载 nvm 环境
39 | export NVM_DIR="$HOME/.nvm"
40 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
41 |
42 | # 使用 Node.js 18
43 | if command -v nvm &> /dev/null; then
44 | nvm use 18 2>/dev/null || nvm install 18
45 | fi
46 |
47 | # 启动开发服务器
48 | npm run dev
49 |
50 |
--------------------------------------------------------------------------------
/frontend/src/components/PageTransition.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useEffect, useState } from 'react'
2 | import { useLocation } from 'react-router-dom'
3 | import { cn } from '@/lib/utils'
4 |
5 | interface PageTransitionProps {
6 | children: ReactNode
7 | }
8 |
9 | export function PageTransition({ children }: PageTransitionProps) {
10 | const location = useLocation()
11 | const [displayLocation, setDisplayLocation] = useState(location)
12 | const [transitionStage, setTransitionStage] = useState<'entering' | 'entered'>('entered')
13 |
14 | useEffect(() => {
15 | if (location !== displayLocation) {
16 | setTransitionStage('entering')
17 | const timer = setTimeout(() => {
18 | setDisplayLocation(location)
19 | setTransitionStage('entered')
20 | }, 150)
21 | return () => clearTimeout(timer)
22 | }
23 | }, [location, displayLocation])
24 |
25 | return (
26 |
32 | {children}
33 |
34 | )
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/rssant_scheduler/main.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | import click
5 | from aiohttp import web
6 |
7 | from rssant_common.logger import configure_logging
8 | from rssant_config import CONFIG
9 |
10 | from .scheduler import RssantScheduler
11 | from .views import routes
12 |
13 | LOG = logging.getLogger(__name__)
14 |
15 |
16 | def create_app():
17 | api = web.Application()
18 | api.add_routes(routes)
19 | app = web.Application()
20 | app.add_subapp('/api/v1', api)
21 | return app
22 |
23 |
24 | def _get_env_bind_address() -> tuple:
25 | bind = os.getenv('RSSANT_BIND_ADDRESS') or '0.0.0.0:6790'
26 | host, port = bind.split(':')
27 | port = int(port)
28 | return host, port
29 |
30 |
31 | @click.command()
32 | def main():
33 | """Run rssant scheduler."""
34 | configure_logging(level=CONFIG.log_level)
35 | scheduler = RssantScheduler(num_worker=CONFIG.scheduler_num_worker)
36 | scheduler.start()
37 | app = create_app()
38 | host, port = _get_env_bind_address()
39 | web.run_app(app, host=host, port=port, reuse_port=True)
40 |
41 |
42 | if __name__ == "__main__":
43 | main()
44 |
--------------------------------------------------------------------------------
/rssant_asyncapi/views.py:
--------------------------------------------------------------------------------
1 | from aiohttp.web import json_response
2 | from aiohttp.web_request import Request
3 | from validr import T
4 |
5 | from rssant_common.health import health_info
6 | from rssant_common.image_token import ImageToken, ImageTokenDecodeError
7 | from rssant_config import CONFIG
8 |
9 | from .image_proxy import image_proxy
10 | from .rest_validr import ValidrRouteTableDef
11 |
12 | routes = ValidrRouteTableDef()
13 |
14 |
15 | @routes.get('/image/proxy')
16 | async def image_proxy_view_v2(
17 | request: Request,
18 | token: T.str,
19 | url: T.url.maxlen(4096),
20 | ):
21 | try:
22 | image_token = ImageToken.decode(
23 | token, secret=CONFIG.image_token_secret, expires=CONFIG.image_token_expires
24 | )
25 | except ImageTokenDecodeError as ex:
26 | return json_response({'message': str(ex)}, status=400)
27 | response = await image_proxy(request, url, image_token.referrer)
28 | return response
29 |
30 |
31 | @routes.get('/image/_health')
32 | async def get_health(request):
33 | result = health_info()
34 | result.update(role='asyncapi')
35 | return json_response(result)
36 |
--------------------------------------------------------------------------------
/unmaintain/convert_qrcode.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import glob
3 | import os.path
4 | from PIL import Image
5 |
6 |
7 | def convert(filepath: str) -> str:
8 | if '.output' in filepath:
9 | return None
10 | filename, ext = os.path.splitext(filepath)
11 | output = filename + '.output.jpeg'
12 | if 'weixin' in filepath:
13 | crop_size = (270, 420, 270 + 580, 420 + 580)
14 | elif 'alipay' in filepath:
15 | crop_size = (202, 561, 202 + 675, 561 + 675)
16 | else:
17 | return None
18 | img = Image.open(filepath)
19 | if img.size[0] <= 320 or img.size[1] <= 320:
20 | return None
21 | img.crop(crop_size)\
22 | .convert('RGB')\
23 | .resize((320, 320), Image.HAMMING)\
24 | .save(output)
25 | return output
26 |
27 |
28 | def main():
29 | if len(sys.argv) < 2:
30 | print('Usage: convert_qrcode.py ...')
31 | return
32 | for pathname in sys.argv[1:]:
33 | for filepath in glob.glob(pathname):
34 | output = convert(filepath)
35 | if output:
36 | print(output)
37 |
38 |
39 | if __name__ == "__main__":
40 | main()
41 |
--------------------------------------------------------------------------------
/box/build-and-restart.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Docker 容器快速重建和重启脚本
4 | # 用于更新代码后快速看到效果
5 |
6 | set -euo pipefail
7 |
8 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9 | PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
10 |
11 | echo "=========================================="
12 | echo "Docker 容器快速重建"
13 | echo "=========================================="
14 | echo ""
15 |
16 | cd "$PROJECT_ROOT"
17 |
18 | # 检查是否有正在运行的容器
19 | if docker ps --format '{{.Names}}' | grep -q '^rssant$'; then
20 | echo "🛑 停止当前运行的容器..."
21 | docker stop rssant
22 | docker rm rssant
23 | echo "✅ 容器已停止并删除"
24 | echo ""
25 | fi
26 |
27 | # 构建镜像
28 | echo "🔨 开始构建 Docker 镜像..."
29 | echo " 这可能需要几分钟时间..."
30 | echo ""
31 |
32 | cd "$SCRIPT_DIR"
33 | docker build -t rssant:custom -f Dockerfile "$PROJECT_ROOT"
34 |
35 | echo ""
36 | echo "✅ 镜像构建完成"
37 | echo ""
38 |
39 | # 启动容器
40 | echo "🚀 启动新容器..."
41 | bash run.sh
42 |
43 | echo ""
44 | echo "=========================================="
45 | echo "✅ 容器已重新构建并启动"
46 | echo "=========================================="
47 | echo ""
48 | echo "🌐 访问地址: http://192.168.191.85:6789"
49 | echo ""
50 | echo "📝 查看日志: docker logs -f rssant"
51 | echo ""
52 |
53 |
--------------------------------------------------------------------------------
/deploy/rssant_server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8.12-bullseye AS build
2 | WORKDIR /app
3 | ARG PYPI_MIRROR="https://mirrors.aliyun.com/pypi/simple/"
4 | ENV PIP_INDEX_URL=$PYPI_MIRROR PIP_DISABLE_PIP_VERSION_CHECK=1
5 | RUN python -m venv .venv
6 | ENV PATH=/app/.venv/bin:$PATH
7 | COPY requirements-pip.txt .
8 | RUN --mount=type=cache,target=/root/.cache/pip \
9 | pip install -r requirements-pip.txt
10 | COPY constraint.txt .
11 | ENV PIP_CONSTRAINT=/app/constraint.txt
12 | COPY requirements.txt .
13 | RUN --mount=type=cache,target=/root/.cache/pip \
14 | pip install -r requirements.txt
15 |
16 | FROM python:3.8.12-slim-bullseye AS runtime
17 | WORKDIR /app
18 | # Fix DNS pollution of local network
19 | COPY etc/resolv.conf /etc/resolv.conf
20 | ARG PYPI_MIRROR="https://mirrors.aliyun.com/pypi/simple/"
21 | ENV PIP_INDEX_URL=$PYPI_MIRROR PIP_DISABLE_PIP_VERSION_CHECK=1
22 | ENV PATH=/app/.venv/bin:$PATH
23 | COPY --from=build /app/.venv /app/.venv
24 | COPY . /app
25 | ARG EZFAAS_BUILD_ID=''
26 | ARG EZFAAS_COMMIT_ID=''
27 | ENV EZFAAS_BUILD_ID=${EZFAAS_BUILD_ID} EZFAAS_COMMIT_ID=${EZFAAS_COMMIT_ID}
28 | RUN python manage.py collectstatic
29 |
30 | FROM runtime
31 | CMD [ "/app/runserver.py" ]
32 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
3 | import { Check } from 'lucide-react'
4 | import { cn } from '@/lib/utils'
5 |
6 | const Checkbox = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
21 |
22 |
23 |
24 | ))
25 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
26 |
27 | export { Checkbox }
28 |
29 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/well/v2ex-jsonfeed.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "JSON Feed",
4 | "description": "\u7c7b\u4f3c RSS \u7684\u7ad9\u70b9\u5185\u5bb9\u4fe1\u606f\u6d41 JSON \u683c\u5f0f\u3002\u8fd9\u91cc\u8ba8\u8bba JSON Feed \u7684\u5b9e\u73b0\u53ca\u9605\u8bfb\u5668\u652f\u6301\u3002",
5 | "home_page_url": "https://www.v2ex.com/go/jsonfeed",
6 | "feed_url": "https://www.v2ex.com/feed/jsonfeed.json",
7 | "icon": "https://cdn.v2ex.com/navatar/db57/6a7d/1054_large.png?m=1567059308",
8 | "favicon": "https://cdn.v2ex.com/navatar/db57/6a7d/1054_normal.png?m=1567059308",
9 | "items": [
10 | {
11 | "author": {
12 | "url": "https://www.v2ex.com/member/DEVN",
13 | "name": "DEVN",
14 | "avatar": "https://cdn.v2ex.com/avatar/6fc1/cc76/467193_large.png?m=1583239760"
15 | },
16 | "url": "https://www.v2ex.com/t/641290",
17 | "title": "\u4ec0\u4e48\u63d2\u4ef6\u53ef\u4ee5\u505a\u5230 JSON \u683c\u5f0f\u5316/\u538b\u7f29/\u8fd8\u539f/\u8f6c 2JZ \u7684\uff1f",
18 | "id": "https://www.v2ex.com/t/641290",
19 | "date_published": "2020-01-31T15:05:16+00:00",
20 | "content_html": ""
21 | }
22 | ]
23 | }
--------------------------------------------------------------------------------
/prompts/hacker_news_hours_topic_openai_prompt.txt:
--------------------------------------------------------------------------------
1 | 你是一个关注 Hacker News 的技术专家,擅于洞察技术热点和发展趋势。
2 |
3 | 任务:
4 | 1.根据你收到的 Hacker News Top List,分析和总结当前技术圈讨论的热点话题。
5 | 2.使用中文生成报告,内容仅包含5个热点话题,并保留原始链接。
6 |
7 | 格式:
8 | # Hacker News 热门话题 {日期} {小时}
9 |
10 | 1. **Rust 编程语言的讨论**:关于 Rust 的多个讨论,尤其是关于小字符串处理和安全垃圾回收技术的文章,显示出 Rust 语言在现代编程中的应用迅速增长,开发者对其性能和安全特性的兴趣不断上升。
11 | - https://fasterthanli.me/articles/small-strings-in-rust
12 | - https://kyju.org/blog/rust-safe-garbage-collection/
13 |
14 | 2. **网络安全思考**:有关于"防守者和攻击者思考方式"的讨论引发了对网络安全策略的深入思考。这种对比强调防守与攻击之间的心理与技术差异,表明网络安全领域对攻击者策略的关注日益增加。
15 | - https://github.com/JohnLaTwC/Shared/blob/master/Defenders%20think%20in%20lists.%20Attackers%20think%20in%20graphs.%20As%20long%20as%20this%20is%20true%2C%20attackers%20win.md
16 |
17 | 3. **Linux 开发者的理由**:关于 Linux 的讨论,强调了 Linux 在现代开发中的重要性和应用性。
18 | - https://opiero.medium.com/why-you-should-learn-linux-9ceace168e5c
19 |
20 | 4. **Nvidia 的秘密客户**:有关于 Nvidia 的四个未知客户,每个人购买价值超过 3 亿美元的讨论,显示出 N 维达在 AI 领域中的强大竞争力。
21 | - https://fortune.com/2024/08/29/nvidia-jensen-huang-ai-customers/
22 |
23 | 5. **Building Bubbletea Programs**:有关于构建 Bubbletea 程序的讨论,展示了 Bubbletea 在开发中的应用性和可能性。
24 | - https://leg100.github.io/en/posts/building-bubbletea-programs/
25 |
26 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0012_auto_20191025_1526.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2019-10-25 15:26
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0011_auto_20190714_0550'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='feed',
15 | name='monthly_story_count_data',
16 | field=models.BinaryField(blank=True, help_text='monthly story count data', max_length=514, null=True),
17 | ),
18 | migrations.AlterField(
19 | model_name='feed',
20 | name='status',
21 | field=models.CharField(choices=[('pending', 'pending'), ('updating', 'updating'), ('ready', 'ready'), ('error', 'error'), ('discard', 'discard')], default='pending', help_text='状态', max_length=20),
22 | ),
23 | migrations.AlterField(
24 | model_name='feedcreation',
25 | name='status',
26 | field=models.CharField(choices=[('pending', 'pending'), ('updating', 'updating'), ('ready', 'ready'), ('error', 'error'), ('discard', 'discard')], default='pending', help_text='状态', max_length=20),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/well/v2ex-jsonfeed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | JSON Feed
4 | way to explore
5 |
6 | https:https://cdn.v2ex.com/navatar/db57/6a7d/1054_normal.png?m=1567059308
7 |
8 |
9 | https:https://cdn.v2ex.com/navatar/db57/6a7d/1054_large.png?m=1567059308
10 |
11 |
12 |
13 | https://www.v2ex.com/
14 |
15 | 2020-01-31T11:05:16Z
16 |
17 | Copyright © 2010-2018, V2EX
18 |
19 | 什么插件可以做到 JSON 格式化/压缩/还原/转 2JZ 的?
20 |
21 | tag:www.v2ex.com,2020-01-31:/t/641290
22 | 2020-01-31T15:05:16Z
23 | 2020-01-31T11:05:16Z
24 |
25 | DEVN
26 | https://www.v2ex.com/member/DEVN
27 |
28 |
31 |
32 |
--------------------------------------------------------------------------------
/scripts/django_pre_migrate.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.db import connection
4 |
5 |
6 | LOG = logging.getLogger(__name__)
7 |
8 |
9 | def fix_django_migrations_id_seq():
10 | """
11 | Postgres django_migrations_id_seq may out of sync after pg_store, which cause migrate failed:
12 | django.db.utils.IntegrityError: duplicate key value violates unique constraint "django_migrations_pkey"
13 | """
14 | # https://stackoverflow.com/questions/4448340/postgresql-duplicate-key-violates-unique-constraint
15 | # https://stackoverflow.com/questions/244243/how-to-reset-postgres-primary-key-sequence-when-it-falls-out-of-sync
16 | # https://www.calazan.com/how-to-reset-the-primary-key-sequence-in-postgresql-with-django/
17 | table_names = connection.introspection.table_names()
18 | table = "django_migrations"
19 | if table not in table_names:
20 | return
21 | sql = """
22 | BEGIN;
23 | SELECT setval(pg_get_serial_sequence('"{table}"','id'),
24 | coalesce(max("id"), 1), max("id") IS NOT null) FROM "{table}";
25 | COMMIT;
26 | """.format(table=table)
27 | with connection.cursor() as cursor:
28 | cursor.execute(sql)
29 |
30 |
31 | def run():
32 | fix_django_migrations_id_seq()
33 |
--------------------------------------------------------------------------------
/rssant_feedlib/helper.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | RE_URL = re.compile(
5 | r"https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&//=]*)" # noqa
6 | , re.I) # noqa
7 |
8 |
9 | class LXMLError(Exception):
10 | """Wrapper for lxml error"""
11 |
12 |
13 | def lxml_call(f, text: str, *args, **kwargs):
14 | '''
15 | Fix ValueError: Unicode strings with encoding declaration are not supported.
16 | Please use bytes input or XML fragments without declaration.
17 | See also: https://stackoverflow.com/questions/15830421/xml-unicode-strings-with-encoding-declaration-are-not-supported
18 | https://lxml.de/parsing.html
19 | ''' # noqa: E501
20 | try:
21 | text = text.strip()
22 | try:
23 | r = f(text, *args, **kwargs)
24 | except ValueError as ex:
25 | is_unicode_error = ex.args and 'encoding declaration' in ex.args[0]
26 | if not is_unicode_error:
27 | raise
28 | r = f(text.encode('utf-8'), *args, **kwargs)
29 | if isinstance(r, bytes):
30 | r = r.decode('utf-8')
31 | except Exception as ex: # lxml will raise too many errors
32 | raise LXMLError(ex) from ex
33 | return r
34 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as SwitchPrimitives from '@radix-ui/react-switch'
3 | import { cn } from '@/lib/utils'
4 |
5 | const Switch = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 |
22 |
23 | ))
24 | Switch.displayName = SwitchPrimitives.Root.displayName
25 |
26 | export { Switch }
27 |
28 |
--------------------------------------------------------------------------------
/runserver.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 |
4 |
5 | def scheduler_main():
6 | from rssant_scheduler.main import main
7 |
8 | main()
9 |
10 |
11 | def asyncapi_main():
12 | from rssant_asyncapi.main import main
13 |
14 | main()
15 |
16 |
17 | def gunicorn_main():
18 | bind_address = os.getenv('RSSANT_BIND_ADDRESS') or '0.0.0.0:9000'
19 | num_workers = int(os.getenv('RSSANT_NUM_WORKERS') or 1)
20 | num_threads = int(os.getenv('RSSANT_NUM_THREADS') or 50)
21 | gunicorn_argv = [
22 | 'gunicorn',
23 | '-b',
24 | bind_address,
25 | f'--workers={num_workers}',
26 | f'--threads={num_threads}',
27 | '--forwarded-allow-ips=*',
28 | '--reuse-port',
29 | '--timeout=300',
30 | '--keep-alive=7200',
31 | '--access-logfile=-',
32 | '--error-logfile=-',
33 | '--log-level=info',
34 | 'rssant.wsgi',
35 | ]
36 | os.execvp('gunicorn', gunicorn_argv)
37 |
38 |
39 | def main():
40 | role = os.getenv('RSSANT_ROLE')
41 | if role == 'scheduler':
42 | scheduler_main()
43 | elif role == 'asyncapi':
44 | asyncapi_main()
45 | else:
46 | gunicorn_main()
47 |
48 |
49 | if __name__ == '__main__':
50 | main()
51 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 | import { cn } from '@/lib/utils'
4 |
5 | const badgeVariants = cva(
6 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7 | {
8 | variants: {
9 | variant: {
10 | default:
11 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
12 | secondary:
13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
14 | destructive:
15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
16 | outline: 'text-foreground',
17 | },
18 | },
19 | defaultVariants: {
20 | variant: 'default',
21 | },
22 | }
23 | )
24 |
25 | export interface BadgeProps
26 | extends React.HTMLAttributes,
27 | VariantProps {}
28 |
29 | function Badge({ className, variant, ...props }: BadgeProps) {
30 | return (
31 |
32 | )
33 | }
34 |
35 | export { Badge, badgeVariants }
36 |
37 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0048_add_separate_ai_entertainment_reports.py:
--------------------------------------------------------------------------------
1 | # Generated manually
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0047_add_ai_entertainment_refs'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='aientertainmentreport',
15 | name='ai_entertainment_report_content',
16 | field=models.TextField(blank=True, help_text='AI影视报告内容(Markdown格式)', null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='aientertainmentreport',
20 | name='aigc_report_content',
21 | field=models.TextField(blank=True, help_text='AIGC报告内容(Markdown格式)', null=True),
22 | ),
23 | migrations.AddField(
24 | model_name='aientertainmentreport',
25 | name='ai_entertainment_report_file_path',
26 | field=models.CharField(blank=True, help_text='AI影视报告文件路径', max_length=500, null=True),
27 | ),
28 | migrations.AddField(
29 | model_name='aientertainmentreport',
30 | name='aigc_report_file_path',
31 | field=models.CharField(blank=True, help_text='AIGC报告文件路径', max_length=500, null=True),
32 | ),
33 | ]
34 |
35 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0009_auto_20190518_0659.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.7 on 2019-05-18 06:59
2 |
3 | from django.db import migrations
4 | import ool
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('rssant_api', '0008_auto_20190429_1550'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='feed',
16 | name='_version',
17 | field=ool.VersionField(default=0),
18 | ),
19 | migrations.AddField(
20 | model_name='feedurlmap',
21 | name='_version',
22 | field=ool.VersionField(default=0),
23 | ),
24 | migrations.AddField(
25 | model_name='rawfeed',
26 | name='_version',
27 | field=ool.VersionField(default=0),
28 | ),
29 | migrations.AddField(
30 | model_name='story',
31 | name='_version',
32 | field=ool.VersionField(default=0),
33 | ),
34 | migrations.AddField(
35 | model_name='userfeed',
36 | name='_version',
37 | field=ool.VersionField(default=0),
38 | ),
39 | migrations.AddField(
40 | model_name='userstory',
41 | name='_version',
42 | field=ool.VersionField(default=0),
43 | ),
44 | ]
45 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0026_feedstorystat.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.12 on 2020-05-23 14:00
2 |
3 | from django.db import migrations, models
4 | import ool
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('rssant_api', '0025_auto_20200516_0959'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='FeedStoryStat',
16 | fields=[
17 | ('_version', ool.VersionField(default=0)),
18 | ('_created', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
19 | ('_updated', models.DateTimeField(auto_now=True, help_text='更新时间')),
20 | ('id', models.PositiveIntegerField(help_text='feed id', primary_key=True, serialize=False)),
21 | ('monthly_story_count_data', models.BinaryField(blank=True, help_text='monthly story count data', max_length=514, null=True)),
22 | ('checksum_data', models.BinaryField(blank=True, help_text='feed checksum data', max_length=4096, null=True)),
23 | ('unique_ids_data', models.BinaryField(blank=True, help_text='unique ids data', max_length=102400, null=True)),
24 | ],
25 | options={
26 | 'abstract': False,
27 | },
28 | bases=(ool.VersionedMixin, models.Model),
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/cursor/docs/GitHub-技术方案设计.md:
--------------------------------------------------------------------------------
1 | # 技术方案设计文档:GitHub 功能
2 |
3 | ## 文档信息
4 | - 作者:系统生成
5 | - 版本:v1.0
6 | - 日期:2025-11-20
7 | - 状态:已确认
8 | - 架构类型:非GBF框架
9 |
10 | # 一、名词解释
11 | | 术语 | 解释 |
12 | |------|------|
13 | | GitHubSubscription | 用户订阅仓库与频率配置 |
14 | | GitHubProgress | 每日进度(提交/Issue/PR等统计) |
15 | | GitHubReport | 基于进度生成的报告(文本与文件) |
16 |
17 | # 二、领域模型
18 | - `GitHubSubscription/GitHubProgress/GitHubReport`(`rssant_api/models/__init__.py:3-15`)。
19 |
20 | # 三、应用调用关系
21 | ```mermaid
22 | flowchart TD
23 | U[用户] --> GHV[GitHubView]
24 | GHV --> GHClient[GitHub 客户端]
25 | GHV --> ReportGen[报告生成]
26 | GHV --> Notifier[通知]
27 | GHV --> DB[(Postgres)]
28 | ```
29 |
30 | # 四、详细方案设计
31 | ## 架构选型
32 | - Controller(GitHubView)→ Service(客户端/报告生成/发现/通知)→ Repository(ORM)。
33 |
34 | ### 分层架构说明
35 | - 视图:`rssant_api/views/github.py:1`。
36 | - 报告生成:`github.report.generate`(`rssant_api/views/github.py:374-504,486`)。
37 |
38 | ## 典型接口
39 | - 生成报告:`POST /api/v1/github.report.generate`(`rssant_api/views/github.py:374-504`)。
40 | - 报告列表:`POST /api/v1/github.report.list`(`rssant_api/views/github.py:516-567`)。
41 | - 订阅与发现:参考 `GitHubDiscoveryService` 等服务。
42 |
43 | ## 关键规则
44 | - 当指定日期无进度,尝试回溯近7天数据(`rssant_api/views/github.py:394-412`)。
45 | - 报告模型标注 `model_name/model_version`,便于溯源。
46 |
47 | ## 接口改动点
48 | - 当前无协议变更;如支持“企业版GitHub”,需扩展认证与数据源选择参数。
49 |
50 | ## 数据库变更
51 | - 无;如支持更精细统计,需要在 `GitHubProgress` 增加维度字段。
--------------------------------------------------------------------------------
/tests/feedlib/testdata/parser/warn/v2ex-jsonfeed-warning.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "JSON Feed",
4 | "description": "\u7c7b\u4f3c RSS \u7684\u7ad9\u70b9\u5185\u5bb9\u4fe1\u606f\u6d41 JSON \u683c\u5f0f\u3002\u8fd9\u91cc\u8ba8\u8bba JSON Feed \u7684\u5b9e\u73b0\u53ca\u9605\u8bfb\u5668\u652f\u6301\u3002",
5 | "home_page_url": "https://www.v2ex.com/go/jsonfeed",
6 | "feed_url": "https://www.v2ex.com/feed/jsonfeed.json",
7 | "icon": "https://cdn.v2ex.com/navatar/db57/6a7d/1054_large.png?m=1567059308",
8 | "favicon": "https://cdn.v2ex.com/navatar/db57/6a7d/1054_normal.png?m=1567059308",
9 | "items": [
10 | {
11 | "id": "",
12 | "title": "",
13 | "date_published": "2020-01-31T15:05:16+00:00",
14 | "content_html": "2020-01-31"
15 | },
16 | {
17 | "author": {
18 | "url": "https://www.v2ex.com/member/DEVN",
19 | "name": "DEVN",
20 | "avatar": "https://cdn.v2ex.com/avatar/6fc1/cc76/467193_large.png?m=1583239760"
21 | },
22 | "url": "https://www.v2ex.com/t/641290",
23 | "title": "\u4ec0\u4e48\u63d2\u4ef6\u53ef\u4ee5\u505a\u5230 JSON \u683c\u5f0f\u5316/\u538b\u7f29/\u8fd8\u539f/\u8f6c 2JZ \u7684\uff1f",
24 | "id": "https://www.v2ex.com/t/641290",
25 | "date_published": "2020-01-31T15:05:16+00:00",
26 | "content_html": ""
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/scripts/migrate_story_v0_1_0.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import tqdm
4 | from django.db import transaction, connection
5 |
6 | from rssant.helper.content_hash import compute_hash_base64
7 | from rssant_api.models import Feed, Story
8 |
9 |
10 | LOG = logging.getLogger(__name__)
11 |
12 |
13 | def query_old_storys_by_feed(feed_id):
14 | sql = """
15 | SELECT unique_id, title, link, author, dt_published, dt_updated, summary, content
16 | FROM rssant_api_story_bak
17 | WHERE feed_id=%s
18 | """
19 | fields = ['unique_id', 'title', 'link', 'author',
20 | 'dt_published', 'dt_updated', 'summary', 'content']
21 | storys = []
22 | with connection.cursor() as cursor:
23 | cursor.execute(sql, [feed_id])
24 | for row in cursor.fetchall():
25 | story = dict(zip(fields, row))
26 | story['content_hash_base64'] = compute_hash_base64(
27 | story['content'], story['summary'], story['title'])
28 | storys.append(story)
29 | return storys
30 |
31 |
32 | def run():
33 | with transaction.atomic():
34 | feed_ids = [feed.id for feed in Feed.objects.only('id').all()]
35 | LOG.info('total %s feeds', len(feed_ids))
36 | for feed_id in tqdm.tqdm(feed_ids, ncols=80, ascii=True):
37 | storys = query_old_storys_by_feed(feed_id)
38 | Story.bulk_save_by_feed(feed_id, storys)
39 |
--------------------------------------------------------------------------------
/cursor/docs/调度与任务-技术方案设计.md:
--------------------------------------------------------------------------------
1 | # 技术方案设计文档:调度与任务
2 |
3 | ## 文档信息
4 | - 作者:系统生成
5 | - 版本:v1.0
6 | - 日期:2025-11-20
7 | - 状态:已确认
8 | - 架构类型:非GBF框架
9 |
10 | # 一、名词解释
11 | | 术语 | 解释 |
12 | |------|------|
13 | | Scheduler | 调度服务(aiohttp),周期性生成待处理任务与重试 |
14 | | WorkerTask | 任务实体(api/key/data/priority/expired_seconds) |
15 | | FIND_FEED/SYNC_FEED/FETCH_STORY | 订阅发现/同步/全文抓取任务类型 |
16 |
17 | # 二、领域模型
18 | - `WorkerTask`(`rssant_api/models/worker_task.py`)。
19 |
20 | # 三、应用调用关系
21 | ```mermaid
22 | flowchart TD
23 | SCH[Scheduler] --> HB[HarborService]
24 | HB --> DB[(Postgres)]
25 | HB --> WK[WorkerService]
26 | WK --> HB
27 | HB --> DB
28 | ```
29 |
30 | # 四、详细方案设计
31 | ## 架构选型
32 | - Scheduler(生成任务)→ Harbor(派发与持久化)→ Worker(执行)。
33 |
34 | ### 分层架构说明
35 | - 调度入口:`rssant_scheduler/main.py`(aiohttp 服务与对外 API)。
36 | - 任务服务:`rssant_harbor/task_service.py:33-47,49-74,76-95`(获取/生成/重试与批量保存)。
37 |
38 | ## 任务策略
39 | - 同步任务:根据 `CONFIG.check_feed_minutes` 选取过期 `Feed`(`rssant_harbor/task_service.py:49`)。
40 | - 发现任务:对 `FeedCreation` 的 `PENDING/UPDATING` 超时进行重试(`rssant_harbor/task_service.py:95`)。
41 | - 过期清理:定期清理过期任务(`rssant_harbor/harbor_service.py:368`)。
42 |
43 | ## 接口改动点
44 | - 内部服务:`worker_rss.find_feed/sync_feed/fetch_story`(`rssant_worker/view.py:13,26,51`)。
45 | - Harbor 接口:`harbor_rss.update_feed/save_feed_creation_result/update_feed_info`(`rssant_harbor/view.py:181,195,214`)。
46 |
47 | ## 数据库变更
48 | - 无;如需“任务幂等性增强”,可引入状态机字段与唯一键策略(api+key)。
--------------------------------------------------------------------------------
/cursor/docs/HackerNews-技术方案设计.md:
--------------------------------------------------------------------------------
1 | # 技术方案设计文档:Hacker News 功能
2 |
3 | ## 文档信息
4 | - 作者:系统生成
5 | - 版本:v1.0
6 | - 日期:2025-11-20
7 | - 状态:已确认
8 | - 架构类型:非GBF框架
9 |
10 | # 一、名词解释
11 | | 术语 | 解释 |
12 | |------|------|
13 | | HackerNewsStory | HN 文章实体(标题、URL、积分、评论等) |
14 | | AIAnalysis/Report | AI 分析与报告实体 |
15 | | Insight | 讨论精华提取功能 |
16 |
17 | # 二、领域模型
18 | - `HackerNewsStory`、`HackerNewsAIAnalysis`、`HackerNewsAIAnalysisSummary`、`HackerNewsReport`、`HackerNewsDiscussionInsight`(`rssant_api/models/__init__.py:6-18`)。
19 |
20 | # 三、应用调用关系
21 | ```mermaid
22 | flowchart TD
23 | U[用户] --> HNV[HackerNewsView]
24 | HNV --> HNClient[HackerNewsClient]
25 | HNV --> AI[AI_SERVICE]
26 | HNV --> DB[(Postgres)]
27 | HNV --> Feishu[FeishuService]
28 | ```
29 |
30 | # 四、详细方案设计
31 | ## 架构选型
32 | - Controller(HackerNewsView)→ Service(客户端/分类器/报告生成/精华提取)→ Repository(ORM)。
33 |
34 | ### 分层架构说明
35 | - 视图:`rssant_api/views/hacker_news.py:1` 提供查询、分析、报告与导出接口。
36 | - 服务:`HackerNewsClient/HN_CLASSIFIER/HN_REPORT_GENERATOR/HN_INSIGHT_EXTRACTOR`。
37 |
38 | ## 典型接口
39 | - 列表/详情/分析总结/报告等接口(参考 `rssant_api/views/hacker_news.py` 各段)。
40 | - 导出到飞书:`POST /api/v1/hacker_news.story.export_to_feishu`(`rssant_api/views/hacker_news.py:1171-1243`)。
41 |
42 | ## 关键规则
43 | - AI 分析统一 Schema 与跨源聚合(见 `rssant_api/views/story.py:808-935` 的统一列表接口)。
44 | - 导出文档内容可拼接 AI 分析摘要与要点。
45 |
46 | ## 接口改动点
47 | - 当前不调整协议;若支持按话题聚合,需要扩展查询过滤与报告生成参数。
48 |
49 | ## 数据库变更
50 | - 无新增字段;如引入“话题聚类”,需增加话题表与关联。
--------------------------------------------------------------------------------
/cursor/docs/业务流程说明.md:
--------------------------------------------------------------------------------
1 | # 业务流程说明
2 |
3 | ## 全量设为已读流程
4 |
5 | ```mermaid
6 | flowchart TD
7 | A[触发: feed.set_all_readed] --> B[获取用户所有订阅]
8 | B --> C{存在未读文章?}
9 | C -- 否 --> D[结束]
10 | C -- 是 --> E[更新UserFeed.story_offset到total_storys]
11 | E --> F[查询未读offset范围]
12 | F --> G[批量查找已存在的UserStory]
13 | G --> H{存在未标记记录?}
14 | H -- 是 --> I[批量更新: is_watched=true, dt_watched=now]
15 | H -- 否 --> J[准备需要创建的UserStory列表]
16 | I --> K[事务提交]
17 | J --> K[事务提交]
18 | K --> L[返回更新结果]
19 | ```
20 |
21 | - 实现位置:`rssant_api/models/union_feed.py:342`。
22 |
23 | ## 文章导出到飞书文档流程
24 |
25 | ```mermaid
26 | flowchart TD
27 | A[触发: story.export_to_feishu] --> B[检查配置: app_id/app_secret]
28 | B -->|缺失| C[返回错误]
29 | B -->|齐备| D[创建FeishuService实例]
30 | D --> E[权限校验: feed_unionids]
31 | E --> F[遍历文章: 获取详情]
32 | F --> G{文章存在?}
33 | G -->|否| H[记录警告并跳过]
34 | G -->|是| I[调用导出接口创建文档]
35 | I --> J[收集结果: 文档ID/URL/标题]
36 | J --> K[汇总并返回]
37 | ```
38 |
39 | - 实现位置:`rssant_api/views/story.py:336`。
40 |
41 | ## 订阅抓取与更新流程(简化)
42 |
43 | ```mermaid
44 | flowchart TD
45 | A[定时触发] --> B[查询过期订阅: take_outdated_feeds]
46 | B --> C[批量更新为pending并返回抓取参数]
47 | C --> D[worker抓取RSS]
48 | D --> E{成功?}
49 | E -- 否 --> F[状态error, 记录响应]
50 | E -- 是 --> G[更新Feed与Story集合]
51 | G --> H[刷新统计与时间字段]
52 | H --> I[结束]
53 | ```
54 |
55 | - 实现位置:`rssant_api/models/feed.py:246`、`rssant_api/models/story.py:253`。
--------------------------------------------------------------------------------
/cursor/docs/RSSHub集成-技术方案设计.md:
--------------------------------------------------------------------------------
1 | # 技术方案设计文档:RSSHub 集成
2 |
3 | ## 文档信息
4 | - 作者:系统生成
5 | - 版本:v1.0
6 | - 日期:2025-11-20
7 | - 状态:已确认
8 | - 架构类型:非GBF框架
9 |
10 | # 一、名词解释
11 | | 术语 | 解释 |
12 | |------|------|
13 | | RSSHub | 将网站行为转换为可订阅路由的服务 |
14 | | 路由模板 | 如 `/telegram/channel/:username`,需要参数替换 |
15 |
16 | # 二、领域模型
17 | - RSSHubClient:路由解析/生成/有效性测试(`rssant_api/services/rsshub_client.py:77,99`)。
18 |
19 | # 三、应用调用关系
20 | ```mermaid
21 | flowchart TD
22 | U[用户] --> API[FeedView]
23 | API --> RH[RSSHubClient]
24 | API --> UF[UnionFeed]
25 | UF --> DB[(Postgres)]
26 | ```
27 |
28 | # 四、详细方案设计
29 | ## 架构选型
30 | - Controller(FeedView)→ Service(RSSHubClient)→ 订阅创建逻辑。
31 |
32 | ### 分层架构说明
33 | - 视图:`rssant_api/views/rsshub.py:1` 注册路由接口;`rssant_api/views/feed.py:413-475` 在导入流程中集成 RSSHub 检查与生成。
34 | - 服务:`get_rsshub_client().generate_feed_url/test_feed_url`。
35 |
36 | ## 接口与设计
37 | - 获取路由:`GET /api/v1/rsshub.routes`(`rssant_api/views/rsshub.py:14-27`)
38 | - 生成订阅:`POST /api/v1/rsshub.generate`(`rssant_api/views/rsshub.py:42-72`)
39 | - 导入融合:`feed.import` 在 `_create_feeds_by_imports` 中尝试替换为 RSSHub 订阅(`rssant_api/views/feed.py:413-475`)。
40 |
41 | ## 关键规则
42 | - 仅在启用时生效:`client.is_enabled()`;未启用返回 503。
43 | - 参数替换:模板中 `:param` 与 `params` 一致(`rssant_api/services/rsshub_client.py:77-99`)。
44 | - 有效性测试:`test_feed_url` 确保替换后订阅可用再使用。
45 |
46 | ## 接口改动点
47 | - 无外部协议字段变更;如后续支持批量生成,需在 `feed.import` 返回中标注转换来源。
48 |
49 | ## 数据库变更
50 | - 无;实际效果通过 `FeedUrlMap` 与 `FeedCreation` 侧体现。
--------------------------------------------------------------------------------
/rssant_common/_proxy_helper.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from rssant_config import CONFIG
4 |
5 | from .blacklist import compile_url_blacklist
6 | from .ezproxy import EZPROXY_SERVICE
7 |
8 | USE_PROXY_URL_LIST = '''
9 | tandfonline.com
10 | sagepub.com
11 | cnki.net
12 | '''
13 |
14 | _is_use_proxy_url = compile_url_blacklist(USE_PROXY_URL_LIST)
15 |
16 |
17 | def is_use_proxy_url(url):
18 | """
19 | >>> is_use_proxy_url('https://navi.cnki.net/knavi/rss/SHXJ')
20 | True
21 | """
22 | return _is_use_proxy_url(url)
23 |
24 |
25 | def choice_proxy(*, proxy_url, rss_proxy_url) -> bool:
26 | if proxy_url and rss_proxy_url:
27 | use_rss_proxy = random.random() > 0.67
28 | else:
29 | use_rss_proxy = bool(rss_proxy_url)
30 | return use_rss_proxy
31 |
32 |
33 | def get_proxy_options(url: str = None) -> dict:
34 | options = {}
35 | if CONFIG.proxy_enable:
36 | options.update(proxy_url=CONFIG.proxy_url)
37 | elif CONFIG.ezproxy_enable:
38 | proxy_url = EZPROXY_SERVICE.pick_proxy(url=url)
39 | if proxy_url:
40 | options.update(proxy_url=proxy_url)
41 | # 特殊处理的URL,不使用RSS代理
42 | if url and is_use_proxy_url(url):
43 | return options
44 | if CONFIG.rss_proxy_enable:
45 | options.update(
46 | rss_proxy_url=CONFIG.rss_proxy_url,
47 | rss_proxy_token=CONFIG.rss_proxy_token,
48 | )
49 | return options
50 |
--------------------------------------------------------------------------------
/rssant_api/models/registery.py:
--------------------------------------------------------------------------------
1 | from django.utils import timezone
2 | from jsonfield import JSONField
3 |
4 | from .helper import Model, models
5 |
6 |
7 | class Registery(Model):
8 | """scheduler/worker registery"""
9 |
10 | class Meta:
11 | indexes = [
12 | models.Index(fields=["registery_node"]),
13 | ]
14 |
15 | class Admin:
16 | display_fields = ['registery_node', 'registery_node_spec', 'dt_updated']
17 |
18 | registery_node = models.CharField(unique=True, max_length=200, help_text='registery node name')
19 | registery_node_spec = JSONField(help_text="registery node spec")
20 | node_specs = JSONField(help_text="node specs")
21 | dt_updated = models.DateTimeField(help_text="更新时间")
22 |
23 | @staticmethod
24 | def create_or_update(registery_node_spec, node_specs):
25 | registery_node = registery_node_spec['name']
26 | obj, created = Registery.objects.update_or_create(
27 | registery_node=registery_node,
28 | defaults=dict(
29 | registery_node_spec=registery_node_spec,
30 | node_specs=node_specs,
31 | dt_updated=timezone.now(),
32 | )
33 | )
34 | return obj
35 |
36 | @staticmethod
37 | def get(registery_node):
38 | try:
39 | return Registery.objects.get(registery_node=registery_node)
40 | except Registery.DoesNotExist:
41 | return None
42 |
--------------------------------------------------------------------------------
/rssant_api/tests/duplicate_feed_detector_tests.py:
--------------------------------------------------------------------------------
1 | from rssant_api.helper import DuplicateFeedDetector, reverse_url
2 |
3 |
4 | URL_LIST = [
5 | 'https://rsshub.app/v2ex/topics/hot',
6 | 'https://rsshub.app/v2ex/topics/hot.atom?mode=fulltext',
7 | 'https://rsshub.app/v2ex/topics/hot?mode=fulltext',
8 | 'https://rsshub.app/v2ex/topics/hot?mode=fulltext.atom',
9 | 'http://rsshub.app/v2ex/topics/hot',
10 | 'http://rsshub.app/v2ex/topics/hot?mode=fulltext',
11 | 'https://rsshub.ioiox.com/v2ex/topics/hot',
12 | 'https://rsshub.rssforever.com/v2ex/topics/hot',
13 | 'https://datatube.dev/api/rss/v2ex/topics/hot',
14 | 'http://wssgwps.fun:1200/v2ex/topics/hot',
15 | 'https://feed.glaceon.net/v2ex/topics/hot?mode=fulltext',
16 | 'https://rss.ez.rw/v2ex/topics/hot',
17 | ]
18 |
19 |
20 | def test_detect_duplicate_feed():
21 | detector = DuplicateFeedDetector()
22 | for index, url in enumerate(URL_LIST):
23 | detector.push(index, reverse_url(url))
24 | result_url_s = []
25 | for id_s in detector.poll():
26 | result_url_s.append(tuple(URL_LIST[x] for x in id_s))
27 | assert len(result_url_s) == 2
28 | assert result_url_s[0] == (
29 | 'https://rsshub.app/v2ex/topics/hot',
30 | 'http://rsshub.app/v2ex/topics/hot',
31 | )
32 | assert result_url_s[1] == (
33 | 'https://rsshub.app/v2ex/topics/hot?mode=fulltext',
34 | 'http://rsshub.app/v2ex/topics/hot?mode=fulltext',
35 | )
36 |
--------------------------------------------------------------------------------
/cursor/docs/ArXiv-技术方案设计.md:
--------------------------------------------------------------------------------
1 | # 技术方案设计文档:ArXiv 功能
2 |
3 | ## 文档信息
4 | - 作者:系统生成
5 | - 版本:v1.0
6 | - 日期:2025-11-20
7 | - 状态:已确认
8 | - 架构类型:非GBF框架
9 |
10 | # 一、名词解释
11 | | 术语 | 解释 |
12 | |------|------|
13 | | ArXivPaper | 论文实体(标题、摘要、作者、分类、时间) |
14 | | ArXivAIAnalysis | 论文AI分析结果(摘要/要点/评分/标签) |
15 | | ArXivReport | 分类/每日报告 |
16 |
17 | # 二、领域模型
18 | - `ArXivPaper/ArXivAIAnalysis/ArXivAIAnalysisSummary/ArXivReport`(`rssant_api/views/arxiv.py:1-149`)。
19 |
20 | # 三、应用调用关系
21 | ```mermaid
22 | flowchart TD
23 | U[用户] --> AXV[ArXivView]
24 | AXV --> AI[AI_SERVICE]
25 | AXV --> Sync[ArXivSyncService]
26 | AXV --> Trend[TrendAnalyzer]
27 | AXV --> DB[(Postgres)]
28 | ```
29 |
30 | # 四、详细方案设计
31 | ## 架构选型
32 | - Controller(ArXivView)→ Service(AI分析/报告生成/同步/趋势)→ Repository(ORM)。
33 |
34 | ### 分层架构说明
35 | - 视图:`rssant_api/views/arxiv.py`。
36 | - 报告生成与下载:`arxiv.report.generate_*` 与 `arxiv.report.download`(`rssant_api/views/arxiv.py:1037-1075`)。
37 |
38 | ## 典型接口
39 | - 论文列表/详情:`rssant_api/views/arxiv.py:249-314,356-379`。
40 | - AI分析生成:`rssant_api/views/arxiv.py:502-533`。
41 | - 分类报告/每日汇总生成:`rssant_api/views/arxiv.py:652-680,744-771`。
42 | - 报告下载:`GET /api/v1/arxiv.report.download`(`rssant_api/views/arxiv.py:1037-1075`)。
43 |
44 | ## 关键规则
45 | - 自动保存报告为 AI 分析总结以便统一列表展示(`rssant_api/views/arxiv.py:652-680,744-771`)。
46 | - ArXiv ID 生成 URL 的格式处理(`rssant_api/views/arxiv.py:276`)。
47 |
48 | ## 接口改动点
49 | - 当前无协议变更;若引入“引文网络图”,需扩展返回结构并提供下载接口。
50 |
51 | ## 数据库变更
52 | - 无;如支持“引用趋势与期刊影响因子”持久化,可扩展对应模型与字段。
--------------------------------------------------------------------------------
/scripts/check_rsshub.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # RSSHub 健康检查脚本
4 |
5 | set -e
6 |
7 | # 获取脚本所在目录
8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9 | cd "$SCRIPT_DIR/.."
10 |
11 | # 使用 docker compose 或 docker-compose
12 | if docker compose version &> /dev/null; then
13 | DOCKER_COMPOSE="docker compose"
14 | else
15 | DOCKER_COMPOSE="docker-compose"
16 | fi
17 |
18 | # 配置文件路径
19 | COMPOSE_FILE="docker-compose.rsshub.yml"
20 |
21 | # 颜色输出
22 | RED='\033[0;31m'
23 | GREEN='\033[0;32m'
24 | YELLOW='\033[1;33m'
25 | NC='\033[0m'
26 |
27 | echo "检查 RSSHub 服务状态..."
28 | echo ""
29 |
30 | # 检查容器状态
31 | echo "容器状态:"
32 | $DOCKER_COMPOSE -f "$COMPOSE_FILE" ps
33 | echo ""
34 |
35 | # 检查 RSSHub 健康状态
36 | RSSHUB_URL="http://localhost:1200"
37 | if curl -f -s "$RSSHUB_URL" > /dev/null 2>&1; then
38 | echo -e "${GREEN}✓ RSSHub 服务正常运行${NC}"
39 | echo " 访问地址: $RSSHUB_URL"
40 | else
41 | echo -e "${RED}✗ RSSHub 服务无法访问${NC}"
42 | echo " 请检查服务是否已启动: $DOCKER_COMPOSE -f $COMPOSE_FILE ps"
43 | echo " 查看日志: $DOCKER_COMPOSE -f $COMPOSE_FILE logs rsshub"
44 | fi
45 |
46 | echo ""
47 |
48 | # 检查 Redis 健康状态
49 | if docker exec rsshub-redis redis-cli ping > /dev/null 2>&1; then
50 | echo -e "${GREEN}✓ Redis 服务正常运行${NC}"
51 | else
52 | echo -e "${YELLOW}⚠ Redis 服务可能未正常运行${NC}"
53 | fi
54 |
55 | echo ""
56 |
57 | # 显示最近日志
58 | echo "最近日志(最后 20 行):"
59 | echo "----------------------------------------"
60 | $DOCKER_COMPOSE -f "$COMPOSE_FILE" logs --tail 20
61 |
62 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0011_auto_20190714_0550.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.7 on 2019-07-14 05:50
2 |
3 | from django.db import migrations, models
4 | import jsonfield.fields
5 | import ool
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('rssant_api', '0010_auto_20190519_0725'),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Registery',
17 | fields=[
18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19 | ('_version', ool.VersionField(default=0)),
20 | ('_created', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
21 | ('_updated', models.DateTimeField(auto_now=True, help_text='更新时间')),
22 | ('registery_node', models.CharField(help_text='registery node name', max_length=200, unique=True)),
23 | ('registery_node_spec', jsonfield.fields.JSONField(help_text='registery node spec')),
24 | ('node_specs', jsonfield.fields.JSONField(help_text='node specs')),
25 | ('dt_updated', models.DateTimeField(help_text='更新时间')),
26 | ],
27 | bases=(ool.VersionedMixin, models.Model),
28 | ),
29 | migrations.AddIndex(
30 | model_name='registery',
31 | index=models.Index(fields=['registery_node'], name='rssant_api__registe_c3cb8e_idx'),
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/rssant_asyncapi/main.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import click
4 | import gunicorn.app.base
5 |
6 | from .app import create_app
7 |
8 |
9 | class StandaloneApplication(gunicorn.app.base.BaseApplication):
10 |
11 | def __init__(self, app, options=None):
12 | self.options = options or {}
13 | self.application = app
14 | super().__init__()
15 |
16 | def load_config(self):
17 | for key, value in self.options.items():
18 | if key not in self.cfg.settings:
19 | raise ValueError(f'Unknown gunicorn option {key!r}')
20 | self.cfg.set(key, value)
21 |
22 | def load(self):
23 | return self.application
24 |
25 |
26 | @click.command()
27 | def main():
28 | """Run rssant asyncapi server."""
29 | bind = os.getenv('RSSANT_BIND_ADDRESS') or '0.0.0.0:6786'
30 | workers = int(os.getenv('RSSANT_NUM_WORKERS') or 1)
31 | keep_alive = int(os.getenv('RSSANT_KEEP_ALIVE') or 7200)
32 | options = {
33 | 'bind': bind,
34 | 'workers': workers,
35 | 'keepalive': keep_alive,
36 | 'worker_class': 'aiohttp.GunicornWebWorker',
37 | 'forwarded_allow_ips': '*',
38 | 'reuse_port': True,
39 | 'timeout': 300,
40 | 'accesslog': '-',
41 | 'errorlog': '-',
42 | 'loglevel': 'info',
43 | }
44 | wsgi_app = create_app()
45 | server = StandaloneApplication(wsgi_app, options)
46 | server.run()
47 |
48 |
49 | if __name__ == "__main__":
50 | main()
51 |
--------------------------------------------------------------------------------
/tests/common/test_rss_proxy.py:
--------------------------------------------------------------------------------
1 | from pytest_httpserver import HTTPServer
2 |
3 | from rssant_common.rss_proxy import RSSProxyClient, ProxyStrategy
4 |
5 |
6 | def test_rss_proxy_direct_first(rss_proxy_server, httpserver: HTTPServer):
7 | options = rss_proxy_server
8 | direct_url = httpserver.url_for('/direct/200')
9 | not_proxy_url = httpserver.url_for('/not-proxy?status=200')
10 | client = RSSProxyClient(
11 | rss_proxy_url=options['rss_proxy_url'],
12 | rss_proxy_token=options['rss_proxy_token'],
13 | )
14 | res = client.request('GET', direct_url)
15 | assert res.status_code == 200
16 | assert res.text == 'DIRECT'
17 | # direct request /not-proxy will response 500
18 | assert client.request('GET', not_proxy_url).status_code == 500
19 |
20 |
21 | def test_rss_proxy_proxy_first(rss_proxy_server, httpserver: HTTPServer):
22 | options = rss_proxy_server
23 | direct_url = httpserver.url_for('/direct/200')
24 | not_proxy_url = httpserver.url_for('/not-proxy?status=200')
25 | client = RSSProxyClient(
26 | rss_proxy_url=options['rss_proxy_url'],
27 | rss_proxy_token=options['rss_proxy_token'],
28 | proxy_strategy=ProxyStrategy.PROXY_FIRST,
29 | )
30 | # rss proxy failed, fallback to direct
31 | res = client.request('GET', direct_url)
32 | assert res.status_code == 200
33 | assert res.text == 'DIRECT'
34 | # rss proxy success
35 | assert client.request('GET', not_proxy_url).status_code == 200
36 |
--------------------------------------------------------------------------------
/frontend/src/store/useThemeStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { persist, createJSONStorage } from 'zustand/middleware'
3 |
4 | type Theme = 'light' | 'dark' | 'system'
5 |
6 | interface ThemeState {
7 | theme: Theme
8 | setTheme: (theme: Theme) => void
9 | }
10 |
11 | export const useThemeStore = create()(
12 | persist(
13 | (set) => ({
14 | theme: 'system',
15 | setTheme: (theme) => {
16 | set({ theme })
17 | applyTheme(theme)
18 | },
19 | }),
20 | {
21 | name: 'theme-storage',
22 | storage: createJSONStorage(() => localStorage),
23 | }
24 | )
25 | )
26 |
27 | function applyTheme(theme: Theme) {
28 | const root = window.document.documentElement
29 | root.classList.remove('light', 'dark')
30 |
31 | if (theme === 'system') {
32 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
33 | ? 'dark'
34 | : 'light'
35 | root.classList.add(systemTheme)
36 | } else {
37 | root.classList.add(theme)
38 | }
39 | }
40 |
41 | // Initialize theme on load
42 | if (typeof window !== 'undefined') {
43 | const store = useThemeStore.getState()
44 | applyTheme(store.theme)
45 |
46 | // Listen for system theme changes
47 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
48 | const currentTheme = useThemeStore.getState().theme
49 | if (currentTheme === 'system') {
50 | applyTheme('system')
51 | }
52 | })
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/scripts/pg_count.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 |
4 | import click
5 |
6 | from rssant_common.helper import pretty_format_json
7 | from rssant_harbor.pg_count import pg_count, pg_verify
8 |
9 |
10 | @click.command()
11 | @click.option('--verify', type=str, help='target filepath, or - to query database')
12 | @click.option('--verify-bias', type=float, default=0.003)
13 | @click.argument('filepath', type=str, default='-')
14 | def main(verify, filepath, verify_bias):
15 | import rssant_common.django_setup # noqa:F401
16 |
17 | if verify:
18 | if verify != '-':
19 | with open(verify) as f:
20 | result = json.load(f)
21 | else:
22 | result = pg_count()
23 | if filepath and filepath != '-':
24 | with open(filepath) as f:
25 | content = f.read()
26 | else:
27 | content = sys.stdin.read()
28 | expect_result = json.loads(content)
29 | verify_result = pg_verify(result, expect_result, verify_bias)
30 | for detail in verify_result['details']:
31 | print(detail['message'])
32 | is_ok = verify_result['is_all_ok']
33 | sys.exit(0 if is_ok else 1)
34 | else:
35 | result = pg_count()
36 | content = pretty_format_json(result)
37 | if filepath and filepath != '-':
38 | with open(filepath, 'w') as f:
39 | f.write(content)
40 | else:
41 | print(content)
42 |
43 |
44 | if __name__ == "__main__":
45 | main()
46 |
--------------------------------------------------------------------------------
/rssant_common/throttle.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import time
3 |
4 |
5 | class throttle:
6 | def __init__(self, seconds: int, _is_async=False) -> None:
7 | self._seconds = seconds
8 | self._is_async = _is_async
9 | self._last_call_time = None
10 |
11 | def _check_call(self):
12 | now = time.monotonic()
13 | if self._last_call_time is None:
14 | self._last_call_time = now
15 | return True
16 | if now - self._last_call_time >= self._seconds:
17 | self._last_call_time = now
18 | return True
19 | return False
20 |
21 | def _get_wrapper(self, func):
22 | state = self
23 |
24 | @functools.wraps(func)
25 | def wrapper(self, *args, **kwargs):
26 | if state._check_call():
27 | func(self, *args, **kwargs)
28 |
29 | return wrapper
30 |
31 | def _get_async_wrapper(self, func):
32 | state = self
33 |
34 | @functools.wraps(func)
35 | async def wrapper(self, *args, **kwargs):
36 | if state._check_call():
37 | await func(self, *args, **kwargs)
38 |
39 | return wrapper
40 |
41 | def __call__(self, func):
42 | if self._is_async:
43 | wrapper = self._get_async_wrapper(func)
44 | else:
45 | wrapper = self._get_wrapper(func)
46 | return wrapper
47 |
48 |
49 | class async_throttle(throttle):
50 | def __init__(self, seconds: int) -> None:
51 | super().__init__(seconds, _is_async=True)
52 |
--------------------------------------------------------------------------------
/rssant_common/chnlist/chnlist.py:
--------------------------------------------------------------------------------
1 | import gzip
2 | from pathlib import Path
3 |
4 | import tldextract
5 | from marisa_trie import Trie
6 |
7 | _CHNLIST_FILEPATH = Path(__file__).parent / 'chnlist.txt.gz'
8 |
9 |
10 | def _get_main_domain(fqdn: str):
11 | extract_domain = tldextract.TLDExtract(suffix_list_urls=[])
12 | extracted = extract_domain(fqdn)
13 | main_domain = f"{extracted.domain}.{extracted.suffix}"
14 | return main_domain
15 |
16 |
17 | class ChinaWebsiteList:
18 | def __init__(self) -> None:
19 | self._chnlist_trie: Trie = None
20 |
21 | def _read_chnlist_text(self):
22 | content = _CHNLIST_FILEPATH.read_bytes()
23 | content = gzip.decompress(content)
24 | text = content.decode('utf-8')
25 | return text
26 |
27 | def _build_chnlist_trie(self):
28 | text = self._read_chnlist_text()
29 | domain_s = []
30 | for line in text.splitlines():
31 | line = line.strip()
32 | if not line or line.startswith('#'):
33 | continue
34 | domain_s.append(line)
35 | trie = Trie(domain_s)
36 | return trie
37 |
38 | def _get_trie(self):
39 | if self._chnlist_trie is None:
40 | self._chnlist_trie = self._build_chnlist_trie()
41 | return self._chnlist_trie
42 |
43 | def is_china_website(self, hostname: str) -> bool:
44 | trie = self._get_trie()
45 | domain = _get_main_domain(hostname)
46 | return domain in trie
47 |
48 |
49 | CHINA_WEBSITE_LIST = ChinaWebsiteList()
50 |
--------------------------------------------------------------------------------
/unmaintain/benchmark/benchmark_asyncio_postgres.py:
--------------------------------------------------------------------------------
1 | import time
2 | import asyncio
3 | import aiopg
4 | import uvloop
5 | import asyncpg
6 |
7 |
8 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
9 |
10 |
11 | dsn = 'dbname=rssant user=rssant password=rssant host=127.0.0.1'
12 |
13 |
14 | async def run_aiopg():
15 | pool = await aiopg.create_pool(dsn, minsize=5, maxsize=5)
16 | t0 = time.time()
17 | for i in range(1000):
18 | async with pool.acquire() as conn:
19 | async with conn.cursor() as cur:
20 | await cur.execute("SELECT 1")
21 | ret = []
22 | async for row in cur:
23 | ret.append(row)
24 | assert ret == [(1,)]
25 | print('run_aiopg', time.time() - t0)
26 | pool.close()
27 | await pool.wait_closed()
28 |
29 |
30 | async def run_asyncpg():
31 | async with asyncpg.create_pool(
32 | user='rssant', password='rssant',
33 | database='rssant', host='127.0.0.1',
34 | command_timeout=60, min_size=5, max_size=5
35 | ) as pool:
36 | t0 = time.time()
37 | for i in range(1000):
38 | async with pool.acquire() as conn:
39 | values = await conn.fetch("SELECT 1")
40 | assert values == [(1,)]
41 | print('run_asyncpg', time.time() - t0)
42 | await pool.close()
43 |
44 |
45 | loop = asyncio.get_event_loop()
46 | loop.run_until_complete(run_aiopg())
47 |
48 | loop = asyncio.get_event_loop()
49 | loop.run_until_complete(run_asyncpg())
50 |
--------------------------------------------------------------------------------
/cursor/docs/发布能力-技术方案设计.md:
--------------------------------------------------------------------------------
1 | # 技术方案设计文档:发布能力(匿名发布与鉴权)
2 |
3 | ## 文档信息
4 | - 作者:系统生成
5 | - 版本:v1.0
6 | - 日期:2025-11-20
7 | - 状态:已确认
8 | - 架构类型:非GBF框架
9 |
10 | # 一、名词解释
11 | | 术语 | 解释 |
12 | |------|------|
13 | | PublishView | 发布相关的公开接口视图 |
14 | | UserPublish | 用户发布配置(是否开启、根地址、标题、是否全公开) |
15 | | publish_unionid | 发布联合ID(用户与发布ID组合) |
16 |
17 | # 二、领域模型
18 | - `UserPublish`(`rssant_api/models/user_publish.py`)。
19 |
20 | # 三、应用调用关系
21 | ```mermaid
22 | flowchart TD
23 | U[匿名/登录用户] --> PV[PublishView]
24 | PV --> UPV[UserPublishView]
25 | PV --> DB[(Postgres)]
26 | 其他视图 --> PV
27 | ```
28 |
29 | # 四、详细方案设计
30 | ## 架构选型
31 | - Controller(PublishView/UserPublishView)→ Service(UserPublish 缓存与解析)→ Repository(ORM)。
32 |
33 | ### 分层架构说明
34 | - 视图:`rssant_api/views/publish.py:1`(发布信息获取与统一鉴权辅助);`rssant_api/views/user_publish.py:1`(用户配置读写)。
35 | - 其他视图与发布态:`FeedView.post('publish.feed_get')`(`rssant_api/views/feed.py:230-251`)。
36 |
37 | ## 接口与设计
38 | - 公开获取发布页面配置:`POST /api/v1/publish.info`(`rssant_api/views/publish.py:17`)
39 | - 用户获取/设置发布配置:`POST /api/v1/user_publish.get/set`(`rssant_api/views/user_publish.py:23-63`)
40 | - 发布态鉴权:
41 | - 通过请求头 `x-rssant-publish` 或登录态解析发布信息(`rssant_api/views/publish.py:41-88`)。
42 | - `require_publish_user/is_only_publish` 在相关接口中控制用户身份与可见范围(`rssant_api/views/publish.py:90-98`)。
43 |
44 | ## 关键规则
45 | - 发布禁用时返回 `is_enable=False`,其他视图在发布态下仅返回公开内容。
46 | - 图片代理信息可下发用于前端渲染(`rssant_api/views/publish.py:25-33`)。
47 |
48 | ## 接口改动点
49 | - 当前无协议变更;如支持“多站点发布”,需扩展 `UserPublish` 与路由解析逻辑。
50 |
51 | ## 数据库变更
52 | - 无;如支持多站点与多主题,需要在 `UserPublish` 中增加配置项与索引。
--------------------------------------------------------------------------------
/rssant/auth_serializer.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urljoin
2 |
3 | from django.contrib.auth.forms import PasswordResetForm
4 | from rest_auth.serializers import PasswordResetSerializer
5 |
6 | from rssant_common.standby_domain import get_request_root_url
7 |
8 | from .settings import DEFAULT_FROM_EMAIL
9 | from .email_template import RESET_PASSWORD_TEMPLATE
10 |
11 |
12 | class RssantPasswordResetForm(PasswordResetForm):
13 |
14 | def __init__(self, *args, **kwargs):
15 | self._request = None
16 | super().__init__(*args, **kwargs)
17 |
18 | def get_from_email(self):
19 | """
20 | This is a hook that can be overridden to programatically
21 | set the 'from' email address for sending emails
22 | """
23 | return DEFAULT_FROM_EMAIL
24 |
25 | def send_mail(self, subject_template_name, email_template_name,
26 | context, from_email, to_email, html_email_template_name=None):
27 | link = 'reset-password/{}?token={}'.format(context['uid'], context['token'])
28 | root_url = get_request_root_url(self._request)
29 | link = urljoin(root_url, link)
30 | my_context = dict(rssant_url=root_url, email=to_email, link=link)
31 | RESET_PASSWORD_TEMPLATE.send(self.get_from_email(), to_email, my_context)
32 |
33 | def save(self, *args, **kwargs):
34 | self._request = kwargs.get('request')
35 | super().save(*args, **kwargs)
36 |
37 |
38 | class RssantPasswordResetSerializer(PasswordResetSerializer):
39 | password_reset_form_class = RssantPasswordResetForm
40 |
--------------------------------------------------------------------------------
/scripts/docker/start-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # RSS-AIGC Docker启动脚本
4 | # 使用conda环境中的配置
5 |
6 | set -e
7 |
8 | # 获取脚本所在目录
9 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10 | cd "$SCRIPT_DIR"
11 |
12 | # 创建Docker volumes
13 | echo "创建Docker volumes..."
14 | docker volume create rssant-data || true
15 | docker volume create rssant-postgres-data || true
16 | docker volume create rssant-postgres-logs || true
17 |
18 | # 停止并删除旧容器
19 | echo "停止并删除旧容器..."
20 | docker stop -t 3 rssant 2>/dev/null || true
21 | docker rm -f rssant 2>/dev/null || true
22 |
23 | # 检查配置文件
24 | ENV_FILE="box/rssant.env"
25 | if [ ! -f "$ENV_FILE" ]; then
26 | echo "错误: 配置文件 $ENV_FILE 不存在"
27 | exit 1
28 | fi
29 |
30 | # 启动Docker容器
31 | echo "启动Docker容器..."
32 | # 获取项目根目录(脚本所在目录的父目录)
33 | PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
34 | docker run -ti --name rssant -d \
35 | -p 6789:80 \
36 | -p 5432:5432 \
37 | --env-file "$ENV_FILE" \
38 | -v rssant-data:/app/data \
39 | -v rssant-postgres-data:/var/lib/postgresql/11/main \
40 | -v rssant-postgres-logs:/var/log/postgresql \
41 | --log-driver json-file --log-opt max-size=50m --log-opt max-file=10 \
42 | --restart unless-stopped \
43 | guyskk/rssant:latest "$@"
44 |
45 | echo ""
46 | echo "容器已启动!"
47 | echo "查看日志: docker logs -f rssant"
48 | echo "查看状态: docker exec -ti rssant supervisorctl status"
49 | echo "访问地址: http://localhost:6789"
50 | echo "管理后台: http://localhost:6789/admin/ (用户名: admin, 密码: admin)"
51 | echo ""
52 | echo "正在显示容器日志(按Ctrl+C退出)..."
53 | docker logs --tail 100 -f rssant
54 |
55 |
--------------------------------------------------------------------------------
/rssant_api/tests/story_storage_tests.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.test import TestCase
3 |
4 | from rssant_config import CONFIG
5 | from rssant_api.models.story_storage import PostgresClient, PostgresStoryStorage
6 |
7 |
8 | FEED_IDS = [123, 66_000]
9 | CONTENTS = {
10 | 'empty': None,
11 | 'simple': 'hello world\n你好世界\n',
12 | }
13 |
14 |
15 | @pytest.mark.dbtest
16 | class PostgresStoryStorageTestCase(TestCase):
17 |
18 | def test_story_storage(self):
19 | for feed_id in FEED_IDS:
20 | for content_name in CONTENTS:
21 | self._test_story_storage(feed_id, content_name)
22 |
23 | def setUp(self):
24 | db = dict(
25 | user=CONFIG.pg_user,
26 | password=CONFIG.pg_password,
27 | host=CONFIG.pg_host,
28 | port=CONFIG.pg_port,
29 | db='test_' + CONFIG.pg_db,
30 | )
31 | volumes = {
32 | 0: dict(**db, table='story_volume_0'),
33 | 1: dict(**db, table='story_volume_1'),
34 | }
35 | self.client = PostgresClient(volumes)
36 |
37 | def tearDown(self):
38 | self.client.close()
39 |
40 | def _test_story_storage(self, feed_id, content_name):
41 | storage = PostgresStoryStorage(self.client)
42 | content = CONTENTS[content_name]
43 | storage.save_content(feed_id, 234, content)
44 | got = storage.get_content(feed_id, 234)
45 | assert got == content
46 | storage.delete_content(feed_id, 234)
47 | got = storage.get_content(feed_id, 234)
48 | assert got is None
49 |
--------------------------------------------------------------------------------
/deploy/rssant_asyncapi/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8.12-bullseye AS build
2 | WORKDIR /app
3 | ARG PYPI_MIRROR="https://mirrors.aliyun.com/pypi/simple/"
4 | ENV PIP_INDEX_URL=$PYPI_MIRROR PIP_DISABLE_PIP_VERSION_CHECK=1
5 | RUN python -m venv .venv
6 | ENV PATH=/app/.venv/bin:$PATH
7 | COPY requirements-pip.txt .
8 | RUN --mount=type=cache,target=/root/.cache/pip \
9 | pip install -r requirements-pip.txt
10 | COPY constraint.txt .
11 | ENV PIP_CONSTRAINT=/app/constraint.txt
12 | COPY requirements.txt .
13 | COPY requirements-build.txt .
14 | RUN --mount=type=cache,target=/root/.cache/pip \
15 | pip install -r requirements.txt -r requirements-build.txt
16 | COPY . /app
17 | RUN bash deploy/rssant_asyncapi/pyinstaller_build.sh && \
18 | /app/dist/run-asyncapi/run-asyncapi --help && \
19 | du -sh /app/dist/run-asyncapi
20 |
21 | FROM debian:bullseye-slim AS runtime
22 | ENV LC_ALL=C.UTF-8 LANG=C.UTF-8
23 | WORKDIR /app
24 | ARG PYPI_MIRROR="https://mirrors.aliyun.com/pypi/simple/"
25 | ENV PIP_INDEX_URL=$PYPI_MIRROR PIP_DISABLE_PIP_VERSION_CHECK=1
26 | RUN apt-get update && \
27 | apt-get install -y --no-install-recommends ca-certificates && \
28 | update-ca-certificates && \
29 | rm -rf /var/lib/apt/lists/*
30 |
31 | FROM runtime AS check
32 | COPY --from=build /app/dist/run-asyncapi /app
33 | RUN /app/run-asyncapi --help
34 |
35 | FROM runtime
36 | COPY --from=check /app /app
37 | ARG EZFAAS_BUILD_ID=''
38 | ARG EZFAAS_COMMIT_ID=''
39 | ENV EZFAAS_BUILD_ID=${EZFAAS_BUILD_ID} EZFAAS_COMMIT_ID=${EZFAAS_COMMIT_ID}
40 | ENV RSSANT_BIND_ADDRESS=0.0.0.0:9000
41 | CMD [ "/app/run-asyncapi" ]
42 |
--------------------------------------------------------------------------------
/rssant_api/migrations/0003_auto_20190327_1304.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.7 on 2019-03-27 13:04
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rssant_api', '0002_auto_20190317_1020'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='feed',
15 | name='content_hash_method',
16 | ),
17 | migrations.RemoveField(
18 | model_name='feed',
19 | name='content_hash_value',
20 | ),
21 | migrations.RemoveField(
22 | model_name='rawfeed',
23 | name='content_hash_method',
24 | ),
25 | migrations.RemoveField(
26 | model_name='rawfeed',
27 | name='content_hash_value',
28 | ),
29 | migrations.AddField(
30 | model_name='feed',
31 | name='content_hash_base64',
32 | field=models.CharField(blank=True, help_text='base64 hash value of content', max_length=200, null=True),
33 | ),
34 | migrations.AddField(
35 | model_name='rawfeed',
36 | name='content_hash_base64',
37 | field=models.CharField(blank=True, help_text='base64 hash value of content', max_length=200, null=True),
38 | ),
39 | migrations.AddField(
40 | model_name='story',
41 | name='content_hash_base64',
42 | field=models.CharField(blank=True, help_text='base64 hash value of content', max_length=200, null=True),
43 | ),
44 | ]
45 |
--------------------------------------------------------------------------------
/tests/feedlib/test_response_file.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from rssant_feedlib import FeedResponseBuilder
4 | from rssant_feedlib.response_file import FeedResponseFile
5 |
6 |
7 | def _build_simple_response(status):
8 | builder = FeedResponseBuilder()
9 | builder.url('https://www.example.com/feed.xml')
10 | builder.status(status)
11 | builder.headers({
12 | 'etag': '5e8c43f8-4d269b',
13 | 'content-type': 'application/xml',
14 | })
15 | builder.content('''
16 |
17 |
18 | V2EX - 技术
19 | '''.encode('utf-8'))
20 | response = builder.build()
21 | return response
22 |
23 |
24 | def _build_no_content_response(status):
25 | builder = FeedResponseBuilder()
26 | builder.url('https://www.example.com/feed.xml')
27 | builder.status(status)
28 | response = builder.build()
29 | return response
30 |
31 |
32 | _builder_funcs = {
33 | 'simple': _build_simple_response,
34 | 'no_content': _build_no_content_response,
35 | }
36 |
37 |
38 | @pytest.mark.parametrize('builder, status', [
39 | ('simple', 200),
40 | ('simple', 500),
41 | ('no_content', 204),
42 | ('no_content', -200),
43 | ])
44 | def test_response_file(tmp_path, builder, status):
45 | response = _builder_funcs[builder](status)
46 | file = FeedResponseFile(tmp_path / 'test_response_file')
47 | file.write(response)
48 | file2 = FeedResponseFile(file.filepath)
49 | response2 = file2.read()
50 | assert repr(response) == repr(response2)
51 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as AvatarPrimitive from '@radix-ui/react-avatar'
3 | import { cn } from '@/lib/utils'
4 |
5 | const Avatar = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Avatar.displayName = AvatarPrimitive.Root.displayName
19 |
20 | const AvatarImage = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
31 |
32 | const AvatarFallback = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
46 |
47 | export { Avatar, AvatarImage, AvatarFallback }
48 |
49 |
--------------------------------------------------------------------------------
/cursor/docs/AI娱乐与AIGC-技术方案设计.md:
--------------------------------------------------------------------------------
1 | # 技术方案设计文档:AI娱乐与AIGC
2 |
3 | ## 文档信息
4 | - 作者:系统生成
5 | - 版本:v1.0
6 | - 日期:2025-11-20
7 | - 状态:已确认
8 | - 架构类型:非GBF框架
9 |
10 | # 一、名词解释
11 | | 术语 | 解释 |
12 | |------|------|
13 | | AIEntertainmentReport | AI 影视/娱乐报告实体 |
14 | | AIEntertainmentOnlyReport | 独立的AI影视日报报告实体 |
15 | | AIGCReport | AIGC 报告实体 |
16 |
17 | # 二、领域模型
18 | - `AIEntertainmentReport/AIEntertainmentOnlyReport/AIGCReport`(参考 `rssant_api/views/ai_entertainment.py:94,208-249` 与 `rssant_api/views/story.py:1211-1250`)。
19 |
20 | # 三、应用调用关系
21 | ```mermaid
22 | flowchart TD
23 | U[用户] --> AEV[AIEntertainmentView]
24 | AEV --> Generator[ReportGenerator]
25 | AEV --> DB[(Postgres)]
26 | FeishuBotView --> ContentList[内容转换]
27 | ContentList --> DB
28 | ```
29 |
30 | # 四、详细方案设计
31 | ## 架构选型
32 | - Controller(AIEntertainmentView/StoryView)→ Service(报告生成与转换)→ Repository(ORM)。
33 |
34 | ### 分层架构说明
35 | - 视图:`rssant_api/views/ai_entertainment.py:94,208-249`(生成与获取报告)。
36 | - 统一列表与聚合:`rssant_api/views/story.py:1211-1250`。
37 | - 飞书内容列表转换:`rssant_api/views/feishu_bot.py:813-876`。
38 |
39 | ## 典型接口
40 | - 生成独立AI影视日报:`POST /api/v1/ai_entertainment.only.report.generate`(`rssant_api/views/ai_entertainment.py:94-152`)。
41 | - 获取单个报告:`POST /api/v1/ai_entertainment.report.get`(`rssant_api/views/ai_entertainment.py:208-249`)。
42 | - 统一AI报告列表:`POST /api/v1/ai_summary.list_all`(`rssant_api/views/story.py:808-1250`)。
43 |
44 | ## 关键规则
45 | - 若报告已存在,优先返回现有记录(幂等)(`rssant_api/views/ai_entertainment.py:94-108`)。
46 | - 内容落库后可作为飞书机器人待发送列表的来源(`feishu_bot.content.list`)。
47 |
48 | ## 接口改动点
49 | - 当前无协议变更;如支持“搜索引擎切换”,需在生成接口中增加参数(如 `use_tavily` 已支持)。
50 |
51 | ## 数据库变更
52 | - 无;扩展可考虑持久化“来源链接标题映射”以提升展示质量。
--------------------------------------------------------------------------------
/scripts/README_FIX_LOGIN.md:
--------------------------------------------------------------------------------
1 | # 登录 500 错误快速修复
2 |
3 | ## 快速修复
4 |
5 | 运行以下命令之一来自动诊断和修复登录 500 错误:
6 |
7 | ### 方法 1: 使用 Bash 脚本(推荐)
8 |
9 | ```bash
10 | cd /Users/gongfan/Desktop/公司项目/rss-subscribe
11 | bash scripts/fix_login_500.sh
12 | ```
13 |
14 | ### 方法 2: 使用 Python 脚本
15 |
16 | ```bash
17 | cd /Users/gongfan/Desktop/公司项目/rss-subscribe
18 | python scripts/fix_login_500.py
19 | ```
20 |
21 | ## 脚本会自动执行以下操作
22 |
23 | 1. ✅ 检查 Docker 容器是否运行
24 | 2. ✅ 检查数据库连接
25 | 3. ✅ 运行数据库迁移(修复 Token 表问题)
26 | 4. ✅ 检查 Token 表是否存在
27 | 5. ✅ 检查用户是否存在
28 | 6. ✅ 查看最近的错误日志
29 |
30 | ## 手动修复步骤
31 |
32 | 如果脚本无法自动修复,可以手动执行以下步骤:
33 |
34 | ### 1. 运行数据库迁移
35 |
36 | ```bash
37 | docker exec -it rssant python manage.py migrate
38 | ```
39 |
40 | ### 2. 确保 Token 表存在
41 |
42 | ```bash
43 | docker exec -it rssant python manage.py migrate authtoken
44 | ```
45 |
46 | ### 3. 检查用户
47 |
48 | ```bash
49 | docker exec -it rssant python manage.py shell
50 | ```
51 |
52 | 然后在 Python shell 中:
53 |
54 | ```python
55 | from django.contrib.auth.models import User
56 | users = User.objects.all()
57 | for u in users:
58 | print(f"Username: {u.username}, Email: {u.email}, Active: {u.is_active}")
59 | ```
60 |
61 | ### 4. 查看日志
62 |
63 | ```bash
64 | docker logs -f rssant
65 | ```
66 |
67 | ## 常见问题
68 |
69 | ### Q: 脚本提示容器未运行?
70 |
71 | A: 先启动容器:
72 | ```bash
73 | docker start rssant
74 | # 或者
75 | cd /path/to/rss-subscribe
76 | docker-compose up -d
77 | ```
78 |
79 | ### Q: 迁移失败?
80 |
81 | A: 检查数据库连接和配置,确保数据库服务正常运行。
82 |
83 | ### Q: 仍然出现 500 错误?
84 |
85 | A: 查看详细日志:
86 | ```bash
87 | docker logs --tail 100 rssant | grep -i error
88 | ```
89 |
90 | 然后查看 `docs/DEBUG_500_ERROR.md` 获取更多帮助。
91 |
92 |
--------------------------------------------------------------------------------
/cleanup_sensitive_data.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Git 历史敏感信息清理脚本
3 | # 警告:此操作会重写 Git 历史,请谨慎使用
4 |
5 | set -e
6 |
7 | echo "=========================================="
8 | echo "Git 历史敏感信息清理脚本"
9 | echo "=========================================="
10 | echo ""
11 | echo "⚠️ 警告:此操作会重写 Git 历史记录"
12 | echo " 如果仓库已推送到远程,需要强制推送"
13 | echo " 建议先备份仓库或创建新分支测试"
14 | echo ""
15 | read -p "是否继续?(yes/no): " confirm
16 |
17 | if [ "$confirm" != "yes" ]; then
18 | echo "操作已取消"
19 | exit 0
20 | fi
21 |
22 | echo ""
23 | echo "开始清理敏感信息..."
24 |
25 | # 要清理的敏感信息列表
26 | SENSITIVE_KEYS=(
27 | "[REDACTED_API_KEY]"
28 | "[REDACTED_TAVILY_KEY]"
29 | "[REDACTED_FEISHU_SECRET]"
30 | "bce-v3/ALTAK-AyPWlyFTQg1OGWxD2vXzp"
31 | "cli_a99c30170f6e501c"
32 | )
33 |
34 | # 使用 git filter-branch 清理历史
35 | for key in "${SENSITIVE_KEYS[@]}"; do
36 | echo "清理密钥: ${key:0:20}..."
37 | git filter-branch --force --index-filter \
38 | "git rm --cached --ignore-unmatch -r . && git reset --hard" \
39 | --prune-empty --tag-name-filter cat -- --all 2>/dev/null || true
40 |
41 | # 替换文件内容中的敏感信息
42 | git filter-branch --force --tree-filter \
43 | "find . -type f -name '*.env*' -exec sed -i '' 's|${key}|REDACTED|g' {} + 2>/dev/null || true" \
44 | --prune-empty --tag-name-filter cat -- --all 2>/dev/null || true
45 | done
46 |
47 | echo ""
48 | echo "清理完成!"
49 | echo ""
50 | echo "下一步操作:"
51 | echo "1. 检查清理结果: git log --all --oneline"
52 | echo "2. 如果满意,清理备份: rm -rf .git/refs/original/"
53 | echo "3. 强制垃圾回收: git reflog expire --expire=now --all && git gc --prune=now --aggressive"
54 | echo "4. 如果已推送到远程,需要强制推送: git push --force --all"
55 | echo ""
56 |
--------------------------------------------------------------------------------
/rssant_api/tests/reverse_url_tests.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import yarl
3 | from rssant_api.helper import reverse_url, forward_url
4 |
5 |
6 | reverse_and_forward_url_cases = [
7 | (
8 | 'https://rss.anyant.com',
9 | 'com.anyant.rss!443!https/'
10 | ),
11 | (
12 | 'http://rss.anyant.com',
13 | 'com.anyant.rss!80!http/'
14 | ),
15 | (
16 | 'http://rss.anyant.com:8000',
17 | 'com.anyant.rss!8000!http/'
18 | ),
19 | (
20 | 'https://rss.anyant.com/changelog',
21 | 'com.anyant.rss!443!https/changelog'
22 | ),
23 | (
24 | 'https://rss.anyant.com/changelog?',
25 | 'com.anyant.rss!443!https/changelog'
26 | ),
27 | (
28 | 'https://rss.anyant.com/changelog.atom?version=1.0.0',
29 | 'com.anyant.rss!443!https/changelog.atom?version=1.0.0'
30 | ),
31 | (
32 | 'https://rss.anyant.com/changelog.atom?version=1.0.0#tag=abc',
33 | 'com.anyant.rss!443!https/changelog.atom?version=1.0.0#tag=abc'
34 | ),
35 | (
36 | 'https://rss.anyant.com/博客?版本=1.1.0#tag=abc',
37 | 'com.anyant.rss!443!https/%E5%8D%9A%E5%AE%A2?%E7%89%88%E6%9C%AC=1.1.0#tag=abc'
38 | ),
39 | (
40 | 'https://rss.anyant.com/%E5%8D%9A%E5%AE%A2?%E7%89%88%E6%9C%AC=1.1.0#tag=abc',
41 | 'com.anyant.rss!443!https/%E5%8D%9A%E5%AE%A2?%E7%89%88%E6%9C%AC=1.1.0#tag=abc'
42 | ),
43 | ]
44 |
45 |
46 | @pytest.mark.parametrize('n', range(len(reverse_and_forward_url_cases)))
47 | def test_reverse_and_forward_url(n):
48 | url, rev_url = reverse_and_forward_url_cases[n]
49 | assert reverse_url(url) == rev_url
50 | assert yarl.URL(forward_url(rev_url)) == yarl.URL(url)
51 |
--------------------------------------------------------------------------------
/rssant_api/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include, path
2 |
3 | from rssant_config import CONFIG
4 | from rssant_harbor.view import HarborView
5 | from rssant_worker.view import WorkerView
6 |
7 | from .views import health
8 | from .views.ezrevenue import EzrevenueView
9 | from .views.feed import FeedView
10 | from .views.github import GitHubView
11 | from .views.hacker_news import HackerNewsView
12 | from .views.arxiv import ArXivView
13 | from .views.ai_entertainment import AIEntertainmentView
14 | from .views.aigc import AIGCView
15 | from .views.feishu_bot import FeishuBotView
16 | from .views.publish import PublishView
17 | from .views.story import StoryView
18 | from .views.user import UserView
19 | from .views.user_publish import UserPublishView
20 |
21 |
22 | def _gen_urlpatterns():
23 | yield path('', include(health.urls))
24 | if not CONFIG.is_role_api:
25 | yield path('', include(WorkerView.urls))
26 | else:
27 | yield path('', include(FeedView.urls))
28 | yield path('', include(StoryView.urls))
29 | yield path('', include(HackerNewsView.urls))
30 | yield path('', include(GitHubView.urls))
31 | yield path('', include(ArXivView.urls))
32 | yield path('', include(AIEntertainmentView.urls))
33 | yield path('', include(AIGCView.urls))
34 | yield path('', include(FeishuBotView.urls))
35 | yield path('', include(UserView.urls))
36 | yield path('', include(PublishView.urls))
37 | yield path('', include(UserPublishView.urls))
38 | yield path('', include(EzrevenueView.urls))
39 | yield path('', include(HarborView.urls))
40 |
41 |
42 | app_name = 'rssant_api'
43 | urlpatterns = list(_gen_urlpatterns())
44 |
--------------------------------------------------------------------------------