├── .gitignore ├── Dockerfile ├── Dockerfile.aipro ├── Dockerfile.popai ├── Dockerfile.wrtnai ├── LICENSE ├── README.md ├── docker-compose.yml ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── src ├── aipro.py ├── chat2api │ ├── __init__.py │ ├── api.py │ ├── chat.py │ └── util.py ├── popai.py └── wrtnai.py └── tests └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | ### Windows template 4 | # Windows thumbnail cache files 5 | Thumbs.db 6 | ehthumbs.db 7 | ehthumbs_vista.db 8 | 9 | # Dump file 10 | *.stackdump 11 | 12 | # Folder config file 13 | [Dd]esktop.ini 14 | 15 | # Recycle Bin used on file shares 16 | $RECYCLE.BIN/ 17 | 18 | # Windows Installer files 19 | *.cab 20 | *.msi 21 | *.msix 22 | *.msm 23 | *.msp 24 | 25 | # Windows shortcuts 26 | *.lnk 27 | ### macOS template 28 | # General 29 | .DS_Store 30 | .AppleDouble 31 | .LSOverride 32 | 33 | # Icon must end with two \r 34 | Icon 35 | 36 | # Thumbnails 37 | ._* 38 | 39 | # Files that might appear in the root of a volume 40 | .DocumentRevisions-V100 41 | .fseventsd 42 | .Spotlight-V100 43 | .TemporaryItems 44 | .Trashes 45 | .VolumeIcon.icns 46 | .com.apple.timemachine.donotpresent 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | ### Python template 55 | # Byte-compiled / optimized / DLL files 56 | __pycache__/ 57 | *.py[cod] 58 | *$py.class 59 | 60 | # C extensions 61 | *.so 62 | 63 | # Distribution / packaging 64 | .Python 65 | build/ 66 | develop-eggs/ 67 | dist/ 68 | downloads/ 69 | eggs/ 70 | .eggs/ 71 | lib/ 72 | lib64/ 73 | parts/ 74 | sdist/ 75 | var/ 76 | wheels/ 77 | *.egg-info/ 78 | .installed.cfg 79 | *.egg 80 | MANIFEST 81 | 82 | # PyInstaller 83 | # Usually these files are written by a python script from a template 84 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 85 | *.manifest 86 | *.spec 87 | 88 | # Installer logs 89 | pip-log.txt 90 | pip-delete-this-directory.txt 91 | 92 | # Unit test / coverage reports 93 | htmlcov/ 94 | .tox/ 95 | .coverage 96 | .coverage.* 97 | .cache 98 | nosetests.xml 99 | coverage.xml 100 | *.cover 101 | .hypothesis/ 102 | .pytest_cache/ 103 | 104 | # Translations 105 | *.mo 106 | *.pot 107 | 108 | # Django stuff: 109 | *.log 110 | local_settings.py 111 | db.sqlite3 112 | 113 | # Flask stuff: 114 | instance/ 115 | .webassets-cache 116 | 117 | # Scrapy stuff: 118 | .scrapy 119 | 120 | # Sphinx documentation 121 | docs/_build/ 122 | 123 | # PyBuilder 124 | target/ 125 | 126 | # Jupyter Notebook 127 | .ipynb_checkpoints 128 | 129 | # pyenv 130 | .python-version 131 | 132 | # celery beat schedule file 133 | celerybeat-schedule 134 | 135 | # SageMath parsed files 136 | *.sage.py 137 | 138 | # Environments 139 | .env 140 | .venv 141 | env/ 142 | venv/ 143 | ENV/ 144 | env.bak/ 145 | venv.bak/ 146 | 147 | # Spyder project settings 148 | .spyderproject 149 | .spyproject 150 | 151 | # Rope project settings 152 | .ropeproject 153 | 154 | # mkdocs documentation 155 | /site 156 | 157 | # mypy 158 | .mypy_cache/ 159 | ### Linux template 160 | *~ 161 | 162 | # temporary files which can be created if a process still has a handle open of a deleted file 163 | .fuse_hidden* 164 | 165 | # KDE directory preferences 166 | .directory 167 | 168 | # Linux trash folder which might appear on any partition or disk 169 | .Trash-* 170 | 171 | # .nfs files are created when an open file is removed but is still being accessed 172 | .nfs* 173 | 174 | ### VisualStudioCode template 175 | .vscode 176 | 177 | ### Jebrains template 178 | .idea 179 | ### Dynaconf config 180 | **/*.local.yml 181 | **/.secrets.yml 182 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | 3 | COPY ./requirements.txt /app/ 4 | 5 | WORKDIR /app 6 | 7 | RUN pip install --no-cache-dir -U -r requirements.txt 8 | 9 | COPY ./src /app 10 | 11 | EXPOSE 5000 12 | 13 | ENTRYPOINT ["python"] 14 | -------------------------------------------------------------------------------- /Dockerfile.aipro: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | 3 | COPY ./requirements.txt /app/ 4 | 5 | WORKDIR /app 6 | 7 | RUN pip install --no-cache-dir -U -r requirements.txt 8 | 9 | COPY ./src /app 10 | 11 | EXPOSE 5000 12 | 13 | ENTRYPOINT ["python", "aipro.py"] 14 | -------------------------------------------------------------------------------- /Dockerfile.popai: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | 3 | COPY ./requirements.txt /app/ 4 | 5 | WORKDIR /app 6 | 7 | RUN pip install --no-cache-dir -U -r requirements.txt 8 | 9 | COPY ./src /app 10 | 11 | EXPOSE 5000 12 | 13 | ENTRYPOINT ["python", "popai.py"] 14 | -------------------------------------------------------------------------------- /Dockerfile.wrtnai: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | 3 | COPY ./requirements.txt /app/ 4 | 5 | WORKDIR /app 6 | 7 | RUN pip install --no-cache-dir -U -r requirements.txt 8 | 9 | COPY ./src /app 10 | 11 | EXPOSE 5000 12 | 13 | ENTRYPOINT ["python", "wrtnai.py"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 用户手册 2 | 3 | ## docker 启动 4 | 5 | ```bash 6 | 7 | docker run --name popai --restart=always -d -p 8888:5000 -e AUTHORIZATION= -e GTOKEN= wmymz/chat2api popai.py 8 | 9 | #aipro 10 | docker run --name aipro --restart=always -d -p 8881:5000 -e PYTHONUNBUFFERED=1 -e PROXY=http://proxyip:port wmymz/aipro 11 | # popai 12 | docker run --name popai --restart=always -d -p 8882:5000 -e PYTHONUNBUFFERED=1 -e PROXY=http://proxyip:port -e AUTHORIZATION= -e GTOKEN= wmymz/popai 13 | # wrtnai 14 | docker run --name wrtnai --restart=always -d -p 8883:5000 -e PYTHONUNBUFFERED=1 -e PROXY=http://proxyip:port -e REFRESH_TOKEN= wmymz/wrtnai 15 | 16 | ``` 17 | 18 | ## docker compose 19 | 20 | ```yaml 21 | services: 22 | aipro: 23 | image: wmymz/chat2api:latest 24 | command: popai.py 25 | restart: always 26 | ports: 27 | - "8881:5000" 28 | environment: 29 | PYTHONUNBUFFERED: 1 30 | PROXY: "http://proxyip:port" 31 | popai: 32 | image: wmymz/chat2api:latest 33 | command: popai.py 34 | restart: always 35 | ports: 36 | - "8882:5000" 37 | environment: 38 | PYTHONUNBUFFERED: 1 39 | PROXY: "http://proxyip:port" 40 | AUTHORIZATION: "" 41 | GTOKEN: "" 42 | wrtnai: 43 | image: wmymz/chat2api:latest 44 | command: wrtnai.py 45 | restart: always 46 | ports: 47 | - "8883:5000" 48 | environment: 49 | PYTHONUNBUFFERED: 1 50 | PROXY: "http://proxyip:port" 51 | REFRESH_TOKEN: "" 52 | ``` 53 | 54 | # 开发手册 55 | 56 | ## 开发步骤 57 | 58 | > 具体请参考aipro的demo 59 | 60 | * 实现APIClient 61 | * 实现ChatServer 62 | * 使用Chat2API糊一个接口 63 | 64 | ## 打包docker镜像 65 | 66 | ```bash 67 | # 构建镜像,上传Docker hub 68 | docker compose build --no-cache 69 | docker compose push 70 | ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | chat2api: 3 | image: wmymz/chat2api:latest 4 | build: . 5 | aipro: 6 | image: wmymz/aipro:latest 7 | build: 8 | context: . 9 | dockerfile: Dockerfile.aipro 10 | popai: 11 | image: wmymz/popai:latest 12 | build: 13 | context: . 14 | dockerfile: Dockerfile.popai 15 | wrtnai: 16 | image: wmymz/wrtnai:latest 17 | build: 18 | context: . 19 | dockerfile: Dockerfile.wrtnai 20 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "certifi" 5 | version = "2024.2.2" 6 | description = "Python package for providing Mozilla's CA Bundle." 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 11 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 12 | ] 13 | 14 | [package.source] 15 | type = "legacy" 16 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 17 | reference = "tsinghua" 18 | 19 | [[package]] 20 | name = "charset-normalizer" 21 | version = "3.3.2" 22 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 23 | optional = false 24 | python-versions = ">=3.7.0" 25 | files = [ 26 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 27 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 28 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 29 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 30 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 31 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 32 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 33 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 34 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 35 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 36 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 37 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 38 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 39 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 40 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 41 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 42 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 43 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 44 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 45 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 46 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 47 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 48 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 49 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 50 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 51 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 52 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 53 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 54 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 55 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 56 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 57 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 58 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 59 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 60 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 61 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 62 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 63 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 64 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 65 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 66 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 67 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 68 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 69 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 70 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 71 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 72 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 73 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 74 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 75 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 76 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 77 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 78 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 79 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 80 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 81 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 82 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 83 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 84 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 85 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 86 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 87 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 88 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 89 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 90 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 91 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 92 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 93 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 94 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 95 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 96 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 97 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 98 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 99 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 100 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 101 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 102 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 103 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 104 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 105 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 106 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 107 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 108 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 109 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 110 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 111 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 112 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 113 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 114 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 115 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 116 | ] 117 | 118 | [package.source] 119 | type = "legacy" 120 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 121 | reference = "tsinghua" 122 | 123 | [[package]] 124 | name = "fastapi" 125 | version = "0.110.2" 126 | description = "" 127 | optional = false 128 | python-versions = "*" 129 | files = [ 130 | {file = "fastapi-0.110.2-py3-none-any.whl", hash = "sha256:239403f2c0a3dda07a9420f95157a7f014ddb2b770acdbc984f9bdf3ead7afdb"}, 131 | {file = "fastapi-0.110.2.tar.gz", hash = "sha256:b53d673652da3b65e8cd787ad214ec0fe303cad00d2b529b86ce7db13f17518d"}, 132 | ] 133 | 134 | [package.source] 135 | type = "legacy" 136 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 137 | reference = "tsinghua" 138 | 139 | [[package]] 140 | name = "idna" 141 | version = "3.7" 142 | description = "Internationalized Domain Names in Applications (IDNA)" 143 | optional = false 144 | python-versions = ">=3.5" 145 | files = [ 146 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 147 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 148 | ] 149 | 150 | [package.source] 151 | type = "legacy" 152 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 153 | reference = "tsinghua" 154 | 155 | [[package]] 156 | name = "requests" 157 | version = "2.31.0" 158 | description = "Python HTTP for Humans." 159 | optional = false 160 | python-versions = ">=3.7" 161 | files = [ 162 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 163 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 164 | ] 165 | 166 | [package.dependencies] 167 | certifi = ">=2017.4.17" 168 | charset-normalizer = ">=2,<4" 169 | idna = ">=2.5,<4" 170 | urllib3 = ">=1.21.1,<3" 171 | 172 | [package.extras] 173 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 174 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 175 | 176 | [package.source] 177 | type = "legacy" 178 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 179 | reference = "tsinghua" 180 | 181 | [[package]] 182 | name = "urllib3" 183 | version = "2.2.1" 184 | description = "HTTP library with thread-safe connection pooling, file post, and more." 185 | optional = false 186 | python-versions = ">=3.8" 187 | files = [ 188 | {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, 189 | {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, 190 | ] 191 | 192 | [package.extras] 193 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 194 | h2 = ["h2 (>=4,<5)"] 195 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 196 | zstd = ["zstandard (>=0.18.0)"] 197 | 198 | [package.source] 199 | type = "legacy" 200 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 201 | reference = "tsinghua" 202 | 203 | [[package]] 204 | name = "uvicorn" 205 | version = "0.29.0" 206 | description = "" 207 | optional = false 208 | python-versions = "*" 209 | files = [ 210 | {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, 211 | {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, 212 | ] 213 | 214 | [package.source] 215 | type = "legacy" 216 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 217 | reference = "tsinghua" 218 | 219 | [metadata] 220 | lock-version = "2.0" 221 | python-versions = ">=3.12,<3.13" 222 | content-hash = "4ddedc4d01198a878e0bedf0401b37dec6a351fc5b11c1d7abb247b9b07fa0da" 223 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "chat2api" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["wmymz "] 6 | readme = "README.md" 7 | packages = [{ include = "chat2api", from = "src" }] 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.12,<3.13" 11 | requests = "^2.31.0" 12 | fastapi = "^0.110.2" 13 | uvicorn = "^0.29.0" 14 | starlette = "^0.37.2" 15 | click = "^8.1.7" 16 | pydantic = "^2.7.1" 17 | typing-extensions = "^4.11.0" 18 | pydantic-core = "^2.18.2" 19 | annotated-types = "^0.6.0" 20 | h11 = "^0.14.0" 21 | 22 | 23 | [build-system] 24 | requires = ["poetry-core"] 25 | build-backend = "poetry.core.masonry.api" 26 | 27 | [[tool.poetry.source]] 28 | name = "tsinghua" 29 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 30 | priority = "default" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.110.2 2 | requests==2.31.0 3 | uvicorn==0.29.0 4 | -------------------------------------------------------------------------------- /src/aipro.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import requests 5 | import uvicorn 6 | from fastapi import FastAPI, Request, Response 7 | from starlette.middleware.cors import CORSMiddleware 8 | 9 | from chat2api import Chat2API 10 | from chat2api.api import OpenaiAPI 11 | from chat2api.chat import ChatServer 12 | from chat2api.util import LRUCache 13 | from chat2api.util import now 14 | 15 | 16 | class AiProChat(ChatServer): 17 | MODELS = { 18 | 'gpt-3.5-turbo': 'https://chatpro.ai-pro.org/api/ask/openAI', 19 | 'gpt-4-1106-preview': 'https://chatpro.ai-pro.org/api/ask/openAI', 20 | 'gpt-4-pro-max': 'https://chatpro.ai-pro.org/api/ask/openAI', 21 | 22 | 'chat-bison': 'https://chatpro.ai-pro.org/api/ask/google', 23 | 'text-bison': 'https://chatpro.ai-pro.org/api/ask/google', 24 | 'codechat-bison': 'https://chatpro.ai-pro.org/api/ask/google', 25 | 26 | 'openchat_3.5': 'https://chatpro.ai-pro.org/api/ask/Opensource', 27 | 'zephyr-7B-beta': 'https://chatpro.ai-pro.org/api/ask/Opensource', 28 | } 29 | 30 | def __init__(self, client: OpenaiAPI): 31 | self.client = client 32 | 33 | @staticmethod 34 | def get_url(model): 35 | if model in AiProChat.MODELS: 36 | return AiProChat.MODELS[model] 37 | return AiProChat.MODELS['gpt-4-pro-max'] 38 | 39 | def answer_stream(self): 40 | question = self.client.question 41 | model = self.client.model 42 | context_id = None 43 | if len(self.client.messages) > 2: 44 | # 上下文 45 | for msg in self.client.messages: 46 | if msg['role'] == 'user': 47 | context_id = FIND_CHAT_BY_QUESTION.get(msg['content']) 48 | break 49 | stream = self.client.stream 50 | 51 | print('-' * 30, '\n') 52 | print('question: \n', question) 53 | print('-' * 30, '\n') 54 | print('answer: ') 55 | 56 | url = AiProChat.get_url(model) 57 | endpoint = url[url.rfind('/') + 1:] 58 | req_json = { 59 | "conversationId": context_id, 60 | 'parentMessageId': FIND_LAST_MSG_IN_CHAT.get(context_id, '00000000-0000-0000-0000-000000000000'), 61 | "text": question, 62 | "endpoint": endpoint, 63 | "model": model 64 | } 65 | headers = { 66 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0', 67 | 'Origin': 'https://chatpro.ai-pro.org', 68 | 'Referer': 'https://chatpro.ai-pro.org/chat/new', 69 | } 70 | proxy = os.environ.get('PROXY') 71 | if proxy: 72 | proxies = {'all': proxy} 73 | else: 74 | proxies = None 75 | resp = requests.post(url, json=req_json, headers=headers, proxies=proxies, stream=True) 76 | resp.raise_for_status() 77 | resp.encoding = 'utf-8' 78 | last_text = '' 79 | lines = resp.iter_lines(decode_unicode=True) 80 | for data in lines: 81 | # 首条消息包含对话信息 82 | if data.startswith('data'): 83 | infos = json.loads(data[6:]) 84 | context_id = infos['message']['conversationId'] 85 | FIND_CHAT_BY_QUESTION[question] = context_id 86 | if stream: 87 | msg_id = infos['message']['messageId'] 88 | FIND_LAST_MSG_IN_CHAT[context_id] = msg_id 89 | break 90 | for data in lines: 91 | if data.startswith('data'): 92 | infos = json.loads(data[6:]) 93 | if 'text' in infos: 94 | text = infos['text'] 95 | word = text[len(last_text):] 96 | print(word, end='') 97 | yield word 98 | last_text = text 99 | print() 100 | 101 | 102 | class AiProDraw(ChatServer): 103 | def __init__(self, client: OpenaiAPI): 104 | self.client = client 105 | 106 | def answer_stream(self): 107 | yield "这是图片:\n" 108 | print('prompt: \n', self.client.question) 109 | headers = { 110 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0', 111 | 'Origin': 'https://chatpro.ai-pro.org', 112 | 'Referer': 'https://chatpro.ai-pro.org/chat/new', 113 | } 114 | resp = requests.post( 115 | 'https://app.ai-pro.org/api/aipsd/v2/dream-photo-create', 116 | headers=headers, 117 | data={ 118 | 'payload': json.dumps( 119 | { 120 | "positive": f"{self.client.question}", 121 | "negative": "", 122 | "height": "512", 123 | "width": "512", 124 | "model": "RealitiesEdgeXL_4" 125 | } 126 | ), 127 | 'slug': 'dream-photo', 128 | 'surfToken': '51dc3d9891224881' 129 | } 130 | ) 131 | resp.raise_for_status() 132 | for base_img in resp.json()['data']['images']: 133 | # todo 图床 134 | print('img: \n', f"{base_img[:10]}******") 135 | yield f"![asd](data:img/png;base64,{base_img})" 136 | 137 | 138 | class AiPro(ChatServer): 139 | def __init__(self, client: OpenaiAPI): 140 | self.client = client 141 | self.chat = AiProChat(client) 142 | self.draw = AiProDraw(client) 143 | 144 | def answer_stream(self): 145 | if self.client.question.startswith('画图:'): 146 | return self.draw.answer_stream() 147 | else: 148 | return self.chat.answer_stream() 149 | 150 | 151 | app = FastAPI(title="AIPro Chat", description="AIPro Chat") 152 | 153 | app.add_middleware( 154 | CORSMiddleware, 155 | allow_origins=["*"], 156 | allow_credentials=True, 157 | allow_methods=["*"], 158 | allow_headers=["*"], 159 | ) 160 | 161 | 162 | @app.get('/v1/models') 163 | def list_models(): 164 | return { 165 | "object": "list", 166 | "data": [{ 167 | "id": m, 168 | "object": "model", 169 | "created": now(), 170 | "owned_by": AiProChat.MODELS[m].rsplit('/', 1) 171 | } for m in AiProChat.MODELS] 172 | } 173 | 174 | 175 | @app.options('/v1/chat/completions') 176 | async def pre_chat(): 177 | return Response() 178 | 179 | 180 | @app.post('/v1/chat/completions') 181 | async def chat(request: Request): 182 | cli = OpenaiAPI() 183 | ser = AiPro(cli) 184 | return await Chat2API(cli, ser).response(request) 185 | 186 | 187 | if __name__ == '__main__': 188 | FIND_CHAT_BY_QUESTION = LRUCache(1000) 189 | FIND_LAST_MSG_IN_CHAT = LRUCache(1000) 190 | uvicorn.run(app, host='0.0.0.0', port=5000) 191 | -------------------------------------------------------------------------------- /src/chat2api/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | 3 | from chat2api.api import APIClient 4 | from chat2api.chat import ChatServer 5 | 6 | 7 | class Chat2API: 8 | def __init__(self, client: APIClient, server: ChatServer): 9 | self.client = client 10 | self.server = server 11 | 12 | async def response(self, request: Request): 13 | await self.client.parse_request(request) 14 | return self.client.response(self.server.answer_stream()) 15 | -------------------------------------------------------------------------------- /src/chat2api/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from abc import abstractmethod 3 | from typing import Iterable 4 | 5 | from fastapi import Request 6 | from starlette.responses import StreamingResponse 7 | 8 | from chat2api.util import now 9 | 10 | 11 | class APIClient: 12 | 13 | @abstractmethod 14 | async def parse_request(self, *args, **kwargs): 15 | pass 16 | 17 | @abstractmethod 18 | def response(self, *args, **kwargs): 19 | pass 20 | 21 | 22 | class OpenaiAPI(APIClient): 23 | def __init__(self): 24 | self.messages = None 25 | self.question = None 26 | self.model = None 27 | self.stream = None 28 | 29 | async def parse_request(self, request: Request, *args, **kwargs): 30 | request_json = await request.json() 31 | self.messages = request_json['messages'] 32 | 33 | self.question = request_json['messages'][-1]['content'] 34 | self.model = request_json['model'] 35 | self.stream = request_json.get('stream') 36 | 37 | def response(self, stream): 38 | if self.stream: 39 | return StreamingResponse(self.response_stream(stream), media_type="text/event-stream") 40 | else: 41 | return self.response_sync(''.join(stream)) 42 | 43 | def response_stream(self, words: Iterable): 44 | for word in words: 45 | yield 'data: ' + json.dumps({ 46 | 'id': f'chatcmpl-{now()}', 47 | 'object': "chat.completion.chunk", 48 | 'created': now(), 49 | 'model': self.model, 50 | 'choices': [{ 51 | 'index': 0, 52 | 'delta': { 53 | "role": "assistant", 54 | 'content': word, 55 | }, 56 | 'finish_reason': None if word else 'stop' 57 | }] 58 | }, ensure_ascii=False) + '\n\n' 59 | yield 'data: [DONE]\n\n' 60 | 61 | def response_sync(self, words: str): 62 | return { 63 | 'id': f'chatcmpl-{now()}', 64 | 'object': "chat.completion", 65 | 'created': now(), 66 | 'model': self.model, 67 | 'choices': [{ 68 | 'index': 0, 69 | 'message': { 70 | "role": "assistant", 71 | 'content': words, 72 | }, 73 | }] 74 | } 75 | -------------------------------------------------------------------------------- /src/chat2api/chat.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | 4 | class ChatServer: 5 | 6 | @abstractmethod 7 | def answer_stream(self, *args, **kwargs): 8 | pass 9 | -------------------------------------------------------------------------------- /src/chat2api/util.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import time 3 | from collections import OrderedDict 4 | 5 | 6 | def is_summary(text: str) -> bool: 7 | if '请总结上述对话为10个字以内的标题,不需要包含标点符号' in text: 8 | return True 9 | if '使用四到五个字直接返回这句话的简要主题' in text: 10 | return True 11 | return False 12 | 13 | 14 | def now(): 15 | return int(time.time()) 16 | 17 | 18 | def md5(msg: str): 19 | if isinstance(msg, str): 20 | hash_obj = hashlib.md5() 21 | hash_obj.update(msg.encode()) 22 | return hash_obj.hexdigest() 23 | 24 | 25 | class LRUCache: 26 | def __init__(self, capacity, key_hash_func=md5): 27 | self.cache = OrderedDict() 28 | self.capacity = capacity 29 | self.key_hash_func = key_hash_func 30 | 31 | def __getitem__(self, key): 32 | if self.key_hash_func: 33 | key = self.key_hash_func(key) 34 | if key not in self.cache: 35 | return None 36 | else: 37 | # 将元素移到字典末尾表示最近访问 38 | self.cache.move_to_end(key) 39 | return self.cache[key] 40 | 41 | def get(self, key, default=None): 42 | value = self[key] 43 | return value if value is not None else default 44 | 45 | def __setitem__(self, key, value): 46 | if self.key_hash_func: 47 | key = self.key_hash_func(key) 48 | if key in self.cache: 49 | # 更新键值,并将元素移到字典末尾 50 | self.cache.move_to_end(key) 51 | self.cache[key] = value 52 | if len(self.cache) > self.capacity: 53 | # 弹出字典开头的元素,即最久未访问的元素 54 | self.cache.popitem(last=False) 55 | -------------------------------------------------------------------------------- /src/popai.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import requests 5 | import uvicorn 6 | from fastapi import Request, Response, FastAPI 7 | from starlette.middleware.cors import CORSMiddleware 8 | 9 | from chat2api import ChatServer, Chat2API 10 | from chat2api.api import OpenaiAPI 11 | from chat2api.util import now, LRUCache, is_summary 12 | 13 | 14 | class PopAi(ChatServer): 15 | MODELS = { 16 | 'gpt-3.5': 'Standard', 17 | 'gpt-4': 'GPT-4', 18 | 'internet': 'Web Search', 19 | 'draw': 'Image generation', 20 | } 21 | 22 | def __init__(self, client: OpenaiAPI, authorization, gtoken): 23 | self.client = client 24 | self.authorization = authorization 25 | self.gtoken = gtoken 26 | 27 | def answer_stream(self): 28 | question = self.client.question 29 | if is_summary(question): 30 | model = PopAi.MODELS['gpt-3.5'] 31 | else: 32 | model = PopAi.MODELS.get(self.client.model, 'GPT-4') 33 | proxy = os.environ.get("PROXY") 34 | if proxy: 35 | proxies = {'all': proxy} 36 | else: 37 | proxies = None 38 | context_id = None 39 | if len(self.client.messages) > 2 and not is_summary(question): 40 | # 上下文 41 | for msg in self.client.messages: 42 | if msg['role'] == 'user': 43 | context_id = FIND_CHAT_BY_QUESTION.get(msg['content']) 44 | if context_id: 45 | break 46 | 47 | if context_id is None: 48 | channel_resp = requests.post( 49 | 'https://api.popai.pro/api/v1/chat/getChannel', 50 | headers={'Authorization': self.authorization}, 51 | json={ 52 | "model": model, 53 | "templateId": "", 54 | "message": question, 55 | "language": "English", 56 | "fileType": None 57 | }, 58 | proxies=proxies, 59 | ) 60 | channel_resp.raise_for_status() 61 | context_id = channel_resp.json()['data']['channelId'] 62 | 63 | print('-' * 30, '\n') 64 | print('question: \n', question) 65 | print('-' * 30, '\n') 66 | print('answer: ') 67 | 68 | url = 'https://api.popai.pro/api/v1/chat/send' 69 | headers = { 70 | "accept": "text/event-stream", 71 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,zh-HK;q=0.6", 72 | "app-name": "popai-web", 73 | "authorization": self.authorization, 74 | "content-type": "application/json", 75 | "device-info": "{web_id:k-s8Xp4S9LEmrHghBhT2m,baidu_id:18f1ff567e243687188711}", 76 | "gtoken": self.gtoken, 77 | "language": "en", 78 | "origin": "https://www.popai.pro", 79 | "priority": "u=1, i", 80 | "referer": "https://www.popai.pro/", 81 | "sec-ch-ua-mobile": "?0", 82 | "sec-ch-ua-platform": "Windows", 83 | "sec-fetch-mode": "cors", 84 | "sec-fetch-site": "same-site", 85 | } 86 | 87 | req_json = { 88 | "isGetJson": True, 89 | "version": "1.3.6", 90 | "language": "zh-CN", 91 | "channelId": context_id, 92 | "message": question, 93 | "model": model, 94 | "messageIds": [], 95 | "improveId": None, 96 | "richMessageId": None, 97 | "isNewChat": False, 98 | "action": None, 99 | "isGeneratePpt": False, 100 | "isSlidesChat": False, 101 | "imageUrls": [], 102 | "roleEnum": None, 103 | "pptCoordinates": "", 104 | "translateLanguage": None, 105 | "docPromptTemplateId": None 106 | } 107 | resp = requests.post(url, json=req_json, headers=headers, proxies=proxies, stream=True) 108 | resp.raise_for_status() 109 | resp.encoding = 'utf-8' 110 | lines = resp.iter_lines(decode_unicode=True) 111 | for data in lines: 112 | # 首条消息是用户提问,舍掉 113 | if data.startswith('data:'): 114 | infos = json.loads(data[5:]) 115 | context_id = infos[0].get('channelId') 116 | FIND_CHAT_BY_QUESTION[question] = context_id 117 | break 118 | for data in lines: 119 | if data.startswith('data:'): 120 | infos = json.loads(data[5:]) 121 | for info in infos: 122 | # msg_id = info.get('messageId') 123 | word = info.get('content') 124 | if word: 125 | print(word, end='') 126 | yield word 127 | print() 128 | 129 | 130 | app = FastAPI(title="PopAi Chat", description="PopAi Chat") 131 | 132 | app.add_middleware( 133 | CORSMiddleware, 134 | allow_origins=["*"], 135 | allow_credentials=True, 136 | allow_methods=["*"], 137 | allow_headers=["*"], 138 | ) 139 | 140 | 141 | @app.get('/v1/models') 142 | def list_models(): 143 | return { 144 | "object": "list", 145 | "data": [{ 146 | "id": m, 147 | "object": "model", 148 | "created": now(), 149 | "owned_by": "popai" 150 | } for m in PopAi.MODELS] 151 | } 152 | 153 | 154 | @app.options('/v1/chat/completions') 155 | async def pre_chat(): 156 | return Response() 157 | 158 | 159 | @app.post('/v1/chat/completions') 160 | async def chat(request: Request): 161 | cli = OpenaiAPI() 162 | ser = PopAi(cli, AUTHORIZATION, GTOKEN) 163 | return await Chat2API(cli, ser).response(request) 164 | 165 | 166 | if __name__ == '__main__': 167 | AUTHORIZATION = os.environ.get('AUTHORIZATION') 168 | GTOKEN = os.environ.get('GTOKEN') 169 | assert AUTHORIZATION, 'AUTHORIZATION must be set' 170 | assert GTOKEN, 'GTOKEN must be set' 171 | FIND_CHAT_BY_QUESTION = LRUCache(1000) 172 | 173 | uvicorn.run(app, host='0.0.0.0', port=5000) 174 | -------------------------------------------------------------------------------- /src/wrtnai.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import base64 4 | import time 5 | 6 | import requests 7 | import uvicorn 8 | from fastapi import Request, Response, FastAPI 9 | from starlette.middleware.cors import CORSMiddleware 10 | 11 | from chat2api import ChatServer, Chat2API 12 | from chat2api.api import OpenaiAPI 13 | from chat2api.util import now, LRUCache, is_summary 14 | 15 | 16 | class WrtnAi(ChatServer): 17 | MODELS = ['gpt4', 'gpt3.5', 'gpt3.5_16k', 'palm2', 'gpt4v', 'gpt4t', 'wrtn_search', 'claude_instant', 'claude2.1', 18 | 'sdxl', 'sdxl_beta', 'sdxl_jp', 'dalle3', 'haiku', 'sonnet', 'GPT4', 'GPT3.5', 'GPT3.5_16K', 'PALM2', 19 | 'fine-tune-blog', 'GPT4V', 'GPT4T', 'WRTN_SEARCH', 'CLAUDE_INSTANT', 'CLAUDE2.1', 'HAIKU', 'SONNET', 20 | 'stable-diffusion-xl-beta-v2-2-2', 'SDXL', 'SDXL_JP', 'DALLE3'] 21 | 22 | def __init__(self, client: OpenaiAPI, refresh_token, proxies): 23 | self.client = client 24 | self.refresh_token = refresh_token 25 | self.access_token = None 26 | self.user_id = None 27 | self.unit_id = None 28 | self.user_email = None 29 | self.session_arg = None 30 | self.proxies = proxies 31 | self.get_access_token() 32 | 33 | def get_unit_id(self): 34 | if self.unit_id: 35 | return self.unit_id 36 | url = 'https://api.wrtn.ai/be/chat' 37 | headers = { 38 | 'Accept': 'application/json', 39 | 'Authorization': 'Bearer {}'.format(self.get_access_token()), 40 | 'Origin': 'https://wrtn.ai', 41 | 'Platform': 'web', 42 | 'Priority': 'u=1, i', 43 | 'Referer': 'https://wrtn.ai/', 44 | 'Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', 45 | 'Sec-Ch-Ua-Mobile': '?0', 46 | 'Sec-Ch-Ua-Platform': '"Windows"', 47 | 'Sec-Fetch-Dest': 'empty', 48 | 'Sec-Fetch-Mode': 'cors', 49 | 'Sec-Fetch-Site': 'same-site', 50 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 51 | 'Wrtn-Locale': 'ko-KR' 52 | } 53 | resp = requests.get(url, headers=headers, proxies=self.proxies) 54 | resp.raise_for_status() 55 | chats = resp.json()['data'] 56 | if len(chats) == 0: 57 | raise Exception('首次需要在官网创建一个对话') 58 | self.unit_id = chats[0]['tempUnit']['_id'] 59 | return self.unit_id 60 | 61 | def is_expired(self): 62 | if not self.access_token: 63 | return True 64 | message, signature = self.access_token.rsplit('.', 1) 65 | header, payload = message.split('.') 66 | payload = payload + '=' * - (len(payload) % - 4) 67 | # signature = signature + '=' * - (len(signature) % - 4) 68 | exp = json.loads(base64.b64decode(payload).decode()).get('exp') 69 | return exp - time.time() < 60 70 | 71 | def get_access_token(self): 72 | if not self.is_expired(): 73 | return self.access_token 74 | url = 'https://api.wrtn.ai/be/auth/refresh' 75 | headers = { 76 | 'Content-Type': 'application/x-www-form-urlencoded', 77 | 'Origin': 'https://wrtn.ai', 78 | 'Referer': 'https://wrtn.ai/', 79 | 'Refresh': self.refresh_token, 80 | 'Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', 81 | 'Sec-Ch-Ua-Mobile': '?0', 82 | 'Sec-Ch-Ua-Platform': '"Windows"', 83 | 'Sec-Fetch-Dest': 'empty', 84 | 'Sec-Fetch-Mode': 'cors', 85 | 'Sec-Fetch-Site': 'same-site', 86 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 87 | } 88 | resp = requests.post(url, headers=headers, proxies=self.proxies) 89 | resp.raise_for_status() 90 | self.access_token = resp.json()['data']['accessToken'] 91 | message, signature = self.access_token.rsplit('.', 1) 92 | header, payload = message.split('.') 93 | payload = payload + '=' * - (len(payload) % - 4) 94 | payload_json = json.loads(base64.b64decode(payload).decode()) 95 | self.user_id = payload_json.get('id') 96 | self.user_email = payload_json.get('email') 97 | return self.access_token 98 | 99 | def get_session_arg(self): 100 | if self.session_arg: 101 | return self.session_arg 102 | url = 'https://api.wrtn.ai/be/chat' 103 | headers = { 104 | 'Accept': 'application/json', 105 | 'Authorization': 'Bearer {}'.format(self.get_access_token()), 106 | 'Origin': 'https://wrtn.ai', 107 | 'Platform': 'web', 108 | 'Priority': 'u=1, i', 109 | 'Referer': 'https://wrtn.ai/', 110 | 'Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', 111 | 'Sec-Ch-Ua-Mobile': '?0', 112 | 'Sec-Ch-Ua-Platform': '"Windows"', 113 | 'Sec-Fetch-Dest': 'empty', 114 | 'Sec-Fetch-Mode': 'cors', 115 | 'Sec-Fetch-Site': 'same-site', 116 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 117 | 'Wrtn-Locale': 'ko-KR' 118 | } 119 | resp = requests.post(url, headers=headers, proxies=self.proxies, 120 | json={"unitId": f"{self.get_unit_id()}", "type": "model"}) 121 | resp.raise_for_status() 122 | self.session_arg = resp.json()['data']['_id'] 123 | return self.session_arg 124 | 125 | def get_message_arg(self, question, model): 126 | url = f'https://william.wow.wrtn.ai/chat/v3/{self.get_session_arg()}/start?platform=web&user={self.user_email}&model={model}' 127 | headers = { 128 | 'Authorization': 'Bearer {}'.format(self.get_access_token()), 129 | 'Accept': 'application/json', 130 | 'Origin': 'https://wrtn.ai', 131 | 'Platform': 'web', 132 | 'Priority': 'u=1, i', 133 | 'Referer': 'https://wrtn.ai/', 134 | 'Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', 135 | 'Sec-Ch-Ua-Mobile': '?0', 136 | 'Sec-Ch-Ua-Platform': '"Windows"', 137 | 'Sec-Fetch-Dest': 'empty', 138 | 'Sec-Fetch-Mode': 'cors', 139 | 'Sec-Fetch-Site': 'same-site', 140 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 141 | 'Wrtn-Locale': 'ko-KR', 142 | } 143 | resp = requests.post(url, headers=headers, proxies=self.proxies, 144 | json={"message": f"{question}", "reroll": False, "images": []}) 145 | resp.raise_for_status() 146 | return resp.json()['data'] 147 | 148 | def answer_stream(self): 149 | question = self.client.question 150 | model = self.client.model 151 | if model not in self.MODELS: 152 | model = 'gpt4' 153 | if is_summary(question): 154 | yield '闲聊' 155 | else: 156 | url = f'https://william.wow.wrtn.ai/chat/v3/{self.get_session_arg()}/{self.get_message_arg(question, model)}?model={model}&platform=web&user={self.user_email}&isChocoChip=false' 157 | headers = { 158 | 'Authorization': 'Bearer {}'.format(self.get_access_token()), 159 | 'Accept': 'text/event-stream', 160 | 'Origin': 'https://wrtn.ai', 161 | 'Priority': 'u=1, i', 162 | 'Referer': 'https://wrtn.ai/', 163 | 'Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', 164 | 'Sec-Ch-Ua-Mobile': '?0', 165 | 'Sec-Ch-Ua-Platform': '"Windows"', 166 | 'Sec-Fetch-Dest': 'empty', 167 | 'Sec-Fetch-Mode': 'cors', 168 | 'Sec-Fetch-Site': 'same-site', 169 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 170 | 'Wrtn-Locale': 'ko-KR', 171 | } 172 | resp = requests.get(url, headers=headers, proxies=self.proxies, stream=True) 173 | resp.raise_for_status() 174 | resp.encoding = 'utf-8' 175 | lines = resp.iter_lines(decode_unicode=True) 176 | for data in lines: 177 | if data.startswith('data:'): 178 | info = json.loads(data[5:]) 179 | word = info.get('chunk') 180 | if word: 181 | print(word, end='') 182 | yield word 183 | 184 | 185 | app = FastAPI(title="PopAi Chat", description="PopAi Chat") 186 | 187 | app.add_middleware( 188 | CORSMiddleware, 189 | allow_origins=["*"], 190 | allow_credentials=True, 191 | allow_methods=["*"], 192 | allow_headers=["*"], 193 | ) 194 | 195 | 196 | @app.get('/v1/models') 197 | def list_models(): 198 | return { 199 | "object": "list", 200 | "data": [{ 201 | "id": m, 202 | "object": "model", 203 | "created": now(), 204 | "owned_by": "popai" 205 | } for m in WrtnAi.MODELS] 206 | } 207 | 208 | 209 | @app.options('/v1/chat/completions') 210 | async def pre_chat(): 211 | return Response() 212 | 213 | 214 | @app.post('/v1/chat/completions') 215 | async def chat(request: Request): 216 | return await chat2api_server.response(request) 217 | 218 | 219 | if __name__ == '__main__': 220 | REFRESH_TOKEN = os.getenv('REFRESH_TOKEN') 221 | assert REFRESH_TOKEN, 'REFRESH_TOKEN must be set' 222 | FIND_CHAT_BY_QUESTION = LRUCache(1000) 223 | PROXY = os.environ.get("PROXY") 224 | if PROXY: 225 | PROXY = {'all': PROXY} 226 | else: 227 | PROXY = None 228 | 229 | cli = OpenaiAPI() 230 | chat2api_server = Chat2API(cli, WrtnAi(cli, REFRESH_TOKEN, PROXY)) 231 | uvicorn.run(app, host='0.0.0.0', port=5000) 232 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qy527145/chat2api/4df62dbf2d866272ffbfa47e938f036e4dc5a1ce/tests/__init__.py --------------------------------------------------------------------------------