├── requirements.txt ├── _assets ├── X.png ├── discord.png ├── linkedin.png ├── threads.png ├── google_news.png └── icon.svg ├── .env.example ├── main.py ├── provider ├── rsshub.py └── rsshub.yaml ├── PRIVACY.md ├── manifest.yaml ├── LICENSE ├── tools ├── rsshub.yaml ├── custom_route.yaml ├── rsshub.py ├── google_news.py ├── google_news.yaml ├── twitter.yaml ├── discord.yaml ├── linkedin.py ├── threads.py ├── discord.py ├── threads.yaml ├── linkedin.yaml └── twitter.py ├── .gitignore ├── .difyignore ├── README.md └── GUIDE.md /requirements.txt: -------------------------------------------------------------------------------- 1 | dify-plugin==0.0.1b72 2 | requests~=2.32.3 3 | -------------------------------------------------------------------------------- /_assets/X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stvlynn/RSSHub-Dify-Plugin/HEAD/_assets/X.png -------------------------------------------------------------------------------- /_assets/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stvlynn/RSSHub-Dify-Plugin/HEAD/_assets/discord.png -------------------------------------------------------------------------------- /_assets/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stvlynn/RSSHub-Dify-Plugin/HEAD/_assets/linkedin.png -------------------------------------------------------------------------------- /_assets/threads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stvlynn/RSSHub-Dify-Plugin/HEAD/_assets/threads.png -------------------------------------------------------------------------------- /_assets/google_news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stvlynn/RSSHub-Dify-Plugin/HEAD/_assets/google_news.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | INSTALL_METHOD=remote 2 | REMOTE_INSTALL_HOST=debug.dify.ai 3 | REMOTE_INSTALL_PORT=5003 4 | REMOTE_INSTALL_KEY=********-****-****-****-************ 5 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from dify_plugin import Plugin, DifyPluginEnv 2 | 3 | plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120)) 4 | 5 | if __name__ == '__main__': 6 | plugin.run() 7 | -------------------------------------------------------------------------------- /provider/rsshub.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from dify_plugin import ToolProvider 4 | from dify_plugin.errors.tool import ToolProviderCredentialValidationError 5 | 6 | 7 | class RsshubProvider(ToolProvider): 8 | def _validate_credentials(self, credentials: dict[str, Any]) -> None: 9 | # RSSHub不需要凭据 10 | pass 11 | -------------------------------------------------------------------------------- /provider/rsshub.yaml: -------------------------------------------------------------------------------- 1 | identity: 2 | author: stvlynn 3 | name: rsshub 4 | label: 5 | en_US: RSSHub 6 | zh_Hans: RSSHub订阅 7 | ja_JP: RSSHub 8 | description: 9 | en_US: Get RSS feeds from RSSHub 10 | zh_Hans: 获取RSSHub的RSS订阅源 11 | ja_JP: RSSHubからRSSフィードを取得 12 | icon: icon.svg 13 | tools: 14 | - tools/custom_route.yaml 15 | - tools/twitter.yaml 16 | - tools/linkedin.yaml 17 | - tools/threads.yaml 18 | - tools/discord.yaml 19 | - tools/google_news.yaml 20 | extra: 21 | python: 22 | source: provider/rsshub.py 23 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | ## Data Collection 4 | 5 | The RSSHub Dify plugin does not collect any personal data. It is only used to fetch publicly available RSS feeds from RSSHub. 6 | 7 | ## Data Usage 8 | 9 | The plugin only passes RSS feeds obtained from RSSHub to Dify and does not store or process any user data. 10 | 11 | ## Third-Party Services 12 | 13 | This plugin relies on the RSSHub service. When using this plugin, your requests will be forwarded to RSSHub servers. Please refer to [RSSHub's Privacy Policy](https://docs.rsshub.app/privacy) for more information. 14 | 15 | ## Contact 16 | 17 | For any privacy-related concerns, please contact the plugin author. -------------------------------------------------------------------------------- /manifest.yaml: -------------------------------------------------------------------------------- 1 | version: 0.0.2 2 | type: plugin 3 | author: stvlynn 4 | name: rsshub 5 | label: 6 | en_US: RSSHub 7 | ja_JP: RSSHub 8 | zh_Hans: RSSHub订阅 9 | description: 10 | en_US: "Get RSS feeds from RSSHub, a flexible and extensible RSS feed aggregator" 11 | ja_JP: "RSSHubからRSSフィードを取得する" 12 | zh_Hans: "从RSSHub获取RSS订阅源,RSSHub是一个灵活可扩展的RSS聚合器" 13 | icon: icon.svg 14 | resource: 15 | memory: 268435456 16 | permission: {} 17 | plugins: 18 | tools: 19 | - provider/rsshub.yaml 20 | meta: 21 | version: 0.0.2 22 | arch: 23 | - amd64 24 | - arm64 25 | runner: 26 | language: python 27 | version: "3.12" 28 | entrypoint: main 29 | created_at: 2025-03-11T22:10:07.655347+08:00 30 | privacy: PRIVACY.md 31 | verified: false 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Steven Lynn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tools/rsshub.yaml: -------------------------------------------------------------------------------- 1 | identity: 2 | name: rsshub 3 | author: stvlynn 4 | label: 5 | en_US: RSSHub 6 | zh_Hans: RSSHub订阅 7 | pt_BR: RSSHub 8 | description: 9 | human: 10 | en_US: Get RSS feeds from RSSHub 11 | zh_Hans: 获取RSSHub的RSS订阅源 12 | pt_BR: Get RSS feeds from RSSHub 13 | llm: "A tool to fetch RSS feeds from RSSHub, a flexible and extensible RSS feed aggregator." 14 | parameters: 15 | - name: base_url 16 | type: string 17 | required: false 18 | default: "https://rsshub.app" 19 | label: 20 | en_US: RSSHub Base URL 21 | zh_Hans: RSSHub基础URL 22 | pt_BR: RSSHub Base URL 23 | human_description: 24 | en_US: "The base URL of RSSHub instance (default is https://rsshub.app)" 25 | zh_Hans: "RSSHub实例的基础URL(默认为https://rsshub.app)" 26 | pt_BR: The base URL of RSSHub instance 27 | llm_description: "The base URL of RSSHub instance (default is https://rsshub.app)" 28 | form: llm 29 | - name: route 30 | type: string 31 | required: true 32 | label: 33 | en_US: RSSHub Route 34 | zh_Hans: RSSHub路由 35 | pt_BR: RSSHub Route 36 | human_description: 37 | en_US: "The RSSHub route path (e.g. /zhihu/hot, /sspai/matrix)" 38 | zh_Hans: "RSSHub的路由路径(例如:/zhihu/hot, /sspai/matrix)" 39 | pt_BR: The RSSHub route path 40 | llm_description: "The RSSHub route path without the base URL (e.g. /zhihu/hot, /sspai/matrix)" 41 | form: llm 42 | - name: limit 43 | type: number 44 | required: false 45 | label: 46 | en_US: Item Limit 47 | zh_Hans: 条目数量限制 48 | pt_BR: Item Limit 49 | human_description: 50 | en_US: "Maximum number of items to return (default: 10)" 51 | zh_Hans: "返回的最大条目数量(默认:10)" 52 | pt_BR: Maximum number of items to return 53 | llm_description: "Maximum number of items to return (default: 10)" 54 | form: llm 55 | extra: 56 | python: 57 | source: tools/rsshub.py 58 | -------------------------------------------------------------------------------- /tools/custom_route.yaml: -------------------------------------------------------------------------------- 1 | identity: 2 | name: custom_route 3 | author: stvlynn 4 | label: 5 | en_US: Custom Route 6 | zh_Hans: 自定义路由 7 | ja_JP: カスタムルート 8 | description: 9 | human: 10 | en_US: Get RSS feeds from RSSHub using a custom route 11 | zh_Hans: 使用自定义路由从RSSHub获取RSS订阅源 12 | ja_JP: カスタムルートを使用してRSSHubからRSSフィードを取得 13 | llm: "A tool to fetch RSS feeds from RSSHub using any custom route path." 14 | parameters: 15 | - name: base_url 16 | type: string 17 | required: false 18 | default: "https://rsshub.app" 19 | label: 20 | en_US: RSSHub Base URL 21 | zh_Hans: RSSHub基础URL 22 | ja_JP: RSSHubベースURL 23 | human_description: 24 | en_US: "The base URL of RSSHub instance (default is https://rsshub.app)" 25 | zh_Hans: "RSSHub实例的基础URL(默认为https://rsshub.app)" 26 | ja_JP: "RSSHubインスタンスのベースURL(デフォルトはhttps://rsshub.app)" 27 | llm_description: "The base URL of RSSHub instance (default is https://rsshub.app)" 28 | form: form 29 | - name: access_key 30 | type: string 31 | required: false 32 | label: 33 | en_US: RSSHub Access Key 34 | zh_Hans: RSSHub访问密钥 35 | ja_JP: RSSHubアクセスキー 36 | human_description: 37 | en_US: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 38 | zh_Hans: "RSSHub路由的访问密钥(例如:ILoveRSSHub)。如不需要请留空。" 39 | ja_JP: "RSSHubルートのアクセスキー(例:ILoveRSSHub)。必要ない場合は空のままにしてください。" 40 | llm_description: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 41 | form: form 42 | - name: route 43 | type: string 44 | required: true 45 | label: 46 | en_US: RSSHub Route 47 | zh_Hans: RSSHub路由 48 | ja_JP: RSSHubルート 49 | human_description: 50 | en_US: "The RSSHub route path (e.g. /zhihu/hot, /sspai/matrix)" 51 | zh_Hans: "RSSHub的路由路径(例如:/zhihu/hot, /sspai/matrix)" 52 | ja_JP: "RSSHubのルートパス(例:/zhihu/hot, /sspai/matrix)" 53 | llm_description: "The RSSHub route path without the base URL (e.g. /zhihu/hot, /sspai/matrix)" 54 | form: llm 55 | - name: limit 56 | type: number 57 | required: false 58 | label: 59 | en_US: Item Limit 60 | zh_Hans: 条目数量限制 61 | ja_JP: アイテム数制限 62 | human_description: 63 | en_US: "Maximum number of items to return (default: 10)" 64 | zh_Hans: "返回的最大条目数量(默认:10)" 65 | ja_JP: "返される最大アイテム数(デフォルト:10)" 66 | llm_description: "Maximum number of items to return (default: 10)" 67 | form: llm 68 | extra: 69 | python: 70 | source: tools/rsshub.py 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | .idea/ 169 | 170 | # Vscode 171 | .vscode/ 172 | -------------------------------------------------------------------------------- /.difyignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | share/python-wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | *.py,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | cover/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | db.sqlite3-journal 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | .pybuilder/ 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | # For a library or package, you might want to ignore these files since the code is 84 | # intended to run in multiple environments; otherwise, check them in: 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | Pipfile.lock 93 | 94 | # UV 95 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 96 | # This is especially recommended for binary packages to ensure reproducibility, and is more 97 | # commonly ignored for libraries. 98 | uv.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 113 | .pdm.toml 114 | .pdm-python 115 | .pdm-build/ 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | .idea/ 166 | 167 | # Vscode 168 | .vscode/ 169 | 170 | # Git 171 | .git/ 172 | .gitignore 173 | 174 | # Mac 175 | .DS_Store 176 | 177 | # Windows 178 | Thumbs.db 179 | -------------------------------------------------------------------------------- /tools/rsshub.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from typing import Any 3 | import requests 4 | import html 5 | import re 6 | import xml.etree.ElementTree as ET 7 | from datetime import datetime 8 | from urllib.parse import urljoin, urlparse, parse_qs, urlencode 9 | 10 | from dify_plugin import Tool 11 | from dify_plugin.entities.tool import ToolInvokeMessage 12 | 13 | class RsshubTool(Tool): 14 | def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: 15 | # 获取参数 16 | route = tool_parameters.get("route", "") 17 | limit = int(tool_parameters.get("limit", 10)) 18 | base_url = tool_parameters.get("base_url", "https://rsshub.app") 19 | access_key = tool_parameters.get("access_key", "") 20 | 21 | # 确保route以/开头 22 | if not route.startswith("/"): 23 | route = "/" + route 24 | 25 | # 确保base_url以/结尾 26 | if not base_url.endswith("/"): 27 | base_url = base_url + "/" 28 | 29 | # 构建完整URL 30 | url = urljoin(base_url, route) 31 | 32 | # 如果提供了访问密钥,添加到URL中 33 | if access_key: 34 | # 解析URL 35 | parsed_url = urlparse(url) 36 | # 获取现有查询参数 37 | query_params = parse_qs(parsed_url.query) 38 | # 添加key参数 39 | query_params['key'] = [access_key] 40 | # 重新构建查询字符串 41 | new_query = urlencode(query_params, doseq=True) 42 | # 替换URL中的查询部分 43 | url_parts = list(parsed_url) 44 | url_parts[4] = new_query 45 | # 重新组合URL 46 | url = urljoin(base_url, route + "?" + new_query) 47 | 48 | try: 49 | # 获取RSS源 50 | response = requests.get(url, timeout=10) 51 | response.raise_for_status() 52 | 53 | # 解析XML 54 | root = ET.fromstring(response.content) 55 | 56 | # 获取命名空间 57 | namespaces = {'': root.tag.split('}')[0].strip('{')} if '}' in root.tag else {} 58 | 59 | # 提取标题和描述 60 | channel = root.find('.//channel', namespaces) 61 | if channel is None: 62 | channel = root 63 | 64 | title = channel.findtext('./title', '未知标题', namespaces) 65 | description = channel.findtext('./description', '无描述', namespaces) 66 | 67 | # 提取条目 68 | entries = [] 69 | items = channel.findall('./item', namespaces) 70 | 71 | for item in items[:limit]: 72 | item_title = item.findtext('./title', '无标题', namespaces) 73 | item_link = item.findtext('./link', '', namespaces) 74 | item_pubDate = item.findtext('./pubDate', '', namespaces) 75 | item_description = item.findtext('./description', '', namespaces) 76 | 77 | # 清理HTML标签 78 | clean_content = re.sub(r'<.*?>', '', item_description) 79 | clean_content = html.unescape(clean_content) 80 | 81 | # 格式化日期 82 | published = item_pubDate 83 | try: 84 | if published: 85 | dt = datetime.strptime(published, '%a, %d %b %Y %H:%M:%S %Z') 86 | published = dt.strftime('%Y-%m-%d %H:%M:%S') 87 | except: 88 | # 如果日期解析失败,保持原样 89 | pass 90 | 91 | entries.append({ 92 | 'title': item_title, 93 | 'link': item_link, 94 | 'published': published, 95 | 'content': clean_content[:500] + ('...' if len(clean_content) > 500 else '') 96 | }) 97 | 98 | # 返回结果 99 | result = { 100 | "title": title, 101 | "description": description, 102 | "url": url, 103 | "entries": entries 104 | } 105 | 106 | yield self.create_json_message(result) 107 | 108 | except Exception as e: 109 | yield self.create_text_message(f"获取RSS源失败: {str(e)}") 110 | -------------------------------------------------------------------------------- /tools/google_news.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import urllib.parse 4 | from typing import Dict, Any, List, Generator 5 | 6 | import requests 7 | from pydantic import BaseModel, Field 8 | 9 | from dify_plugin import Tool 10 | from dify_plugin.entities.tool import ToolInvokeMessage 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class GoogleNewsRequest(BaseModel): 16 | base_url: str = Field(default="https://rsshub.app", description="RSSHub base URL") 17 | access_key: str = Field(default="", description="Access key for RSSHub routes") 18 | category: str = Field(..., description="Category title of Google News") 19 | language_code: str = Field(..., description="Language code for Google News content") 20 | country_code: str = Field(..., description="Country or region code for Google News content") 21 | country_edition: str = Field(..., description="Country edition for Google News") 22 | limit: int = Field(default=10, description="Maximum number of items to return") 23 | 24 | 25 | class GoogleNewsTool(Tool): 26 | def _invoke(self, tool_parameters: Dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: 27 | """Process the request and return the response.""" 28 | try: 29 | req = GoogleNewsRequest(**tool_parameters) 30 | 31 | # Encode category for URL 32 | encoded_category = urllib.parse.quote(req.category) 33 | 34 | # Construct the locale parameter 35 | locale = f"hl={req.language_code}&gl={req.country_code}&ceid={req.country_edition}" 36 | 37 | # Construct the RSSHub URL for Google News 38 | route = f"/google/news/{encoded_category}/{locale}" 39 | 40 | # 确保route以/开头 41 | if not route.startswith("/"): 42 | route = "/" + route 43 | 44 | # 确保base_url以/结尾 45 | base_url = req.base_url 46 | if not base_url.endswith("/"): 47 | base_url = base_url + "/" 48 | 49 | # 构建完整URL 50 | url = base_url.rstrip("/") + route 51 | 52 | # 如果提供了访问密钥,添加到URL中 53 | if req.access_key: 54 | # 检查URL是否已经有查询参数 55 | if "?" in url: 56 | url += f"&key={req.access_key}" 57 | else: 58 | url += f"?key={req.access_key}" 59 | 60 | logger.info(f"Fetching Google News from: {url}") 61 | 62 | # 获取RSS源 63 | response = requests.get(url, timeout=10) 64 | response.raise_for_status() 65 | 66 | # 解析XML 67 | import xml.etree.ElementTree as ET 68 | root = ET.fromstring(response.content) 69 | 70 | # 获取命名空间 71 | namespaces = {'': root.tag.split('}')[0].strip('{')} if '}' in root.tag else {} 72 | 73 | # 提取标题和描述 74 | channel = root.find('.//channel', namespaces) 75 | if channel is None: 76 | channel = root 77 | 78 | title = channel.findtext('./title', '未知标题', namespaces) 79 | description = channel.findtext('./description', '无描述', namespaces) 80 | 81 | # 提取条目 82 | items = channel.findall('./item', namespaces) 83 | 84 | # 限制条目数量 85 | if len(items) > req.limit: 86 | items = items[:req.limit] 87 | 88 | # 构建结果 89 | result = { 90 | "title": title, 91 | "description": description, 92 | "url": url, 93 | "items": [] 94 | } 95 | 96 | for item in items: 97 | item_title = item.findtext('./title', '', namespaces) 98 | item_link = item.findtext('./link', '', namespaces) 99 | item_description = item.findtext('./description', '', namespaces) 100 | item_pubDate = item.findtext('./pubDate', '', namespaces) 101 | 102 | result["items"].append({ 103 | "title": item_title, 104 | "link": item_link, 105 | "description": item_description, 106 | "pubDate": item_pubDate 107 | }) 108 | 109 | # 使用Tool类中的辅助方法创建JSON消息 110 | yield self.create_json_message(result) 111 | 112 | except Exception as e: 113 | logger.error(f"Error fetching Google News: {str(e)}") 114 | error_message = f"Error fetching Google News: {str(e)}" 115 | # 使用Tool类中的辅助方法创建文本消息 116 | yield self.create_text_message(error_message) 117 | 118 | 119 | if __name__ == "__main__": 120 | # 注意:这里只是为了测试,实际运行时Tool类会由Dify框架初始化 121 | print("This is a test module. Run the main.py file to start the plugin.") -------------------------------------------------------------------------------- /tools/google_news.yaml: -------------------------------------------------------------------------------- 1 | identity: 2 | name: google_news 3 | author: stvlynn 4 | label: 5 | en_US: Google News 6 | zh_Hans: Google 新闻 7 | ja_JP: Google ニュース 8 | description: 9 | human: 10 | en_US: Get RSS feeds from Google News via RSSHub 11 | zh_Hans: 通过RSSHub获取Google新闻的RSS源 12 | ja_JP: RSSHubを通じてGoogleニュースのRSSフィードを取得 13 | llm: "A tool to fetch RSS feeds from Google News using RSSHub as a proxy." 14 | parameters: 15 | - name: base_url 16 | type: string 17 | required: false 18 | default: https://rsshub.app 19 | label: 20 | en_US: RSSHub Base URL 21 | zh_Hans: RSSHub基础URL 22 | ja_JP: RSSHubベースURL 23 | human_description: 24 | en_US: The base URL of RSSHub instance 25 | zh_Hans: RSSHub实例的基础URL 26 | ja_JP: RSSHubインスタンスのベースURL 27 | llm_description: "The base URL of the RSSHub instance to use" 28 | form: form 29 | 30 | - name: access_key 31 | type: string 32 | required: false 33 | label: 34 | en_US: RSSHub Access Key 35 | zh_Hans: RSSHub访问密钥 36 | ja_JP: RSSHubアクセスキー 37 | human_description: 38 | en_US: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 39 | zh_Hans: "RSSHub路由的访问密钥(例如:ILoveRSSHub)。如不需要请留空。" 40 | ja_JP: "RSSHubルートのアクセスキー(例:ILoveRSSHub)。必要ない場合は空のままにしてください。" 41 | llm_description: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 42 | form: form 43 | 44 | - name: category 45 | type: string 46 | required: true 47 | label: 48 | en_US: Category Title 49 | zh_Hans: 分类标题 50 | ja_JP: カテゴリタイトル 51 | human_description: 52 | en_US: The category title of Google News, e.g. 'Top stories', 'World', 'Business' 53 | zh_Hans: Google新闻的分类标题,例如'Top stories'、'World'、'Business' 54 | ja_JP: Googleニュースのカテゴリタイトル、例:'Top stories'、'World'、'Business' 55 | llm_description: "The category title of Google News, such as 'Top stories', 'World', 'Business'" 56 | form: form 57 | 58 | - name: language_code 59 | type: select 60 | required: true 61 | default: "en-US" 62 | label: 63 | en_US: Language 64 | zh_Hans: 语言 65 | ja_JP: 言語 66 | human_description: 67 | en_US: The language code for Google News content 68 | zh_Hans: Google新闻内容的语言代码 69 | ja_JP: Googleニュースコンテンツの言語コード 70 | llm_description: "The language code for Google News content (hl parameter)" 71 | form: form 72 | options: 73 | - value: en-US 74 | label: 75 | en_US: English (US) 76 | zh_Hans: 英语(美国) 77 | ja_JP: 英語(米国) 78 | - value: zh-CN 79 | label: 80 | en_US: Chinese (Simplified) 81 | zh_Hans: 中文(简体) 82 | ja_JP: 中国語(簡体字) 83 | - value: ja-JP 84 | label: 85 | en_US: Japanese 86 | zh_Hans: 日语 87 | ja_JP: 日本語 88 | - value: fr-FR 89 | label: 90 | en_US: French 91 | zh_Hans: 法语 92 | ja_JP: フランス語 93 | - value: de-DE 94 | label: 95 | en_US: German 96 | zh_Hans: 德语 97 | ja_JP: ドイツ語 98 | - value: es-ES 99 | label: 100 | en_US: Spanish 101 | zh_Hans: 西班牙语 102 | ja_JP: スペイン語 103 | 104 | - name: country_code 105 | type: select 106 | required: true 107 | default: "US" 108 | label: 109 | en_US: Country/Region 110 | zh_Hans: 国家/地区 111 | ja_JP: 国/地域 112 | human_description: 113 | en_US: The country or region code for Google News content 114 | zh_Hans: Google新闻内容的国家或地区代码 115 | ja_JP: Googleニュースコンテンツの国または地域コード 116 | llm_description: "The country or region code for Google News content (gl parameter)" 117 | form: form 118 | options: 119 | - value: US 120 | label: 121 | en_US: United States 122 | zh_Hans: 美国 123 | ja_JP: アメリカ合衆国 124 | - value: CN 125 | label: 126 | en_US: China 127 | zh_Hans: 中国 128 | ja_JP: 中国 129 | - value: JP 130 | label: 131 | en_US: Japan 132 | zh_Hans: 日本 133 | ja_JP: 日本 134 | - value: FR 135 | label: 136 | en_US: France 137 | zh_Hans: 法国 138 | ja_JP: フランス 139 | - value: DE 140 | label: 141 | en_US: Germany 142 | zh_Hans: 德国 143 | ja_JP: ドイツ 144 | - value: GB 145 | label: 146 | en_US: United Kingdom 147 | zh_Hans: 英国 148 | ja_JP: イギリス 149 | 150 | - name: country_edition 151 | type: string 152 | required: true 153 | default: "US:en" 154 | label: 155 | en_US: Country Edition 156 | zh_Hans: 国家版本 157 | ja_JP: 国別エディション 158 | human_description: 159 | en_US: The country edition for Google News, usually in format 'COUNTRY:LANGUAGE', e.g. 'US:en', 'CN:zh' 160 | zh_Hans: Google新闻的国家版本,通常格式为'国家:语言',例如'US:en'、'CN:zh' 161 | ja_JP: Googleニュースの国別エディション、通常'国:言語'の形式、例:'US:en'、'JP:ja' 162 | llm_description: "The country edition for Google News (ceid parameter)" 163 | form: form 164 | 165 | - name: limit 166 | type: number 167 | required: false 168 | default: 10 169 | label: 170 | en_US: Item Limit 171 | zh_Hans: 条目数量限制 172 | ja_JP: アイテム数制限 173 | human_description: 174 | en_US: Maximum number of items to return 175 | zh_Hans: 返回的最大条目数 176 | ja_JP: 返回する最大アイテム数 177 | llm_description: "Maximum number of items to return in the RSS feed" 178 | form: form 179 | extra: 180 | python: 181 | source: tools/google_news.py -------------------------------------------------------------------------------- /tools/twitter.yaml: -------------------------------------------------------------------------------- 1 | identity: 2 | name: twitter 3 | author: stvlynn 4 | label: 5 | en_US: X (Twitter) 6 | zh_Hans: X (Twitter) 7 | ja_JP: X (Twitter) 8 | description: 9 | human: 10 | en_US: Get RSS feeds from X (Twitter) via RSSHub 11 | zh_Hans: 通过RSSHub获取X (Twitter)的RSS订阅源 12 | ja_JP: RSSHubを通じてX (Twitter)のRSSフィードを取得 13 | llm: "A tool to fetch RSS feeds from X (Twitter) using RSSHub as a proxy." 14 | parameters: 15 | - name: route_type 16 | type: select 17 | required: true 18 | default: "user" 19 | label: 20 | en_US: Route Type 21 | zh_Hans: 路由类型 22 | ja_JP: ルートタイプ 23 | human_description: 24 | en_US: "The type of Twitter route to use" 25 | zh_Hans: "要使用的Twitter路由类型" 26 | ja_JP: "使用するTwitterルートのタイプ" 27 | llm_description: "The type of Twitter route to use" 28 | form: form 29 | options: 30 | - value: user 31 | label: 32 | en_US: User Timeline 33 | zh_Hans: 用户时间线 34 | ja_JP: ユーザータイムライン 35 | - value: keyword 36 | label: 37 | en_US: Keyword Search 38 | zh_Hans: 关键词搜索 39 | ja_JP: キーワード検索 40 | - value: list 41 | label: 42 | en_US: List Timeline 43 | zh_Hans: 列表时间线 44 | ja_JP: リストタイムライン 45 | - value: home 46 | label: 47 | en_US: Home Timeline 48 | zh_Hans: 主页时间线 49 | ja_JP: ホームタイムライン 50 | - value: home_latest 51 | label: 52 | en_US: Latest Home Timeline 53 | zh_Hans: 最新主页时间线 54 | ja_JP: 最新ホームタイムライン 55 | - value: likes 56 | label: 57 | en_US: User Likes 58 | zh_Hans: 用户喜欢的推文 59 | ja_JP: ユーザーのいいね 60 | - value: media 61 | label: 62 | en_US: User Media 63 | zh_Hans: 用户媒体推文 64 | ja_JP: ユーザーメディア 65 | - name: base_url 66 | type: string 67 | required: false 68 | default: "https://rsshub.app" 69 | label: 70 | en_US: RSSHub Base URL 71 | zh_Hans: RSSHub基础URL 72 | ja_JP: RSSHubベースURL 73 | human_description: 74 | en_US: "The base URL of RSSHub instance (default is https://rsshub.app)" 75 | zh_Hans: "RSSHub实例的基础URL(默认为https://rsshub.app)" 76 | ja_JP: "RSSHubインスタンスのベースURL(デフォルトはhttps://rsshub.app)" 77 | llm_description: "The base URL of RSSHub instance (default is https://rsshub.app)" 78 | form: form 79 | - name: access_key 80 | type: string 81 | required: false 82 | label: 83 | en_US: RSSHub Access Key 84 | zh_Hans: RSSHub访问密钥 85 | ja_JP: RSSHubアクセスキー 86 | human_description: 87 | en_US: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 88 | zh_Hans: "RSSHub路由的访问密钥(例如:ILoveRSSHub)。如不需要请留空。" 89 | ja_JP: "RSSHubルートのアクセスキー(例:ILoveRSSHub)。必要ない場合は空のままにしてください。" 90 | llm_description: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 91 | form: form 92 | - name: username 93 | type: string 94 | required: false 95 | label: 96 | en_US: Twitter Username 97 | zh_Hans: Twitter用户名 98 | ja_JP: Twitterユーザー名 99 | human_description: 100 | en_US: "The Twitter username without @ (e.g. elonmusk). Required for user, likes, and media routes." 101 | zh_Hans: "Twitter用户名,不包含@符号(例如:elonmusk)。用于用户、喜欢和媒体路由。" 102 | ja_JP: "@なしのTwitterユーザー名(例:elonmusk)。ユーザー、いいね、メディアルートに必要。" 103 | llm_description: "The Twitter username without @ (e.g. elonmusk). Required for user, likes, and media routes." 104 | form: llm 105 | - name: keyword 106 | type: string 107 | required: false 108 | label: 109 | en_US: Keyword 110 | zh_Hans: 关键词 111 | ja_JP: キーワード 112 | human_description: 113 | en_US: "The keyword to search for. Required for keyword route." 114 | zh_Hans: "要搜索的关键词。关键词路由必填。" 115 | ja_JP: "検索するキーワード。キーワードルートに必要。" 116 | llm_description: "The keyword to search for. Required for keyword route." 117 | form: llm 118 | - name: list_id 119 | type: string 120 | required: false 121 | label: 122 | en_US: List ID 123 | zh_Hans: 列表ID 124 | ja_JP: リストID 125 | human_description: 126 | en_US: "The Twitter list ID. Required for list route." 127 | zh_Hans: "Twitter列表ID。列表路由必填。" 128 | ja_JP: "TwitterリストID。リストルートに必要。" 129 | llm_description: "The Twitter list ID. Required for list route." 130 | form: llm 131 | - name: exclude_replies 132 | type: boolean 133 | required: false 134 | default: false 135 | label: 136 | en_US: Exclude Replies 137 | zh_Hans: 排除回复 138 | ja_JP: 返信を除外 139 | human_description: 140 | en_US: "Whether to exclude replies from the timeline" 141 | zh_Hans: "是否从时间线中排除回复" 142 | ja_JP: "タイムラインから返信を除外するかどうか" 143 | llm_description: "Whether to exclude replies from the timeline" 144 | form: form 145 | - name: exclude_rts 146 | type: boolean 147 | required: false 148 | default: false 149 | label: 150 | en_US: Exclude Retweets 151 | zh_Hans: 排除转发 152 | ja_JP: リツイートを除外 153 | human_description: 154 | en_US: "Whether to exclude retweets from the timeline" 155 | zh_Hans: "是否从时间线中排除转发" 156 | ja_JP: "タイムラインからリツイートを除外するかどうか" 157 | llm_description: "Whether to exclude retweets from the timeline" 158 | form: form 159 | - name: limit 160 | type: number 161 | required: false 162 | label: 163 | en_US: Item Limit 164 | zh_Hans: 条目数量限制 165 | ja_JP: アイテム数制限 166 | human_description: 167 | en_US: "Maximum number of items to return (default: 10)" 168 | zh_Hans: "返回的最大条目数量(默认:10)" 169 | ja_JP: "返される最大アイテム数(デフォルト:10)" 170 | llm_description: "Maximum number of items to return (default: 10)" 171 | form: llm 172 | extra: 173 | python: 174 | source: tools/twitter.py -------------------------------------------------------------------------------- /tools/discord.yaml: -------------------------------------------------------------------------------- 1 | identity: 2 | name: discord 3 | author: stvlynn 4 | label: 5 | en_US: Discord 6 | zh_Hans: Discord 7 | ja_JP: Discord 8 | description: 9 | human: 10 | en_US: Get RSS feeds from Discord via RSSHub 11 | zh_Hans: 通过RSSHub获取Discord的RSS订阅源 12 | ja_JP: RSSHubを通じてDiscordのRSSフィードを取得 13 | llm: "A tool to fetch RSS feeds from Discord using RSSHub as a proxy." 14 | parameters: 15 | - name: route_type 16 | type: select 17 | required: true 18 | default: "channel" 19 | label: 20 | en_US: Route Type 21 | zh_Hans: 路由类型 22 | ja_JP: ルートタイプ 23 | human_description: 24 | en_US: "The type of Discord route to use" 25 | zh_Hans: "要使用的Discord路由类型" 26 | ja_JP: "使用するDiscordルートのタイプ" 27 | llm_description: "The type of Discord route to use" 28 | form: form 29 | options: 30 | - value: channel 31 | label: 32 | en_US: Channel Messages 33 | zh_Hans: 频道消息 34 | ja_JP: チャンネルメッセージ 35 | - value: search 36 | label: 37 | en_US: Guild Search 38 | zh_Hans: 服务器搜索 39 | ja_JP: サーバー検索 40 | - name: base_url 41 | type: string 42 | required: false 43 | default: "https://rsshub.app" 44 | label: 45 | en_US: RSSHub Base URL 46 | zh_Hans: RSSHub基础URL 47 | ja_JP: RSSHubベースURL 48 | human_description: 49 | en_US: "The base URL of RSSHub instance (default is https://rsshub.app)" 50 | zh_Hans: "RSSHub实例的基础URL(默认为https://rsshub.app)" 51 | ja_JP: "RSSHubインスタンスのベースURL(デフォルトはhttps://rsshub.app)" 52 | llm_description: "The base URL of RSSHub instance (default is https://rsshub.app)" 53 | form: form 54 | - name: access_key 55 | type: string 56 | required: false 57 | label: 58 | en_US: RSSHub Access Key 59 | zh_Hans: RSSHub访问密钥 60 | ja_JP: RSSHubアクセスキー 61 | human_description: 62 | en_US: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 63 | zh_Hans: "RSSHub路由的访问密钥(例如:ILoveRSSHub)。如不需要请留空。" 64 | ja_JP: "RSSHubルートのアクセスキー(例:ILoveRSSHub)。必要ない場合は空のままにしてください。" 65 | llm_description: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 66 | form: form 67 | - name: channel_id 68 | type: string 69 | required: false 70 | label: 71 | en_US: Channel ID 72 | zh_Hans: 频道ID 73 | ja_JP: チャンネルID 74 | human_description: 75 | en_US: "The ID of Discord channel (required for Channel Messages route type)" 76 | zh_Hans: "Discord频道的ID(频道消息路由类型必填)" 77 | ja_JP: "DiscordチャンネルのID(チャンネルメッセージルートタイプに必要)" 78 | llm_description: "The ID of Discord channel (required for Channel Messages route type)" 79 | form: llm 80 | - name: guild_id 81 | type: string 82 | required: false 83 | label: 84 | en_US: Guild ID 85 | zh_Hans: 服务器ID 86 | ja_JP: サーバーID 87 | human_description: 88 | en_US: "The ID of Discord guild/server (required for Guild Search route type)" 89 | zh_Hans: "Discord服务器的ID(服务器搜索路由类型必填)" 90 | ja_JP: "Discordサーバーの ID(サーバー検索ルートタイプに必要)" 91 | llm_description: "The ID of Discord guild/server (required for Guild Search route type)" 92 | form: llm 93 | - name: search_params 94 | type: string 95 | required: false 96 | label: 97 | en_US: Search Parameters 98 | zh_Hans: 搜索参数 99 | ja_JP: 検索パラメータ 100 | human_description: 101 | en_US: "Search parameters for Guild Search route type (e.g. content=friendly&has=image,video)" 102 | zh_Hans: "服务器搜索路由类型的搜索参数(例如:content=friendly&has=image,video)" 103 | ja_JP: "サーバー検索ルートタイプの検索パラメータ(例:content=friendly&has=image,video)" 104 | llm_description: "Search parameters for Guild Search route type (e.g. content=friendly&has=image,video). Supports content, author_id, mentions, has, min_id, max_id, channel_id, pinned." 105 | form: llm 106 | - name: discord_authorization 107 | type: string 108 | required: false 109 | label: 110 | en_US: Discord Authorization 111 | zh_Hans: Discord授权令牌 112 | ja_JP: Discord認証トークン 113 | human_description: 114 | en_US: "Discord authorization header from the browser (required for both route types)" 115 | zh_Hans: "从浏览器获取的Discord授权头(两种路由类型都需要)" 116 | ja_JP: "ブラウザから取得したDiscord認証ヘッダー(両方のルートタイプに必要)" 117 | llm_description: "Discord authorization header from the browser (required for both route types)" 118 | form: llm 119 | - name: limit 120 | type: number 121 | required: false 122 | default: 10 123 | label: 124 | en_US: Item Limit 125 | zh_Hans: 条目数量限制 126 | ja_JP: アイテム数制限 127 | human_description: 128 | en_US: "Maximum number of items to return (default: 10)" 129 | zh_Hans: "返回的最大条目数量(默认:10)" 130 | ja_JP: "返される最大アイテム数(デフォルト:10)" 131 | llm_description: "Maximum number of items to return (default: 10)" 132 | form: llm 133 | extra: 134 | python: 135 | source: tools/discord.py 136 | output_variables: 137 | - name: title 138 | type: string 139 | description: 140 | en_US: The title of the RSS feed 141 | zh_Hans: RSS订阅源的标题 142 | ja_JP: RSSフィードのタイトル 143 | - name: description 144 | type: string 145 | description: 146 | en_US: The description of the RSS feed 147 | zh_Hans: RSS订阅源的描述 148 | ja_JP: RSSフィードの説明 149 | - name: url 150 | type: string 151 | description: 152 | en_US: The URL of the RSS feed 153 | zh_Hans: RSS订阅源的URL 154 | ja_JP: RSSフィードのURL 155 | - name: entries 156 | type: array 157 | description: 158 | en_US: The entries of the RSS feed 159 | zh_Hans: RSS订阅源的条目 160 | ja_JP: RSSフィードのエントリー 161 | - name: route_info 162 | type: object 163 | description: 164 | en_US: Information about the Discord route 165 | zh_Hans: Discord路由的信息 166 | ja_JP: Discordルートに関する情報 -------------------------------------------------------------------------------- /tools/linkedin.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from typing import Any 3 | import requests 4 | import html 5 | import re 6 | import xml.etree.ElementTree as ET 7 | from datetime import datetime 8 | from urllib.parse import urljoin, quote, urlparse, parse_qs, urlencode 9 | 10 | from dify_plugin import Tool 11 | from dify_plugin.entities.tool import ToolInvokeMessage 12 | 13 | class LinkedInTool(Tool): 14 | def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: 15 | # 获取参数 16 | route_type = tool_parameters.get("route_type", "jobs") 17 | keywords = tool_parameters.get("keywords", "") 18 | job_types = tool_parameters.get("job_types", "all") 19 | exp_levels = tool_parameters.get("exp_levels", "all") 20 | work_type = tool_parameters.get("work_type", "0") 21 | time_posted = tool_parameters.get("time_posted", "0") 22 | geo_id = tool_parameters.get("geo_id", "") 23 | limit = int(tool_parameters.get("limit", 10)) 24 | base_url = tool_parameters.get("base_url", "https://rsshub.app") 25 | access_key = tool_parameters.get("access_key", "") 26 | 27 | # 确保base_url以/结尾 28 | if not base_url.endswith("/"): 29 | base_url = base_url + "/" 30 | 31 | # 根据路由类型构建不同的路由 32 | route = "" 33 | 34 | if route_type == "jobs": 35 | # 构建jobs路由 36 | route = f"/linkedin/jobs/{job_types}/{exp_levels}" 37 | 38 | # 添加关键词(如果有) 39 | if keywords: 40 | encoded_keywords = quote(keywords) 41 | route = f"{route}/{encoded_keywords}" 42 | 43 | # 添加额外参数 44 | params = [] 45 | 46 | # 添加工作方式 47 | if work_type != "0": 48 | params.append(f"f_WT={work_type}") 49 | 50 | # 添加发布时间范围 51 | if time_posted != "0": 52 | params.append(f"f_TPR={time_posted}") 53 | 54 | # 添加地理位置ID 55 | if geo_id: 56 | params.append(f"geoId={geo_id}") 57 | 58 | # 添加访问密钥 59 | if access_key: 60 | params.append(f"key={access_key}") 61 | 62 | # 将参数添加到路由中 63 | if params: 64 | route = f"{route}/?{'&'.join(params)}" 65 | 66 | else: 67 | yield self.create_text_message(f"不支持的路由类型: {route_type}") 68 | return 69 | 70 | # 构建完整URL 71 | url = urljoin(base_url, route) 72 | 73 | try: 74 | # 获取RSS源 75 | response = requests.get(url, timeout=10) 76 | response.raise_for_status() 77 | 78 | # 解析XML 79 | root = ET.fromstring(response.content) 80 | 81 | # 获取命名空间 82 | namespaces = {'': root.tag.split('}')[0].strip('{')} if '}' in root.tag else {} 83 | 84 | # 提取标题和描述 85 | channel = root.find('.//channel', namespaces) 86 | if channel is None: 87 | channel = root 88 | 89 | # 根据路由类型设置默认标题 90 | default_title = "" 91 | if route_type == "jobs": 92 | default_title = f"LinkedIn 职位搜索" 93 | if keywords: 94 | default_title += f": {keywords}" 95 | 96 | title = channel.findtext('./title', default_title, namespaces) 97 | description = channel.findtext('./description', f"LinkedIn RSS 源", namespaces) 98 | 99 | # 提取条目 100 | entries = [] 101 | items = channel.findall('./item', namespaces) 102 | 103 | for item in items[:limit]: 104 | item_title = item.findtext('./title', '无标题', namespaces) 105 | item_link = item.findtext('./link', '', namespaces) 106 | item_pubDate = item.findtext('./pubDate', '', namespaces) 107 | item_description = item.findtext('./description', '', namespaces) 108 | 109 | # 清理HTML标签 110 | clean_content = re.sub(r'<.*?>', '', item_description) 111 | clean_content = html.unescape(clean_content) 112 | 113 | # 格式化日期 114 | published = item_pubDate 115 | try: 116 | if published: 117 | dt = datetime.strptime(published, '%a, %d %b %Y %H:%M:%S %Z') 118 | published = dt.strftime('%Y-%m-%d %H:%M:%S') 119 | except: 120 | # 如果日期解析失败,保持原样 121 | pass 122 | 123 | entries.append({ 124 | 'title': item_title, 125 | 'link': item_link, 126 | 'published': published, 127 | 'content': clean_content[:500] + ('...' if len(clean_content) > 500 else '') 128 | }) 129 | 130 | # 返回结果 131 | result = { 132 | "title": title, 133 | "description": description, 134 | "url": url, 135 | "entries": entries, 136 | "route_type": route_type, 137 | "filters": { 138 | "job_types": job_types, 139 | "exp_levels": exp_levels, 140 | "work_type": work_type if work_type != "0" else None, 141 | "time_posted": time_posted if time_posted != "0" else None, 142 | "geo_id": geo_id if geo_id else None 143 | } 144 | } 145 | 146 | yield self.create_json_message(result) 147 | 148 | except Exception as e: 149 | error_message = f"获取LinkedIn RSS源失败: {str(e)}" 150 | yield self.create_text_message(error_message) 151 | -------------------------------------------------------------------------------- /tools/threads.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from typing import Any 3 | import requests 4 | import html 5 | import re 6 | import xml.etree.ElementTree as ET 7 | from datetime import datetime 8 | from urllib.parse import urljoin, quote, urlencode, parse_qs, urlparse 9 | 10 | from dify_plugin import Tool 11 | from dify_plugin.entities.tool import ToolInvokeMessage 12 | 13 | class ThreadsTool(Tool): 14 | def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: 15 | # 获取参数 16 | route_type = tool_parameters.get("route_type", "user") 17 | username = tool_parameters.get("username", "") 18 | base_url = tool_parameters.get("base_url", "https://rsshub.app") 19 | access_key = tool_parameters.get("access_key", "") 20 | show_author_in_title = tool_parameters.get("show_author_in_title", True) 21 | show_author_in_desc = tool_parameters.get("show_author_in_desc", True) 22 | show_quoted_in_title = tool_parameters.get("show_quoted_in_title", True) 23 | show_emoji_for_quotes_and_reply = tool_parameters.get("show_emoji_for_quotes_and_reply", True) 24 | replies = tool_parameters.get("replies", True) 25 | show_author_avatar_in_desc = tool_parameters.get("show_author_avatar_in_desc", False) 26 | show_quoted_author_avatar_in_desc = tool_parameters.get("show_quoted_author_avatar_in_desc", False) 27 | limit = int(tool_parameters.get("limit", 10)) 28 | 29 | # 验证必要参数 30 | if not username: 31 | yield self.create_text_message("Threads用户名不能为空") 32 | return 33 | 34 | # 确保base_url以/结尾 35 | if not base_url.endswith("/"): 36 | base_url = base_url + "/" 37 | 38 | # 构建路由和查询参数 39 | route = "" 40 | query_params = {} 41 | 42 | if route_type == "user": 43 | route = f"/threads/{username}" 44 | 45 | # 添加显示选项 46 | query_params["showAuthorInTitle"] = "true" if show_author_in_title else "false" 47 | query_params["showAuthorInDesc"] = "true" if show_author_in_desc else "false" 48 | query_params["showQuotedInTitle"] = "true" if show_quoted_in_title else "false" 49 | query_params["showEmojiForQuotesAndReply"] = "true" if show_emoji_for_quotes_and_reply else "false" 50 | query_params["replies"] = "true" if replies else "false" 51 | query_params["showAuthorAvatarInDesc"] = "true" if show_author_avatar_in_desc else "false" 52 | query_params["showQuotedAuthorAvatarInDesc"] = "true" if show_quoted_author_avatar_in_desc else "false" 53 | 54 | # 如果提供了访问密钥,添加到查询参数中 55 | if access_key: 56 | query_params["key"] = access_key 57 | else: 58 | yield self.create_text_message(f"不支持的路由类型: {route_type}") 59 | return 60 | 61 | # 构建最终URL 62 | url = urljoin(base_url, route) 63 | if query_params: 64 | url = f"{url}?{urlencode(query_params)}" 65 | 66 | try: 67 | # 获取RSS源 68 | response = requests.get(url, timeout=10) 69 | response.raise_for_status() 70 | 71 | # 解析XML 72 | root = ET.fromstring(response.content) 73 | 74 | # 获取命名空间 75 | namespaces = {'': root.tag.split('}')[0].strip('{')} if '}' in root.tag else {} 76 | 77 | # 提取标题和描述 78 | channel = root.find('.//channel', namespaces) 79 | if channel is None: 80 | channel = root 81 | 82 | title = channel.findtext('./title', f'{username} 的 Threads', namespaces) 83 | description = channel.findtext('./description', f'{username} 的 Threads 订阅源', namespaces) 84 | 85 | # 提取条目 86 | entries = [] 87 | items = channel.findall('./item', namespaces) 88 | 89 | for item in items[:limit]: 90 | item_title = item.findtext('./title', '无标题', namespaces) 91 | item_link = item.findtext('./link', '', namespaces) 92 | item_pubDate = item.findtext('./pubDate', '', namespaces) 93 | item_description = item.findtext('./description', '', namespaces) 94 | 95 | # 清理HTML标签 96 | clean_content = re.sub(r'<.*?>', '', item_description) 97 | clean_content = html.unescape(clean_content) 98 | 99 | # 格式化日期 100 | published = item_pubDate 101 | try: 102 | if published: 103 | dt = datetime.strptime(published, '%a, %d %b %Y %H:%M:%S %Z') 104 | published = dt.strftime('%Y-%m-%d %H:%M:%S') 105 | except: 106 | # 如果日期解析失败,保持原样 107 | pass 108 | 109 | entries.append({ 110 | 'title': item_title, 111 | 'link': item_link, 112 | 'published': published, 113 | 'content': clean_content[:500] + ('...' if len(clean_content) > 500 else '') 114 | }) 115 | 116 | # 返回结果 117 | result = { 118 | "title": title, 119 | "description": description, 120 | "url": url, 121 | "entries": entries, 122 | "username": username, 123 | "display_options": { 124 | "show_author_in_title": show_author_in_title, 125 | "show_author_in_desc": show_author_in_desc, 126 | "show_quoted_in_title": show_quoted_in_title, 127 | "show_emoji_for_quotes_and_reply": show_emoji_for_quotes_and_reply, 128 | "replies": replies, 129 | "show_author_avatar_in_desc": show_author_avatar_in_desc, 130 | "show_quoted_author_avatar_in_desc": show_quoted_author_avatar_in_desc 131 | } 132 | } 133 | 134 | yield self.create_json_message(result) 135 | 136 | except Exception as e: 137 | error_message = f"获取Threads RSS源失败: {str(e)}" 138 | yield self.create_text_message(error_message) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSSHub Dify Plugin 2 | 3 | > **Note**: For comprehensive information about available routes and parameters, please refer to the official [RSSHub Documentation](https://docs.rsshub.app/). 4 | 5 | This is a RSSHub plugin for Dify that allows you to access RSS feeds from RSSHub through Dify. 6 | 7 | # Installation 8 | 9 | Read more in [Release](https://github.com/stvlynn/RSSHub-Dify-Plugin/releases) 10 | 11 | ## Features 12 | 13 | - Get RSS feeds from RSSHub 14 | - Support for custom RSSHub instances 15 | - Limit the number of returned items 16 | - Convenient access to specific services (such as X/Twitter, LinkedIn) 17 | 18 | ## Usage 19 | 20 | 1. Install this plugin in Dify 21 | 2. Use the plugin in your prompts, for example: 22 | 23 | ``` 24 | Use RSSHub to get the latest articles from sspai 25 | ``` 26 | 27 | ## Available Tools 28 | 29 | ### Custom Route 30 | 31 | Get RSS feeds from RSSHub using a custom route. 32 | 33 | **Parameters:** 34 | - `base_url`: The base URL of the RSSHub instance, default is `https://rsshub.app` 35 | - `route`: The RSSHub route path, e.g. `/zhihu/hot` 36 | - `limit`: Maximum number of items to return, default is 10 37 | 38 | ### X (Twitter) 39 | 40 | Get RSS feeds from X (Twitter) via RSSHub. 41 | 42 | ![](./_assets/X.png) 43 | 44 | **Parameters:** 45 | - `route_type`: Twitter route type, with the following options: 46 | - User Timeline 47 | - Keyword Search 48 | - List Timeline 49 | - Home Timeline 50 | - Latest Home Timeline 51 | - User Likes 52 | - User Media 53 | - `base_url`: The base URL of the RSSHub instance, default is `https://rsshub.app` 54 | - `username`: Twitter username without @ (e.g., elonmusk). Required for user timeline, user likes, and user media route types 55 | - `keyword`: The keyword to search for. Required for keyword search route type 56 | - `list_id`: Twitter list ID. Required for list timeline route type 57 | - `exclude_replies`: Whether to exclude replies, default is false. Only applies to user timeline route type 58 | - `exclude_rts`: Whether to exclude retweets, default is false. Only applies to user timeline route type 59 | - `limit`: Maximum number of items to return, default is 10 60 | 61 | ### LinkedIn 62 | 63 | Get RSS feeds from LinkedIn via RSSHub. 64 | 65 | ![](./_assets/linkedin.png) 66 | 67 | **Parameters:** 68 | - `route_type`: LinkedIn route type, with the following options: 69 | - Jobs 70 | - `base_url`: The base URL of the RSSHub instance, default is `https://rsshub.app` 71 | - `keywords`: Job search keywords 72 | - `job_types`: Job types, available options: 73 | - Full Time 74 | - Part Time 75 | - Contractor 76 | - All 77 | - `exp_levels`: Experience levels, available options: 78 | - Internship 79 | - Entry Level 80 | - Associate 81 | - Mid-Senior Level 82 | - Director 83 | - All 84 | - `work_type`: Work type, available options: 85 | - Onsite 86 | - Remote 87 | - Hybrid 88 | - Any 89 | - `time_posted`: Filter by when the job was posted, available options: 90 | - Past 24 hours 91 | - Past week 92 | - Past month 93 | - Any time 94 | - `geo_id`: Geographic location ID (e.g., 91000012 for East Asia) 95 | - `limit`: Maximum number of items to return, default is 10 96 | 97 | ### Threads 98 | 99 | Get RSS feeds from Threads via RSSHub. 100 | 101 | ![](./_assets/threads.png) 102 | 103 | **Parameters:** 104 | - `route_type`: Threads route type, with the following options: 105 | - User 106 | - `base_url`: The base URL of the RSSHub instance, default is `https://rsshub.app` 107 | - `username`: Threads username without @ symbol 108 | - `show_author_in_title`: Show author name in title, default is true 109 | - `show_author_in_desc`: Show author name in description (RSS body), default is true 110 | - `show_quoted_in_title`: Show quoted thread in title, default is true 111 | - `show_emoji_for_quotes_and_reply`: Use 🔁 instead of QT, ↩️ instead of Re, default is true 112 | - `replies`: Include replies, default is true 113 | - `show_author_avatar_in_desc`: Show author avatar in description, default is false (not recommended if your RSS reader extracts images from description) 114 | - `show_quoted_author_avatar_in_desc`: Show quoted author avatar in description, default is false (not recommended if your RSS reader extracts images from description) 115 | - `limit`: Maximum number of items to return, default is 10 116 | 117 | ### Discord 118 | 119 | Get RSS feeds from Discord via RSSHub. 120 | 121 | ![](./_assets/discord.png) 122 | 123 | **Parameters:** 124 | - `route_type`: Discord route type, with the following options: 125 | - Channel Messages 126 | - Guild Search 127 | - `base_url`: The base URL of the RSSHub instance, default is `https://rsshub.app` 128 | - `channel_id`: Discord channel ID, required for Channel Messages route type 129 | - `guild_id`: Discord server ID, required for Guild Search route type 130 | - `search_params`: Search parameters for Guild Search route type (e.g., content=friendly&has=image,video). Supports content, author_id, mentions, has, min_id, max_id, channel_id, pinned, etc. 131 | - `discord_authorization`: Discord authorization header from the browser (required for both route types) 132 | - `limit`: Maximum number of items to return, default is 10 133 | 134 | ### Google News 135 | 136 | Get RSS feeds from Google News via RSSHub. 137 | 138 | ![](./_assets/google_news.png) 139 | 140 | **Parameters:** 141 | - `base_url`: The base URL of the RSSHub instance, default is `https://rsshub.app` 142 | - `category`: Category title of Google News, e.g. 'Top stories', 'World', 'Business' 143 | - `language_code`: Language code for Google News content, e.g. 'en-US', 'zh-CN', 'ja-JP' 144 | - `country_code`: Country or region code for Google News content, e.g. 'US', 'CN', 'JP' 145 | - `country_edition`: Country edition for Google News, usually in format 'COUNTRY:LANGUAGE', e.g. 'US:en', 'CN:zh' 146 | - `limit`: Maximum number of items to return, default is 10 147 | 148 | ## Supported Routes 149 | 150 | RSSHub supports a large number of routes. You can view all supported routes in the [RSSHub Documentation](https://docs.rsshub.app/). 151 | 152 | Here are some commonly used route examples: 153 | 154 | - `/zhihu/hot` - Zhihu Hot List 155 | - `/sspai/matrix` - Sspai Matrix 156 | - `/36kr/hot-list` - 36Kr Hot List 157 | - `/weibo/search/hot` - Weibo Hot Search 158 | 159 | ## Developer 160 | 161 | - [Steven Lynn](https://github.com/stvlynn) 162 | 163 | ## License 164 | 165 | [MIT](./LICENSE) 166 | 167 | ## rsshub 168 | 169 | **Author:** stvlynn 170 | **Version:** 0.0.1 171 | **Type:** tool 172 | 173 | ### Description 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /tools/discord.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from typing import Any 3 | import requests 4 | import html 5 | import re 6 | import xml.etree.ElementTree as ET 7 | from datetime import datetime 8 | from urllib.parse import urljoin, urlparse, parse_qs, urlencode 9 | 10 | from dify_plugin import Tool 11 | from dify_plugin.entities.tool import ToolInvokeMessage 12 | 13 | class DiscordTool(Tool): 14 | def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: 15 | # 获取参数 16 | route_type = tool_parameters.get("route_type", "channel") 17 | channel_id = tool_parameters.get("channel_id", "") 18 | guild_id = tool_parameters.get("guild_id", "") 19 | search_params = tool_parameters.get("search_params", "") 20 | discord_authorization = tool_parameters.get("discord_authorization", "") 21 | limit = int(tool_parameters.get("limit", 10)) 22 | base_url = tool_parameters.get("base_url", "https://rsshub.app") 23 | access_key = tool_parameters.get("access_key", "") 24 | 25 | # 确保base_url以/结尾 26 | if not base_url.endswith("/"): 27 | base_url = base_url + "/" 28 | 29 | # 根据路由类型构建不同的路由 30 | route = "" 31 | 32 | if route_type == "channel": 33 | if not channel_id: 34 | yield self.create_text_message("频道路由需要提供频道ID") 35 | return 36 | 37 | if not discord_authorization: 38 | yield self.create_text_message("需要提供Discord授权令牌") 39 | return 40 | 41 | route = f"/discord/channel/{channel_id}" 42 | 43 | elif route_type == "search": 44 | if not guild_id: 45 | yield self.create_text_message("搜索路由需要提供服务器ID") 46 | return 47 | 48 | if not discord_authorization: 49 | yield self.create_text_message("需要提供Discord授权令牌") 50 | return 51 | 52 | route = f"/discord/search/{guild_id}" 53 | 54 | # 添加搜索参数(如果有) 55 | if search_params: 56 | route = f"{route}/{search_params}" 57 | 58 | else: 59 | yield self.create_text_message(f"不支持的路由类型: {route_type}") 60 | return 61 | 62 | # 构建完整URL 63 | url = urljoin(base_url, route) 64 | 65 | # 如果提供了访问密钥,添加到URL中 66 | if access_key: 67 | # 解析URL 68 | parsed_url = urlparse(url) 69 | # 获取现有查询参数 70 | query_params = parse_qs(parsed_url.query) 71 | # 添加key参数 72 | query_params['key'] = [access_key] 73 | # 重新构建查询字符串 74 | new_query = urlencode(query_params, doseq=True) 75 | # 替换URL中的查询部分 76 | url_parts = list(parsed_url) 77 | url_parts[4] = new_query 78 | # 重新组合URL 79 | url = urljoin(base_url, route + "?" + new_query) 80 | 81 | try: 82 | # 设置请求头 83 | headers = { 84 | "Authorization": discord_authorization 85 | } 86 | 87 | # 获取RSS源 88 | response = requests.get(url, headers=headers, timeout=10) 89 | response.raise_for_status() 90 | 91 | # 解析XML 92 | root = ET.fromstring(response.content) 93 | 94 | # 获取命名空间 95 | namespaces = {'': root.tag.split('}')[0].strip('{')} if '}' in root.tag else {} 96 | 97 | # 提取标题和描述 98 | channel = root.find('.//channel', namespaces) 99 | if channel is None: 100 | channel = root 101 | 102 | # 根据路由类型设置默认标题 103 | default_title = "" 104 | if route_type == "channel": 105 | default_title = f"Discord 频道 {channel_id}" 106 | elif route_type == "search": 107 | default_title = f"Discord 服务器 {guild_id} 搜索结果" 108 | if search_params: 109 | default_title += f": {search_params}" 110 | 111 | title = channel.findtext('./title', default_title, namespaces) 112 | description = channel.findtext('./description', f"Discord RSS 源", namespaces) 113 | 114 | # 提取条目 115 | entries = [] 116 | items = channel.findall('./item', namespaces) 117 | 118 | for item in items[:limit]: 119 | item_title = item.findtext('./title', '无标题', namespaces) 120 | item_link = item.findtext('./link', '', namespaces) 121 | item_pubDate = item.findtext('./pubDate', '', namespaces) 122 | item_description = item.findtext('./description', '', namespaces) 123 | 124 | # 清理HTML标签 125 | clean_content = re.sub(r'<.*?>', '', item_description) 126 | clean_content = html.unescape(clean_content) 127 | 128 | # 格式化日期 129 | published = item_pubDate 130 | try: 131 | if published: 132 | dt = datetime.strptime(published, '%a, %d %b %Y %H:%M:%S %Z') 133 | published = dt.strftime('%Y-%m-%d %H:%M:%S') 134 | except: 135 | # 如果日期解析失败,保持原样 136 | pass 137 | 138 | entries.append({ 139 | 'title': item_title, 140 | 'link': item_link, 141 | 'published': published, 142 | 'content': clean_content[:500] + ('...' if len(clean_content) > 500 else '') 143 | }) 144 | 145 | # 返回结果 146 | result = { 147 | "title": title, 148 | "description": description, 149 | "url": url, 150 | "entries": entries, 151 | "route_type": route_type, 152 | "route_info": { 153 | "channel_id": channel_id if route_type == "channel" else None, 154 | "guild_id": guild_id if route_type == "search" else None, 155 | "search_params": search_params if route_type == "search" and search_params else None 156 | } 157 | } 158 | 159 | yield self.create_json_message(result) 160 | 161 | except Exception as e: 162 | error_message = f"获取Discord RSS源失败: {str(e)}" 163 | yield self.create_text_message(error_message) -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- 1 | ## User Guide of how to develop a Dify Plugin 2 | 3 | Hi there, looks like you have already created a Plugin, now let's get you started with the development! 4 | 5 | ### Choose a Plugin type you want to develop 6 | 7 | Before start, you need some basic knowledge about the Plugin types, Plugin supports to extend the following abilities in Dify: 8 | - **Tool**: Tool Providers like Google Search, Stable Diffusion, etc. it can be used to perform a specific task. 9 | - **Model**: Model Providers like OpenAI, Anthropic, etc. you can use their models to enhance the AI capabilities. 10 | - **Endpoint**: Like Service API in Dify and Ingress in Kubernetes, you can extend a http service as an endpoint and control its logics using your own code. 11 | 12 | Based on the ability you want to extend, we have divided the Plugin into three types: **Tool**, **Model**, and **Extension**. 13 | 14 | - **Tool**: It's a tool provider, but not only limited to tools, you can implement an endpoint there, for example, you need both `Sending Message` and `Receiving Message` if you are building a Discord Bot, **Tool** and **Endpoint** are both required. 15 | - **Model**: Just a model provider, extending others is not allowed. 16 | - **Extension**: Other times, you may only need a simple http service to extend the functionalities, **Extension** is the right choice for you. 17 | 18 | I believe you have chosen the right type for your Plugin while creating it, if not, you can change it later by modifying the `manifest.yaml` file. 19 | 20 | ### Manifest 21 | 22 | Now you can edit the `manifest.yaml` file to describe your Plugin, here is the basic structure of it: 23 | 24 | - version(version, required):Plugin's version 25 | - type(type, required):Plugin's type, currently only supports `plugin`, future support `bundle` 26 | - author(string, required):Author, it's the organization name in Marketplace and should also equals to the owner of the repository 27 | - label(label, required):Multi-language name 28 | - created_at(RFC3339, required):Creation time, Marketplace requires that the creation time must be less than the current time 29 | - icon(asset, required):Icon path 30 | - resource (object):Resources to be applied 31 | - memory (int64):Maximum memory usage, mainly related to resource application on SaaS for serverless, unit bytes 32 | - permission(object):Permission application 33 | - tool(object):Reverse call tool permission 34 | - enabled (bool) 35 | - model(object):Reverse call model permission 36 | - enabled(bool) 37 | - llm(bool) 38 | - text_embedding(bool) 39 | - rerank(bool) 40 | - tts(bool) 41 | - speech2text(bool) 42 | - moderation(bool) 43 | - node(object):Reverse call node permission 44 | - enabled(bool) 45 | - endpoint(object):Allow to register endpoint permission 46 | - enabled(bool) 47 | - app(object):Reverse call app permission 48 | - enabled(bool) 49 | - storage(object):Apply for persistent storage permission 50 | - enabled(bool) 51 | - size(int64):Maximum allowed persistent memory, unit bytes 52 | - plugins(object, required):Plugin extension specific ability yaml file list, absolute path in the plugin package, if you need to extend the model, you need to define a file like openai.yaml, and fill in the path here, and the file on the path must exist, otherwise the packaging will fail. 53 | - Format 54 | - tools(list[string]): Extended tool suppliers, as for the detailed format, please refer to [Tool Guide](https://docs.dify.ai/docs/plugins/standard/tool_provider) 55 | - models(list[string]):Extended model suppliers, as for the detailed format, please refer to [Model Guide](https://docs.dify.ai/docs/plugins/standard/model_provider) 56 | - endpoints(list[string]):Extended Endpoints suppliers, as for the detailed format, please refer to [Endpoint Guide](https://docs.dify.ai/docs/plugins/standard/endpoint_group) 57 | - Restrictions 58 | - Not allowed to extend both tools and models 59 | - Not allowed to have no extension 60 | - Not allowed to extend both models and endpoints 61 | - Currently only supports up to one supplier of each type of extension 62 | - meta(object) 63 | - version(version, required):manifest format version, initial version 0.0.1 64 | - arch(list[string], required):Supported architectures, currently only supports amd64 arm64 65 | - runner(object, required):Runtime configuration 66 | - language(string):Currently only supports python 67 | - version(string):Language version, currently only supports 3.12 68 | - entrypoint(string):Program entry, in python it should be main 69 | 70 | ### Install Dependencies 71 | 72 | - First of all, you need a Python 3.11+ environment, as our SDK requires that. 73 | - Then, install the dependencies: 74 | ```bash 75 | pip install -r requirements.txt 76 | ``` 77 | - If you want to add more dependencies, you can add them to the `requirements.txt` file, once you have set the runner to python in the `manifest.yaml` file, `requirements.txt` will be automatically generated and used for packaging and deployment. 78 | 79 | ### Implement the Plugin 80 | 81 | Now you can start to implement your Plugin, by following these examples, you can quickly understand how to implement your own Plugin: 82 | 83 | - [OpenAI](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/openai): best practice for model provider 84 | - [Google Search](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/google): a simple example for tool provider 85 | - [Neko](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/neko): a funny example for endpoint group 86 | 87 | ### Test and Debug the Plugin 88 | 89 | You may already noticed that a `.env.example` file in the root directory of your Plugin, just copy it to `.env` and fill in the corresponding values, there are some environment variables you need to set if you want to debug your Plugin locally. 90 | 91 | - `INSTALL_METHOD`: Set this to `remote`, your plugin will connect to a Dify instance through the network. 92 | - `REMOTE_INSTALL_HOST`: The host of your Dify instance, you can use our SaaS instance `https://debug.dify.ai`, or self-hosted Dify instance. 93 | - `REMOTE_INSTALL_PORT`: The port of your Dify instance, default is 5003 94 | - `REMOTE_INSTALL_KEY`: You should get your debugging key from the Dify instance you used, at the right top of the plugin management page, you can see a button with a `debug` icon, click it and you will get the key. 95 | 96 | Run the following command to start your Plugin: 97 | 98 | ```bash 99 | python -m main 100 | ``` 101 | 102 | Refresh the page of your Dify instance, you should be able to see your Plugin in the list now, but it will be marked as `debugging`, you can use it normally, but not recommended for production. 103 | 104 | ### Package the Plugin 105 | 106 | After all, just package your Plugin by running the following command: 107 | 108 | ```bash 109 | dify-plugin plugin package ./ROOT_DIRECTORY_OF_YOUR_PLUGIN 110 | ``` 111 | 112 | you will get a `plugin.difypkg` file, that's all, you can submit it to the Marketplace now, look forward to your Plugin being listed! 113 | 114 | 115 | ## User Privacy Policy 116 | 117 | Please fill in the privacy policy of the plugin if you want to make it published on the Marketplace, refer to [PRIVACY.md](PRIVACY.md) for more details. -------------------------------------------------------------------------------- /tools/threads.yaml: -------------------------------------------------------------------------------- 1 | identity: 2 | name: threads 3 | author: stvlynn 4 | label: 5 | en_US: Threads 6 | zh_Hans: Threads 7 | ja_JP: Threads 8 | description: 9 | human: 10 | en_US: Get RSS feeds from Threads via RSSHub 11 | zh_Hans: 通过RSSHub获取Threads的RSS订阅源 12 | ja_JP: RSSHubを通じてThreadsのRSSフィードを取得 13 | llm: "A tool to fetch RSS feeds from Threads using RSSHub as a proxy." 14 | parameters: 15 | - name: route_type 16 | type: select 17 | required: true 18 | default: "user" 19 | label: 20 | en_US: Route Type 21 | zh_Hans: 路由类型 22 | ja_JP: ルートタイプ 23 | human_description: 24 | en_US: "The type of Threads route to use" 25 | zh_Hans: "要使用的Threads路由类型" 26 | ja_JP: "使用するThreadsルートのタイプ" 27 | llm_description: "The type of Threads route to use" 28 | form: form 29 | options: 30 | - value: user 31 | label: 32 | en_US: User Timeline 33 | zh_Hans: 用户时间线 34 | ja_JP: ユーザータイムライン 35 | - name: base_url 36 | type: string 37 | required: false 38 | default: "https://rsshub.app" 39 | label: 40 | en_US: RSSHub Base URL 41 | zh_Hans: RSSHub基础URL 42 | ja_JP: RSSHubベースURL 43 | human_description: 44 | en_US: "The base URL of RSSHub instance (default is https://rsshub.app)" 45 | zh_Hans: "RSSHub实例的基础URL(默认为https://rsshub.app)" 46 | ja_JP: "RSSHubインスタンスのベースURL(デフォルトはhttps://rsshub.app)" 47 | llm_description: "The base URL of RSSHub instance (default is https://rsshub.app)" 48 | form: form 49 | - name: access_key 50 | type: string 51 | required: false 52 | label: 53 | en_US: RSSHub Access Key 54 | zh_Hans: RSSHub访问密钥 55 | ja_JP: RSSHubアクセスキー 56 | human_description: 57 | en_US: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 58 | zh_Hans: "RSSHub路由的访问密钥(例如:ILoveRSSHub)。如不需要请留空。" 59 | ja_JP: "RSSHubルートのアクセスキー(例:ILoveRSSHub)。必要ない場合は空のままにしてください。" 60 | llm_description: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 61 | form: form 62 | - name: username 63 | type: string 64 | required: true 65 | label: 66 | en_US: Threads Username 67 | zh_Hans: Threads用户名 68 | ja_JP: Threadsユーザー名 69 | human_description: 70 | en_US: "The username of Threads account" 71 | zh_Hans: "Threads账户的用户名" 72 | ja_JP: "Threadsアカウントのユーザー名" 73 | llm_description: "The username of Threads account" 74 | form: llm 75 | - name: show_author_in_title 76 | type: boolean 77 | required: false 78 | default: true 79 | label: 80 | en_US: Show Author in Title 81 | zh_Hans: 在标题中显示作者 82 | ja_JP: タイトルに作者を表示 83 | human_description: 84 | en_US: "Show author name in title" 85 | zh_Hans: "在标题中显示作者名" 86 | ja_JP: "タイトルに作者名を表示する" 87 | llm_description: "Show author name in title" 88 | form: form 89 | - name: show_author_in_desc 90 | type: boolean 91 | required: false 92 | default: true 93 | label: 94 | en_US: Show Author in Description 95 | zh_Hans: 在描述中显示作者 96 | ja_JP: 説明に作者を表示 97 | human_description: 98 | en_US: "Show author name in description (RSS body)" 99 | zh_Hans: "在描述中显示作者名(RSS正文)" 100 | ja_JP: "説明(RSSの本文)に作者名を表示する" 101 | llm_description: "Show author name in description (RSS body)" 102 | form: form 103 | - name: show_quoted_in_title 104 | type: boolean 105 | required: false 106 | default: true 107 | label: 108 | en_US: Show Quoted in Title 109 | zh_Hans: 在标题中显示引用 110 | ja_JP: タイトルに引用を表示 111 | human_description: 112 | en_US: "Show quoted thread in title" 113 | zh_Hans: "在标题中显示引用的帖子" 114 | ja_JP: "タイトルに引用されたスレッドを表示する" 115 | llm_description: "Show quoted thread in title" 116 | form: form 117 | - name: show_emoji_for_quotes_and_reply 118 | type: boolean 119 | required: false 120 | default: true 121 | label: 122 | en_US: Use Emoji for Quotes and Replies 123 | zh_Hans: 使用表情符号表示引用和回复 124 | ja_JP: 引用と返信に絵文字を使用 125 | human_description: 126 | en_US: "Use 🔁 instead of QT, ↩️ instead of Re" 127 | zh_Hans: "使用🔁代替QT,↩️代替Re" 128 | ja_JP: "QTの代わりに🔁、Reの代わりに↩️を使用する" 129 | llm_description: "Use 🔁 instead of QT, ↩️ instead of Re" 130 | form: form 131 | - name: replies 132 | type: boolean 133 | required: false 134 | default: true 135 | label: 136 | en_US: Show Replies 137 | zh_Hans: 显示回复 138 | ja_JP: 返信を表示 139 | human_description: 140 | en_US: "Show replies" 141 | zh_Hans: "显示回复" 142 | ja_JP: "返信を表示する" 143 | llm_description: "Show replies" 144 | form: form 145 | - name: show_author_avatar_in_desc 146 | type: boolean 147 | required: false 148 | default: false 149 | label: 150 | en_US: Show Author Avatar in Description 151 | zh_Hans: 在描述中显示作者头像 152 | ja_JP: 説明に作者のアバターを表示 153 | human_description: 154 | en_US: "Show avatar of author in description (Not recommended if your RSS reader extracts images from description)" 155 | zh_Hans: "在描述中显示作者头像(如果您的RSS阅读器从描述中提取图片,不推荐使用)" 156 | ja_JP: "説明に作者のアバターを表示する(RSSリーダーが説明から画像を抽出する場合は推奨されません)" 157 | llm_description: "Show avatar of author in description (Not recommended if your RSS reader extracts images from description)" 158 | form: form 159 | - name: show_quoted_author_avatar_in_desc 160 | type: boolean 161 | required: false 162 | default: false 163 | label: 164 | en_US: Show Quoted Author Avatar in Description 165 | zh_Hans: 在描述中显示被引用作者头像 166 | ja_JP: 説明に引用された作者のアバターを表示 167 | human_description: 168 | en_US: "Show avatar of quoted author in description (Not recommended if your RSS reader extracts images from description)" 169 | zh_Hans: "在描述中显示被引用作者头像(如果您的RSS阅读器从描述中提取图片,不推荐使用)" 170 | ja_JP: "説明に引用された作者のアバターを表示する(RSSリーダーが説明から画像を抽出する場合は推奨されません)" 171 | llm_description: "Show avatar of quoted author in description (Not recommended if your RSS reader extracts images from description)" 172 | form: form 173 | - name: limit 174 | type: number 175 | required: false 176 | default: 10 177 | label: 178 | en_US: Item Limit 179 | zh_Hans: 条目数量限制 180 | ja_JP: アイテム数制限 181 | human_description: 182 | en_US: "Maximum number of items to return (default: 10)" 183 | zh_Hans: "返回的最大条目数量(默认:10)" 184 | ja_JP: "返される最大アイテム数(デフォルト:10)" 185 | llm_description: "Maximum number of items to return (default: 10)" 186 | form: llm 187 | extra: 188 | python: 189 | source: tools/threads.py 190 | output_variables: 191 | - name: title 192 | type: string 193 | description: 194 | en_US: The title of the RSS feed 195 | zh_Hans: RSS订阅源的标题 196 | ja_JP: RSSフィードのタイトル 197 | - name: description 198 | type: string 199 | description: 200 | en_US: The description of the RSS feed 201 | zh_Hans: RSS订阅源的描述 202 | ja_JP: RSSフィードの説明 203 | - name: url 204 | type: string 205 | description: 206 | en_US: The URL of the RSS feed 207 | zh_Hans: RSS订阅源的URL 208 | ja_JP: RSSフィードのURL 209 | - name: entries 210 | type: array 211 | description: 212 | en_US: The entries of the RSS feed 213 | zh_Hans: RSS订阅源的条目 214 | ja_JP: RSSフィードのエントリー 215 | - name: username 216 | type: string 217 | description: 218 | en_US: The username of the Threads account 219 | zh_Hans: Threads账户的用户名 220 | ja_JP: Threadsアカウントのユーザー名 -------------------------------------------------------------------------------- /tools/linkedin.yaml: -------------------------------------------------------------------------------- 1 | identity: 2 | name: linkedin 3 | author: stvlynn 4 | label: 5 | en_US: LinkedIn 6 | zh_Hans: LinkedIn 7 | ja_JP: LinkedIn 8 | description: 9 | human: 10 | en_US: Get RSS feeds from LinkedIn via RSSHub 11 | zh_Hans: 通过RSSHub获取LinkedIn的RSS订阅源 12 | ja_JP: RSSHubを通じてLinkedInのRSSフィードを取得 13 | llm: "A tool to fetch RSS feeds from LinkedIn using RSSHub as a proxy." 14 | parameters: 15 | - name: route_type 16 | type: select 17 | required: true 18 | default: "jobs" 19 | label: 20 | en_US: Route Type 21 | zh_Hans: 路由类型 22 | ja_JP: ルートタイプ 23 | human_description: 24 | en_US: "The type of LinkedIn route to use" 25 | zh_Hans: "要使用的LinkedIn路由类型" 26 | ja_JP: "使用するLinkedInルートのタイプ" 27 | llm_description: "The type of LinkedIn route to use" 28 | form: form 29 | options: 30 | - value: jobs 31 | label: 32 | en_US: Jobs 33 | zh_Hans: 职位搜索 34 | ja_JP: 求人検索 35 | - name: base_url 36 | type: string 37 | required: false 38 | default: "https://rsshub.app" 39 | label: 40 | en_US: RSSHub Base URL 41 | zh_Hans: RSSHub基础URL 42 | ja_JP: RSSHubベースURL 43 | human_description: 44 | en_US: "The base URL of RSSHub instance (default is https://rsshub.app)" 45 | zh_Hans: "RSSHub实例的基础URL(默认为https://rsshub.app)" 46 | ja_JP: "RSSHubインスタンスのベースURL(デフォルトはhttps://rsshub.app)" 47 | llm_description: "The base URL of RSSHub instance (default is https://rsshub.app)" 48 | form: form 49 | - name: access_key 50 | type: string 51 | required: false 52 | label: 53 | en_US: RSSHub Access Key 54 | zh_Hans: RSSHub访问密钥 55 | ja_JP: RSSHubアクセスキー 56 | human_description: 57 | en_US: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 58 | zh_Hans: "RSSHub路由的访问密钥(例如:ILoveRSSHub)。如不需要请留空。" 59 | ja_JP: "RSSHubルートのアクセスキー(例:ILoveRSSHub)。必要ない場合は空のままにしてください。" 60 | llm_description: "Access key for RSSHub routes (e.g. ILoveRSSHub). Leave empty if not needed." 61 | form: form 62 | - name: keywords 63 | type: string 64 | required: false 65 | label: 66 | en_US: Keywords 67 | zh_Hans: 关键词 68 | ja_JP: キーワード 69 | human_description: 70 | en_US: "Search keywords for jobs" 71 | zh_Hans: "职位搜索关键词" 72 | ja_JP: "求人検索キーワード" 73 | llm_description: "Search keywords for jobs" 74 | form: llm 75 | - name: job_types 76 | type: select 77 | required: false 78 | default: "all" 79 | label: 80 | en_US: Job Types 81 | zh_Hans: 职位类型 82 | ja_JP: 雇用形態 83 | human_description: 84 | en_US: "The type of jobs to search for" 85 | zh_Hans: "要搜索的职位类型" 86 | ja_JP: "検索する雇用形態" 87 | llm_description: "The type of jobs to search for" 88 | form: form 89 | options: 90 | - value: F 91 | label: 92 | en_US: Full Time 93 | zh_Hans: 全职 94 | ja_JP: フルタイム 95 | - value: P 96 | label: 97 | en_US: Part Time 98 | zh_Hans: 兼职 99 | ja_JP: パートタイム 100 | - value: C 101 | label: 102 | en_US: Contractor 103 | zh_Hans: 合同工 104 | ja_JP: 契約社員 105 | - value: all 106 | label: 107 | en_US: All 108 | zh_Hans: 全部 109 | ja_JP: すべて 110 | - name: exp_levels 111 | type: select 112 | required: false 113 | default: "all" 114 | label: 115 | en_US: Experience Levels 116 | zh_Hans: 经验等级 117 | ja_JP: 経験レベル 118 | human_description: 119 | en_US: "The experience level required for the jobs" 120 | zh_Hans: "职位所需的经验等级" 121 | ja_JP: "求人に必要な経験レベル" 122 | llm_description: "The experience level required for the jobs" 123 | form: form 124 | options: 125 | - value: "1" 126 | label: 127 | en_US: Internship 128 | zh_Hans: 实习 129 | ja_JP: インターンシップ 130 | - value: "2" 131 | label: 132 | en_US: Entry Level 133 | zh_Hans: 入门级 134 | ja_JP: 新卒・第二新卒 135 | - value: "3" 136 | label: 137 | en_US: Associate 138 | zh_Hans: 助理级 139 | ja_JP: アソシエイト 140 | - value: "4" 141 | label: 142 | en_US: Mid-Senior Level 143 | zh_Hans: 中高级 144 | ja_JP: 中堅・管理職 145 | - value: "5" 146 | label: 147 | en_US: Director 148 | zh_Hans: 总监级 149 | ja_JP: ディレクター 150 | - value: all 151 | label: 152 | en_US: All 153 | zh_Hans: 全部 154 | ja_JP: すべて 155 | - name: work_type 156 | type: select 157 | required: false 158 | default: "0" 159 | label: 160 | en_US: Work Type 161 | zh_Hans: 工作方式 162 | ja_JP: 勤務形態 163 | human_description: 164 | en_US: "The work type of the jobs" 165 | zh_Hans: "职位的工作方式" 166 | ja_JP: "求人の勤務形態" 167 | llm_description: "The work type of the jobs" 168 | form: form 169 | options: 170 | - value: "1" 171 | label: 172 | en_US: Onsite 173 | zh_Hans: 现场办公 174 | ja_JP: オンサイト 175 | - value: "2" 176 | label: 177 | en_US: Remote 178 | zh_Hans: 远程办公 179 | ja_JP: リモート 180 | - value: "3" 181 | label: 182 | en_US: Hybrid 183 | zh_Hans: 混合办公 184 | ja_JP: ハイブリッド 185 | - value: "0" 186 | label: 187 | en_US: Any 188 | zh_Hans: 任意 189 | ja_JP: 指定なし 190 | - name: time_posted 191 | type: select 192 | required: false 193 | default: "0" 194 | label: 195 | en_US: Time Posted 196 | zh_Hans: 发布时间 197 | ja_JP: 投稿時間 198 | human_description: 199 | en_US: "Filter by when the job was posted" 200 | zh_Hans: "按职位发布时间筛选" 201 | ja_JP: "求人の投稿時間でフィルタリング" 202 | llm_description: "Filter by when the job was posted" 203 | form: form 204 | options: 205 | - value: "r86400" 206 | label: 207 | en_US: Past 24 hours 208 | zh_Hans: 过去24小时 209 | ja_JP: 過去24時間 210 | - value: "r604800" 211 | label: 212 | en_US: Past week 213 | zh_Hans: 过去一周 214 | ja_JP: 過去1週間 215 | - value: "r2592000" 216 | label: 217 | en_US: Past month 218 | zh_Hans: 过去一个月 219 | ja_JP: 過去1ヶ月 220 | - value: "0" 221 | label: 222 | en_US: Any time 223 | zh_Hans: 任意时间 224 | ja_JP: 指定なし 225 | - name: geo_id 226 | type: string 227 | required: false 228 | label: 229 | en_US: Geographic Location ID 230 | zh_Hans: 地理位置ID 231 | ja_JP: 地理的位置ID 232 | human_description: 233 | en_US: "Geographic location ID (e.g. 91000012 for East Asia)" 234 | zh_Hans: "地理位置ID(如91000012代表东亚)" 235 | ja_JP: "地理的位置ID(例:91000012は東アジアを表す)" 236 | llm_description: "Geographic location ID (e.g. 91000012 for East Asia)" 237 | form: llm 238 | - name: limit 239 | type: number 240 | required: false 241 | label: 242 | en_US: Item Limit 243 | zh_Hans: 条目数量限制 244 | ja_JP: アイテム数制限 245 | human_description: 246 | en_US: "Maximum number of items to return (default: 10)" 247 | zh_Hans: "返回的最大条目数量(默认:10)" 248 | ja_JP: "返される最大アイテム数(デフォルト:10)" 249 | llm_description: "Maximum number of items to return (default: 10)" 250 | form: llm 251 | extra: 252 | python: 253 | source: tools/linkedin.py 254 | -------------------------------------------------------------------------------- /tools/twitter.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from typing import Any 3 | import requests 4 | import html 5 | import re 6 | import xml.etree.ElementTree as ET 7 | from datetime import datetime 8 | from urllib.parse import urljoin, urlparse, parse_qs, urlencode 9 | 10 | from dify_plugin import Tool 11 | from dify_plugin.entities.tool import ToolInvokeMessage 12 | 13 | class TwitterTool(Tool): 14 | def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: 15 | # 获取参数 16 | route_type = tool_parameters.get("route_type", "user") 17 | username = tool_parameters.get("username", "") 18 | keyword = tool_parameters.get("keyword", "") 19 | list_id = tool_parameters.get("list_id", "") 20 | limit = int(tool_parameters.get("limit", 10)) 21 | base_url = tool_parameters.get("base_url", "https://rsshub.app") 22 | access_key = tool_parameters.get("access_key", "") 23 | exclude_replies = tool_parameters.get("exclude_replies", False) 24 | exclude_rts = tool_parameters.get("exclude_rts", False) 25 | 26 | # 确保base_url以/结尾 27 | if not base_url.endswith("/"): 28 | base_url = base_url + "/" 29 | 30 | # 根据路由类型构建不同的路由 31 | route = "" 32 | 33 | if route_type == "user": 34 | if not username: 35 | yield self.create_text_message("用户路由需要提供用户名") 36 | return 37 | 38 | # 移除用户名中的@符号(如果有) 39 | username = username.lstrip('@') 40 | route = f"/twitter/user/{username}" 41 | 42 | # 添加额外参数 43 | route_params = [] 44 | if exclude_replies: 45 | route_params.append("exclude_replies") 46 | if exclude_rts: 47 | route_params.append("exclude_rts") 48 | 49 | # 如果有额外参数,添加到路由中 50 | if route_params: 51 | route = f"{route}/{','.join(route_params)}" 52 | 53 | elif route_type == "keyword": 54 | if not keyword: 55 | yield self.create_text_message("关键词路由需要提供关键词") 56 | return 57 | 58 | route = f"/twitter/keyword/{keyword}" 59 | 60 | elif route_type == "list": 61 | if not list_id: 62 | yield self.create_text_message("列表路由需要提供列表ID") 63 | return 64 | 65 | route = f"/twitter/list/{list_id}" 66 | 67 | elif route_type == "home": 68 | route = "/twitter/home" 69 | 70 | elif route_type == "home_latest": 71 | route = "/twitter/home_latest" 72 | 73 | elif route_type == "likes": 74 | if not username: 75 | yield self.create_text_message("喜欢路由需要提供用户名") 76 | return 77 | 78 | # 移除用户名中的@符号(如果有) 79 | username = username.lstrip('@') 80 | route = f"/twitter/likes/{username}" 81 | 82 | elif route_type == "media": 83 | if not username: 84 | yield self.create_text_message("媒体路由需要提供用户名") 85 | return 86 | 87 | # 移除用户名中的@符号(如果有) 88 | username = username.lstrip('@') 89 | route = f"/twitter/media/{username}" 90 | 91 | else: 92 | yield self.create_text_message(f"不支持的路由类型: {route_type}") 93 | return 94 | 95 | # 构建完整URL 96 | url = urljoin(base_url, route) 97 | 98 | # 如果提供了访问密钥,添加到URL中 99 | if access_key: 100 | # 解析URL 101 | parsed_url = urlparse(url) 102 | # 获取现有查询参数 103 | query_params = parse_qs(parsed_url.query) 104 | # 添加key参数 105 | query_params['key'] = [access_key] 106 | # 重新构建查询字符串 107 | new_query = urlencode(query_params, doseq=True) 108 | # 替换URL中的查询部分 109 | url_parts = list(parsed_url) 110 | url_parts[4] = new_query 111 | # 重新组合URL 112 | url = urljoin(base_url, route + "?" + new_query) 113 | 114 | try: 115 | # 获取RSS源 116 | response = requests.get(url, timeout=10) 117 | response.raise_for_status() 118 | 119 | # 解析XML 120 | root = ET.fromstring(response.content) 121 | 122 | # 获取命名空间 123 | namespaces = {'': root.tag.split('}')[0].strip('{')} if '}' in root.tag else {} 124 | 125 | # 提取标题和描述 126 | channel = root.find('.//channel', namespaces) 127 | if channel is None: 128 | channel = root 129 | 130 | # 根据路由类型设置默认标题 131 | default_title = "" 132 | if route_type == "user": 133 | default_title = f"@{username} 的推文" 134 | elif route_type == "keyword": 135 | default_title = f"关于 {keyword} 的推文" 136 | elif route_type == "list": 137 | default_title = f"Twitter 列表 {list_id}" 138 | elif route_type == "home": 139 | default_title = "Twitter 主页时间线" 140 | elif route_type == "home_latest": 141 | default_title = "Twitter 最新主页时间线" 142 | elif route_type == "likes": 143 | default_title = f"@{username} 喜欢的推文" 144 | elif route_type == "media": 145 | default_title = f"@{username} 的媒体推文" 146 | 147 | title = channel.findtext('./title', default_title, namespaces) 148 | description = channel.findtext('./description', f"Twitter RSS 源", namespaces) 149 | 150 | # 提取条目 151 | entries = [] 152 | items = channel.findall('./item', namespaces) 153 | 154 | for item in items[:limit]: 155 | item_title = item.findtext('./title', '无标题', namespaces) 156 | item_link = item.findtext('./link', '', namespaces) 157 | item_pubDate = item.findtext('./pubDate', '', namespaces) 158 | item_description = item.findtext('./description', '', namespaces) 159 | 160 | # 清理HTML标签 161 | clean_content = re.sub(r'<.*?>', '', item_description) 162 | clean_content = html.unescape(clean_content) 163 | 164 | # 格式化日期 165 | published = item_pubDate 166 | try: 167 | if published: 168 | dt = datetime.strptime(published, '%a, %d %b %Y %H:%M:%S %Z') 169 | published = dt.strftime('%Y-%m-%d %H:%M:%S') 170 | except: 171 | # 如果日期解析失败,保持原样 172 | pass 173 | 174 | entries.append({ 175 | 'title': item_title, 176 | 'link': item_link, 177 | 'published': published, 178 | 'content': clean_content[:500] + ('...' if len(clean_content) > 500 else '') 179 | }) 180 | 181 | # 返回结果 182 | result = { 183 | "title": title, 184 | "description": description, 185 | "url": url, 186 | "entries": entries, 187 | "route_type": route_type, 188 | "filters": { 189 | "exclude_replies": exclude_replies if route_type == "user" else None, 190 | "exclude_rts": exclude_rts if route_type == "user" else None 191 | } 192 | } 193 | 194 | yield self.create_json_message(result) 195 | 196 | except Exception as e: 197 | error_message = f"获取Twitter RSS源失败: {str(e)}" 198 | yield self.create_text_message(error_message) -------------------------------------------------------------------------------- /_assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------