├── .prettierignore ├── .github ├── FUNDING.yml └── workflows │ └── pypi-publish.yml ├── pyproject.toml ├── nonebot_plugin_eitherchoice ├── __init__.py ├── config.py ├── __main__.py ├── res │ └── template.html.jinja └── data_source.py ├── LICENSE ├── .gitignore └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://afdian.net/@lgc2333/'] 2 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Use PDM to Build and publish Python 🐍 distributions 📦 to PyPI 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | # IMPORTANT: this permission is mandatory for trusted publishing 15 | id-token: write 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@master 20 | with: 21 | submodules: true 22 | 23 | - name: Setup PDM 24 | uses: pdm-project/setup-pdm@v3 25 | 26 | - name: Build and Publish distribution 📦 to PyPI 27 | run: pdm publish 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "nonebot-plugin-eitherchoice" 3 | version = "0.2.0.post1" 4 | description = "Let AI help you compare two things." 5 | authors = [{ name = "student_2333", email = "lgc2333@126.com" }] 6 | dependencies = [ 7 | "nonebot2>=2.2.0", 8 | "nonebot-plugin-htmlrender>=0.2.1", 9 | "nonebot-plugin-send-anything-anywhere>=0.3.2", 10 | "pydantic>=1.10.0,<2", 11 | "httpx[http2]>=0.24.1", 12 | "beautifulsoup4>=4.12.2", 13 | "lxml>=4.9.3", 14 | ] 15 | requires-python = ">=3.9,<4.0" 16 | readme = "README.md" 17 | license = { text = "MIT" } 18 | 19 | [project.urls] 20 | homepage = "https://github.com/lgc-NB2Dev/nonebot-plugin-eitherchoice" 21 | 22 | [tool.pdm.build] 23 | includes = [] 24 | 25 | [tool.pdm.dev-dependencies] 26 | dev = ["nb-cli>=1.0.5", "black>=23.3.0", "ruff>=0.0.260", "isort>=5.12.0"] 27 | 28 | [build-system] 29 | requires = ["pdm-backend"] 30 | build-backend = "pdm.backend" 31 | -------------------------------------------------------------------------------- /nonebot_plugin_eitherchoice/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import require 2 | from nonebot.plugin import PluginMetadata, inherit_supported_adapters 3 | 4 | require("nonebot_plugin_htmlrender") 5 | require("nonebot_plugin_saa") 6 | 7 | from . import __main__ as __main__ # noqa: E402 8 | from .config import ConfigModel # noqa: E402 9 | 10 | __version__ = "0.2.0" 11 | __plugin_meta__ = PluginMetadata( 12 | name="EitherChoice", 13 | description="让 AI 帮你对比两件事物", 14 | usage=( 15 | "▶ 指令:(对比/比较/锐评/评价/如何评价)[下/一下] 要顶的事物 (和/与/and/vs/&) 要踩的事物\n" 16 | "▶ 示例:\n" 17 | " ▷ 对比 Python 和 JavaScript\n" 18 | " ▷ 锐评一下 C# & Java\n" 19 | " ▷ 比较 下北泽 与 东京 (加上空格防止 `下` 或 `一下` 被当做指令的一部分去除)" 20 | ), 21 | type="application", 22 | homepage="https://github.com/lgc-NB2Dev/nonebot-plugin-eitherchoice", 23 | config=ConfigModel, 24 | supported_adapters=inherit_supported_adapters("nonebot_plugin_saa"), 25 | extra={"License": "MIT", "Author": "student_2333"}, 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 student_2333 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 | -------------------------------------------------------------------------------- /nonebot_plugin_eitherchoice/config.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from nonebot import get_driver 4 | from pydantic import BaseModel, validator 5 | 6 | 7 | class ConfigModel(BaseModel): 8 | proxy: Optional[str] = None 9 | either_choice_timeout: Optional[int] = None 10 | either_choice_retry: int = 2 11 | either_choice_lang: str = "zh-CN" 12 | either_choice_allow_public: str = "true" 13 | either_choice_force_ask: bool = True 14 | either_choice_pic_width: int = 1280 15 | either_choice_main_font: str = ( 16 | "'Microsoft YaHei UI', 'Microsoft YaHei', " 17 | "'Source Han Sans CN', 'Source Han Sans SC', " 18 | "'PingFang SC', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', sans-serif" 19 | ) 20 | either_choice_code_font: str = ( 21 | "'JetBrains Mono', 'JetBrainsMono Nerd Font', " 22 | "'Victor Mono', 'VictorMono Nerd Font', " 23 | "'Fira Code', 'FiraCode Nerd Font', " 24 | "'Cascadia Code', 'CascadiaCode Nerd Font', " 25 | "'Consolas', 'Courier New', monospace" 26 | ) 27 | 28 | @validator("either_choice_allow_public", pre=True) 29 | def either_choice_allow_public_validator(cls, v: Any): # noqa: N805 30 | v = str(v).lower() 31 | if v in ("true", "false"): 32 | return v 33 | raise ValueError("`either_choice_allow_public` must be `true` or `false`") 34 | 35 | 36 | config: ConfigModel = ConfigModel.parse_obj(get_driver().config.dict()) 37 | -------------------------------------------------------------------------------- /nonebot_plugin_eitherchoice/__main__.py: -------------------------------------------------------------------------------- 1 | import string 2 | from typing import Tuple 3 | 4 | from nonebot import logger, on_command 5 | from nonebot.adapters import Message 6 | from nonebot.params import CommandArg 7 | from nonebot.typing import T_State 8 | from nonebot_plugin_saa import Image, MessageFactory 9 | 10 | from .data_source import get_choice_pic 11 | 12 | CMD_PREFIX = ("对比", "比较", "锐评", "评价", "如何评价") 13 | CMD_SUFFIX = ("下", "一下") 14 | COMMANDS = CMD_PREFIX + tuple(f"{x}{y}" for x in CMD_PREFIX for y in CMD_SUFFIX) 15 | 16 | SEP_CHARS = ("和", "与", "and", "vs", "&") 17 | SEP_CHARS = tuple(f" {x}" for x in SEP_CHARS) + SEP_CHARS 18 | STRIP_CHARS = "'\"“”‘’" 19 | 20 | 21 | async def check_rule(state: T_State, arg: Message = CommandArg()) -> bool: 22 | arg_str = arg.extract_plain_text() 23 | sep = next((x for x in SEP_CHARS if x in arg_str), None) 24 | if not sep: 25 | return False 26 | 27 | things = tuple( 28 | x.strip(string.whitespace + STRIP_CHARS) for x in arg_str.split(sep, 1) 29 | ) 30 | if len(things) != 2 or (not all(things)): 31 | return False 32 | 33 | state["things"] = things 34 | return True 35 | 36 | 37 | first_cmd, *other_cmd = COMMANDS 38 | cmd_choice = on_command(first_cmd, aliases=set(other_cmd), rule=check_rule) 39 | 40 | 41 | @cmd_choice.handle() 42 | async def _(state: T_State): 43 | things: Tuple[str, str] = state["things"] 44 | 45 | tip_receipt = await MessageFactory("请稍等,AI 正在帮你评价...").send() 46 | 47 | try: 48 | pic = await get_choice_pic(*things) 49 | except Exception: 50 | logger.exception("发生错误") 51 | await MessageFactory("发生错误,请稍后重试……").send(reply=True) 52 | else: 53 | await MessageFactory(Image(pic)).send(reply=True) 54 | finally: 55 | try: 56 | await tip_receipt.revoke() 57 | except Exception as e: 58 | logger.warning(f"撤回提示消息失败:{e!r}") 59 | -------------------------------------------------------------------------------- /nonebot_plugin_eitherchoice/res/template.html.jinja: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 94 | 95 | 96 | 97 |
98 |
{{ a }} VS {{ b }}
99 |
{{ a }} for sure!
100 |
{{ table|safe }}
101 |
102 | 103 | 104 | {{ extra }} 105 | 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | # pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | .pdm-python 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 | ### Python Patch ### 168 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 169 | # poetry.toml 170 | 171 | # End of https://www.toptal.com/developers/gitignore/api/python 172 | 173 | .vscode 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | NoneBotPluginLogo 7 | 8 | 9 |

