├── .env.example ├── requirements.txt ├── .gitmodules ├── account.example.json ├── .vscode └── settings.json ├── test.py ├── README.md ├── LICENSE ├── .gitignore └── main.py /.env.example: -------------------------------------------------------------------------------- 1 | # local or redis 2 | ADAPTER=local 3 | ACCOUNT_FILE_PATH=account.json 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.3 2 | python-socketio==5.7.1 3 | python-dotenv==0.21.0 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "PyChatGPT"] 2 | path = PyChatGPT 3 | url = git@github.com:yi-ge/PyChatGPT.git 4 | -------------------------------------------------------------------------------- /account.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "email": "a@wyr.me", 4 | "password": "password" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["aiohttp", "chatgpt", "dotenv", "pychatgpt"], 3 | "python.formatting.provider": "yapf", 4 | "python.analysis.typeCheckingMode": "basic", 5 | "editor.formatOnSave": false 6 | } 7 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from PyChatGPT.src.pychatgpt import Chat 6 | 7 | load_dotenv() 8 | env_dist = os.environ 9 | 10 | email = env_dist.get('EMAIL') 11 | password = env_dist.get('PASSWORD') 12 | 13 | if email is None or password is None: 14 | raise Exception('Email and password are not set') 15 | 16 | chat = Chat(email=email, password=password) 17 | chat.cli_chat() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT API 2 | 3 | 的后端程序。 4 | 5 | 借助OpenAPI官方Web的API实现,旨在降低中国大陆用户体验ChatGPT的成本。 6 | 7 | ## 依赖 8 | 9 | 基于 二次开发,Fork to: 10 | 11 | ## 运行 12 | 13 | 参照`.env.example`,新建`.env`文件,配置好环境变量。 14 | 参照`account.example.json`,新建`account.json`文件,配置好默认账户。 15 | 16 | ```bash 17 | python3 main.py 18 | ``` 19 | 20 | ## 部署 21 | 22 | **⚠⚠⚠请勿用于商业用途,请保留开源仓库地址链接,请自觉遵守约定。** 23 | 24 | **⚠⚠⚠由于OpenAPI于(2022年12月12日)增加了Cloudflare防护,考虑合规性,同天此项目的在线系统已被作者关停且开源项目不再更新。有体验需求的用户请访问OpenAI官网,敬请理解。请勿将本系统代码用于商业用途!** 25 | 26 | **仿冒或冒用ChatGPT、OpenAI名义开展经营活动,可能构成《商标法》、《反不正当竞争法》下的一系列侵权行为; 27 | 以之牟利造成消费者损失的,可能产生《商标法》、《反不正当竞争法》、《消费者权益保护法》下的民事或行政责任,情节严重并造成重大损失的,还有可能构成刑事犯罪; 28 | 如果提供这种跨境经营服务存在私自搭建国际信道的情形,还有可能违反《网络安全法》、《刑法》的相关规定,承担行政责任或构成刑事犯罪。** 29 | 30 | ## Disclaimers 免责声明 31 | 32 | This is not an official OpenAI product. This is a personal project and is not affiliated with OpenAI in any way. Don't sue me. 33 | 这不是官方的 OpenAI 产品。这是一个个人项目,与 OpenAI 没有任何关系。别告我。 34 | 35 | The code is for demo and testing only. 36 | 代码仅用于演示和测试。 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Yige 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 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 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 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Account List 132 | account.json 133 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import uuid 5 | from threading import Timer 6 | from urllib import parse 7 | 8 | import socketio 9 | from aiohttp import web 10 | from dotenv import load_dotenv 11 | 12 | from PyChatGPT.src.pychatgpt import Chat, Options 13 | 14 | load_dotenv() 15 | env_dist = os.environ 16 | adapter = env_dist.get('ADAPTER', '') 17 | account_file_path = env_dist.get('ACCOUNT_FILE_PATH', '') 18 | 19 | if adapter == 'local': 20 | user_uuid_set, using_uuid_set, logout_uuid_set, token_set, using_email_set = set( 21 | ), set(), set(), set(), set() 22 | timer_map, sid_uuid_map, token_email_map, email_chat_map, user_token_map = {}, {}, {}, {}, {} 23 | 24 | options = Options() 25 | 26 | # [New] Enable, Disable logs 27 | options.log = False 28 | 29 | # Track conversation 30 | options.track = True 31 | 32 | if os.path.exists(account_file_path) is False: 33 | raise Exception('See the account.example.json file') 34 | 35 | with open("./account.json", 'r') as f: 36 | account_list = json.load(f) 37 | for i in account_list: 38 | email = i.get('email') 39 | password = i.get('password') 40 | email_chat_map[email] = Chat(email=email, 41 | password=password, 42 | options=options) 43 | account_list_len = len(account_list) 44 | else: 45 | raise Exception('See the env.example file') 46 | # TODO: Redis operations 47 | 48 | sio = socketio.AsyncServer(cors_allowed_origins='*') 49 | app = web.Application() 50 | 51 | sio.attach(app) 52 | 53 | 54 | def logout(userUUID): 55 | if userUUID in logout_uuid_set: 56 | if userUUID in user_uuid_set: user_uuid_set.remove(userUUID) 57 | if userUUID in using_uuid_set: using_uuid_set.remove(userUUID) 58 | logout_uuid_set.remove(userUUID) 59 | try: 60 | token = user_token_map[userUUID] 61 | email = token_email_map[token] 62 | token_set.remove(token) 63 | using_email_set.remove(email) 64 | del user_token_map[userUUID] 65 | del timer_map[userUUID] 66 | asyncio.run(broadcastSystemInfo()) 67 | except Exception as e: 68 | print(e) 69 | 70 | 71 | async def broadcastSystemInfo(): 72 | onlineUserNum = len(user_uuid_set) 73 | waitingUserNum = onlineUserNum - len(using_uuid_set) 74 | await sio.emit( 75 | 'systemInfo', { 76 | 'onlineUserNum': onlineUserNum if onlineUserNum > 1 else 1, 77 | 'waitingUserNum': waitingUserNum if waitingUserNum > 0 else 0, 78 | 'accountCount': account_list_len 79 | }) 80 | 81 | 82 | async def rushHandler(sid, userUUID): 83 | if len(using_uuid_set) < account_list_len: # System simultaneous load number 84 | token = str(uuid.uuid4()) 85 | for i in email_chat_map.keys(): 86 | if i not in using_email_set: 87 | using_email_set.add(i) 88 | token_email_map[token] = i 89 | token_set.add(token) 90 | user_token_map[userUUID] = token 91 | userUUID = sid_uuid_map.get(sid) 92 | using_uuid_set.add(userUUID) 93 | await sio.emit('token', token, room=sid) 94 | return 95 | await sio.emit('restricted', room=sid) 96 | 97 | 98 | def getAnswer(sid, text, token): 99 | try: 100 | print("You: " + text) 101 | email = token_email_map.get(token) 102 | if email is None: 103 | asyncio.run(sio.emit('answer', { 104 | 'code': -3, 105 | 'result': '请求超时,请刷新重试' 106 | }, room=sid)) 107 | if email_chat_map[email] is None: 108 | asyncio.run(sio.emit('answer', { 109 | 'code': -4, 110 | 'result': '系统异常,请刷新重试' 111 | }, room=sid)) 112 | answer, _, _ = email_chat_map[email].ask(text) 113 | if answer: 114 | print("AI: " + answer) 115 | asyncio.run(sio.emit('answer', { 116 | 'code': 1, 117 | 'result': answer 118 | }, room=sid)) 119 | else: 120 | asyncio.run(sio.emit('answer', { 121 | 'code': -2, 122 | 'result': '网络错误' 123 | }, room=sid)) 124 | except Exception as err: 125 | print('repr(err):\t', repr(err)) 126 | asyncio.run(sio.emit('answer', { 127 | 'code': -1, 128 | 'msg': str(err) 129 | }, room=sid)) 130 | 131 | 132 | @sio.event 133 | async def connect(sid, environ): 134 | queryDict = parse.parse_qs(environ['QUERY_STRING']) 135 | if 'userUUID' in queryDict.keys() and queryDict['userUUID'][0]: 136 | userUUID = queryDict['userUUID'][0] 137 | sid_uuid_map[sid] = userUUID 138 | if userUUID in logout_uuid_set: 139 | logout_uuid_set.remove(userUUID) 140 | try: 141 | timer_map[userUUID].cancel() 142 | del timer_map[userUUID] 143 | except: 144 | pass 145 | user_uuid_set.add(userUUID) 146 | print("connect ", userUUID) 147 | 148 | 149 | @sio.event 150 | async def rush(sid, data): 151 | userUUID = sid_uuid_map.get(sid) 152 | await rushHandler(sid, userUUID) 153 | 154 | 155 | @sio.event 156 | async def ready(sid, data): 157 | userUUID = sid_uuid_map.get(sid) 158 | if userUUID not in using_uuid_set: await rushHandler(sid, userUUID) 159 | await broadcastSystemInfo() 160 | 161 | 162 | @sio.event 163 | def disconnect(sid): 164 | userUUID = sid_uuid_map[sid] 165 | logout_uuid_set.add(userUUID) 166 | timer_map[userUUID] = Timer(3, logout, (userUUID, )) 167 | timer_map[userUUID].start() 168 | del sid_uuid_map[sid] 169 | print('disconnect uuid:', userUUID) 170 | 171 | 172 | @sio.event 173 | async def chatgpt(sid, data): 174 | text = data.get('text') 175 | token = data.get('token') 176 | userUUID = sid_uuid_map[sid] 177 | if token is None or token not in token_set or user_token_map[userUUID] != token: 178 | await sio.emit('restricted', room=sid) 179 | task = Timer(3, getAnswer, ( 180 | sid, 181 | text, 182 | token, 183 | )) 184 | 185 | task.start() 186 | 187 | 188 | async def index(request): 189 | return web.json_response({'error': -1}) 190 | 191 | 192 | app.router.add_get('/', index) 193 | 194 | if __name__ == '__main__': 195 | web.run_app(app, host="0.0.0.0", port=50000) 196 | --------------------------------------------------------------------------------