10 | NoneBotPluginText 11 |

12 | 13 | # NoneBot-Plugin-EitherChoice 14 | 15 | _✨ 让 AI 帮你~~锐评~~对比两件事物 ✨_ 16 | 17 | python 18 | 19 | pdm-managed 20 | 21 | 22 | wakatime 23 | 24 | 25 |
26 | 27 | 28 | license 29 | 30 | 31 | pypi 32 | 33 | 34 | pypi download 35 | 36 | 37 |
38 | 39 | ## 📖 介绍 40 | 41 | ~~让 AI 帮你一本正经地胡说八道~~ 42 | 43 | 灵感来自 [koishi-plugin-eitherchoice](https://www.npmjs.com/package/koishi-plugin-eitherchoice) ~~(电子蝈蝈,电子博弈,在线观看)~~ 44 | 服务来自 [EitherChoice](https://eitherchoice.com/) 45 | 46 | ## 💿 安装 47 | 48 | 以下提到的方法 任选**其一** 即可 49 | 50 |
51 | [推荐] 使用 nb-cli 安装 52 | 在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装 53 | 54 | ```bash 55 | nb plugin install nonebot-plugin-eitherchoice 56 | ``` 57 | 58 |
59 | 60 |
61 | 使用包管理器安装 62 | 在 nonebot2 项目的插件目录下, 打开命令行, 根据你使用的包管理器, 输入相应的安装命令 63 | 64 |
65 | pip 66 | 67 | ```bash 68 | pip install nonebot-plugin-eitherchoice 69 | ``` 70 | 71 |
72 |
73 | pdm 74 | 75 | ```bash 76 | pdm add nonebot-plugin-eitherchoice 77 | ``` 78 | 79 |
80 |
81 | poetry 82 | 83 | ```bash 84 | poetry add nonebot-plugin-eitherchoice 85 | ``` 86 | 87 |
88 |
89 | conda 90 | 91 | ```bash 92 | conda install nonebot-plugin-eitherchoice 93 | ``` 94 | 95 |
96 | 97 | 打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot]` 部分的 `plugins` 项里追加写入 98 | 99 | ```toml 100 | [tool.nonebot] 101 | plugins = [ 102 | # ... 103 | "nonebot_plugin_eitherchoice" 104 | ] 105 | ``` 106 | 107 |
108 | 109 | ## ⚙️ 配置 110 | 111 | 在 nonebot2 项目的`.env`文件中添加下表中的必填配置 112 | 113 | | 配置项 | 必填 | 默认值 | 说明 | 114 | | :--------------------------: | :--: | :-----: | :--------------------------------------------------: | 115 | | `PROXY` | 否 | 无 | 访问接口使用的代理 | 116 | | `EITHER_CHOICE_TIMEOUT` | 否 | `None` | 访问接口超时,单位秒,可以设置为 `None` 以禁用超时 | 117 | | `EITHER_CHOICE_RETRY` | 否 | `2` | 访问接口最大重试次数 | 118 | | `EITHER_CHOICE_LANG` | 否 | `zh-CN` | 目标语言 | 119 | | `EITHER_CHOICE_ALLOW_PUBLIC` | 否 | `True` | 是否允许 AI 上网搜索相关信息 | 120 | | `EITHER_CHOICE_FORCE_ASK` | 否 | `True` | 是否每次触发指令都调用 ask 接口,不检索网站缓存 | 121 | | `EITHER_CHOICE_PIC_WIDTH` | 否 | `1280` | 生成图片的宽度,单位像素(实际宽度可能比这里大一倍) | 122 | | `EITHER_CHOICE_MAIN_FONT` | 否 | ... | 生成图片的主字体,使用 CSS 语法 | 123 | | `EITHER_CHOICE_CODE_FONT` | 否 | ... | 生成图片的代码字体,使用 CSS 语法 | 124 | 125 | ## 🎉 使用 126 | 127 | ### 指令 128 | 129 | - 指令:`(对比/比较/锐评/评价/如何评价)[下/一下] 要顶的事物 (和/与/and/vs/&) 要踩的事物` 130 | - 示例: 131 | - 对比 Python 和 JavaScript 132 | - 锐评一下 C# & Java 133 | - 比较 下北泽 与 东京 (加上空格防止 `下` 或 `一下` 被当做指令的一部分去除) 134 | 135 | ### 效果图 136 | 137 | ![Alt text](https://raw.githubusercontent.com/lgc-NB2Dev/readme/main/eitherchoice/example.png) 138 | 139 | ## 📞 联系 140 | 141 | QQ:3076823485 142 | Telegram:[@lgc2333](https://t.me/lgc2333) 143 | 吹水群:[1105946125](https://jq.qq.com/?_wv=1027&k=Z3n1MpEp) 144 | 邮箱: 145 | 146 | ## 💡 鸣谢 147 | 148 | ### [EitherChoice](https://eitherchoice.com/) 149 | 150 | - 服务提供 151 | 152 | ## 💰 赞助 153 | 154 | 感谢大家的赞助!你们的赞助将是我继续创作的动力! 155 | 156 | - [爱发电](https://afdian.net/@lgc2333) 157 | -
158 | 赞助二维码(点击展开) 159 | 160 | ![讨饭](https://raw.githubusercontent.com/lgc2333/ShigureBotMenu/master/src/imgs/sponsor.png) 161 | 162 |
163 | 164 | ## 📝 更新日志 165 | 166 | ### 0.2.0 167 | 168 | 真是久违的更新,这玩意好像坏掉有一段时间了……但是这次修没修好还不确定…… 169 | 170 | - 修这玩意用不了的 Bug 171 | - 默认禁用请求超时 172 | - 微调触发指令 173 | - 新配置 `EITHER_CHOICE_FORCE_ASK` 174 | 175 | ### 0.1.2 176 | 177 | - 修复由于 API 地址变动导致的显示 `[object Object]` 问题 178 | - 微调指令匹配方式 179 | 180 | ### 0.1.1 181 | 182 | - 添加配置项 `EITHER_CHOICE_RETRY`,当访问接口出现问题时,将会自动重试 183 | -------------------------------------------------------------------------------- /nonebot_plugin_eitherchoice/data_source.py: -------------------------------------------------------------------------------- 1 | import time 2 | import urllib.parse 3 | from pathlib import Path 4 | from typing import Optional 5 | 6 | import jinja2 7 | from bs4 import BeautifulSoup, Tag 8 | from httpx import AsyncClient 9 | from nonebot import logger 10 | from nonebot_plugin_htmlrender.data_source import get_new_page, read_tpl 11 | 12 | from .config import config 13 | 14 | RES_PATH = Path(__file__).parent / "res" 15 | HTML_TEMPLATE = jinja2.Template((RES_PATH / "template.html.jinja").read_text("u8")) 16 | 17 | TEMPLATE_HEADERS = { 18 | "User-Agent": ( 19 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 20 | "AppleWebKit/537.36 (KHTML, like Gecko) " 21 | "Chrome/114.0.0.0 " 22 | "Safari/537.36" 23 | ), 24 | "Origin": "https://eitherchoice.com", 25 | "Cookie": "ec_ask=1", 26 | } 27 | 28 | 29 | async def render_image(thing_a: str, thing_b: str, content: str) -> bytes: 30 | katex_js = await read_tpl("katex/katex.min.js") 31 | mathtex_js = await read_tpl("katex/mathtex-script-type.min.js") 32 | extra = f"" 33 | css_txt = "\n\n".join( 34 | ( 35 | await read_tpl("github-markdown-light.css"), 36 | await read_tpl("pygments-default.css"), 37 | await read_tpl("katex/katex.min.b64_fonts.css"), 38 | ), 39 | ) 40 | 41 | rendered_html = HTML_TEMPLATE.render( 42 | css=css_txt, 43 | main_font=config.either_choice_main_font, 44 | code_font=config.either_choice_code_font, 45 | a=thing_a, 46 | b=thing_b, 47 | table=content, 48 | extra=extra, 49 | ) 50 | 51 | async with get_new_page( 52 | viewport={"width": config.either_choice_pic_width, "height": 720}, 53 | ) as page: 54 | await page.goto(RES_PATH.as_uri()) 55 | await page.set_content(rendered_html, wait_until="networkidle") 56 | 57 | elem = await page.query_selector(".body") 58 | assert elem 59 | return await elem.screenshot(type="jpeg") 60 | 61 | 62 | def url_enc(s: str) -> str: 63 | return urllib.parse.quote(s, safe="") 64 | 65 | 66 | def build_referer(thing_a: str, thing_b: str) -> str: 67 | return ( 68 | f"https://eitherchoice.com/fighting/{url_enc(thing_a)}-vs-{url_enc(thing_b)}?" 69 | f"t={time.time() * 1000:.0f}&p={config.either_choice_allow_public}" 70 | ) 71 | 72 | 73 | async def get_from_page( 74 | thing_a: str, 75 | thing_b: str, 76 | referer: Optional[str] = None, 77 | ) -> str: 78 | headers = TEMPLATE_HEADERS.copy() 79 | if referer: 80 | headers["Referer"] = referer 81 | 82 | async with AsyncClient( 83 | http2=True, 84 | timeout=config.either_choice_timeout, 85 | proxies=config.proxy, 86 | ) as client: 87 | resp = await client.get( 88 | f"https://eitherchoice.com/fight/{url_enc(thing_a)}-vs-{url_enc(thing_b)}", 89 | follow_redirects=False, 90 | ) 91 | resp.raise_for_status() 92 | text = resp.text 93 | 94 | logger.debug(text) 95 | soup = BeautifulSoup(text, "lxml") 96 | main = soup.find("main") 97 | assert isinstance(main, Tag), "main tag not found, or it is not a tag" 98 | 99 | elements = list(main.children)[3:-3] # 第四个到倒数第四个,剃掉标题和脚注 100 | assert len(elements), "no elements found" 101 | 102 | return "\n".join(str(x) for x in elements) 103 | 104 | 105 | async def ask_choice( 106 | thing_a: str, 107 | thing_b: str, 108 | referer: Optional[str] = None, 109 | ): 110 | headers = TEMPLATE_HEADERS.copy() 111 | if referer: 112 | headers["Referer"] = referer 113 | 114 | body = { 115 | "A": thing_a, 116 | "B": thing_b, 117 | "allowPublic": config.either_choice_allow_public, 118 | "lang": config.either_choice_lang, 119 | } 120 | 121 | async with AsyncClient( 122 | http2=True, 123 | timeout=config.either_choice_timeout, 124 | proxies=config.proxy, 125 | headers=headers, 126 | ) as client: 127 | resp = await client.post("https://eitherchoice.com/api/prompt/ask", json=body) 128 | if resp.status_code != 524: # server timeout 129 | resp.raise_for_status() 130 | else: 131 | logger.warning( 132 | "Server timeout! Maybe the content is not completely generated...", 133 | ) 134 | 135 | 136 | async def get_choice(thing_a: str, thing_b: str) -> str: 137 | if not config.either_choice_force_ask: 138 | try: 139 | return await get_from_page(thing_a, thing_b) 140 | except Exception as e: 141 | logger.info(f"Fight page may not exist, asking now: {e!r}") 142 | 143 | referer = build_referer(thing_a, thing_b) 144 | logger.info(f"Referer: {referer}") 145 | 146 | # 这傻逼玩意真他妈头疼,搞了半天都没搞清楚这玩意的破逻辑, 147 | # 不搞了,我急了,摆烂!主打一个能用就行! 148 | for i in range(config.either_choice_retry + 1): 149 | try: 150 | await ask_choice(thing_a, thing_b, referer) 151 | return await get_from_page(thing_a, thing_b, referer) 152 | except Exception as e: 153 | if i == config.either_choice_retry: 154 | raise 155 | logger.warning( 156 | f"Failed to ask or failed to get content! " 157 | f"retrying ({config.either_choice_retry - i} left): {e!r}", 158 | ) 159 | 160 | raise ValueError 161 | 162 | 163 | async def get_choice_pic(thing_a: str, thing_b: str) -> bytes: 164 | return await render_image(thing_a, thing_b, await get_choice(thing_a, thing_b)) 165 | --------------------------------------------------------------------------------