├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config.py ├── index.py ├── pygpt.py ├── requirements.txt └── sql.py /.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 | nohup.out 132 | .DS_Store 133 | database.db -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.4 2 | 3 | FROM python:3.9-alpine 4 | WORKDIR /app 5 | COPY . /app 6 | RUN pip3 install -r requirements.txt 7 | ENTRYPOINT ["python3"] 8 | CMD ["-u", "index.py"] 9 | EXPOSE 8083 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 wind X 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 此版本不维护了,可以使用node版本的,支持session和key两种模式[dingtalk-chatgpt-node](https://github.com/XueMeijing/dingtalk-chatgpt-node) 2 | 3 | # ***:warning: __This repository is deprecated and no longer maintained!__*** 4 | 5 | The python proxy api repo [pygpt](https://github.com/PawanOsman/PyGPT) didn't work, you can use node version [chatgpt-io](https://github.com/PawanOsman/chatgpt-io) 6 | 7 | # Change Log 8 | - 2022-03-08 9 | - 优化代码,修复代理服务器偶尔connect refused的问题 10 | - 2022-03-03 11 | - 使用sqlite3增加上下文功能, @bot /reset 命令会重新打开新聊天窗口 12 | ![image](https://user-images.githubusercontent.com/35559153/222692011-d4ac1d37-cd66-41ef-9d87-9baf423c3edd.png) 13 | 14 | - 2022-02-14 15 | - 增加docker部署 16 | - 2022-02-10 17 | - 机器人名字叫ChatGPT会被禁止使用, 可以换成其他的 18 | ![image](https://user-images.githubusercontent.com/35559153/217995508-6916bceb-188f-4bfd-b945-8841616d2ade.png) 19 | 20 | # DingTalk ChatGPT Bot(Unofficial API) 21 | Uses API by [PawanOsman](https://github.com/PawanOsman/PyGPT) 22 | 23 | # Disclaimer 24 | This is not open source. [PawanOsman](https://github.com/PawanOsman/) can see all your requests and your session token. 25 | 26 | # Prerequisites 27 | - DingTalk admin role to create DingTalk bot, [how to create a DingTalk bot](https://xie.infoq.cn/article/3340770024c49b5b1a54597d5) 28 | - OpenAi ChatGPT session 29 | # Feature 30 | ## chat conversation context 31 | ## reset conversation 32 | 33 | # Usage 34 | ## python 35 | 1. install dependencies 36 | ``` 37 | pip3 install -r requirements.txt 38 | ``` 39 | 2. Update config.py variables with your own info 40 | 3. execute script in background 41 | ``` 42 | nohup python3 -u index.py > nohup.out 2>&1 & 43 | ``` 44 | 4. watch logs 45 | ``` 46 | tail -30f nohup.out 47 | ``` 48 | ## docker 49 | 1. get docker image and run 50 | ``` 51 | docker run -dp 8083:8083 fengcailing/dingtalk-chatgpt-bot:1.0.2 52 | ``` 53 | 2. show docker list and get docker container id 54 | ``` 55 | docker ps 56 | ``` 57 | 3. cd docker 58 | ``` 59 | docker exec -it /bin/sh 60 | ``` 61 | 4. update config.py(GPT_SESSION、APP_SECRET) 62 | 5. exit docker 63 | ``` 64 | exit 65 | ``` 66 | 6. create new iamge 67 | ``` 68 | docker commit -m 'update config' dingtalk-chatgpt-bot:v1 69 | ``` 70 | 7. stop pre container and run new image 71 | ``` 72 | docker stop 73 | docker run -dp 8083:8083 dingtalk-chatgpt-bot:v1 74 | ``` 75 | 8. watch logs 76 | ``` 77 | docker logs -n 30 -f 78 | ``` 79 | 80 | If you @YourBotName in DingTalk group, it will get ChatGPT answer and reply. 81 | 82 | E.g. 83 | 84 | ![demo](https://user-images.githubusercontent.com/35559153/216219243-4df07e62-090a-470d-af99-e64a0c8a36a4.png) 85 | 86 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | OpenAi ChatGPT session, You can find the session token manually from your browser: 3 | 4 | 1. Go to https://chat.openai.com/api/auth/session 5 | 2. Press F12 to open console 6 | 3. Go to Application > Cookies 7 | 4. Copy the session token value in __Secure-next-auth.session-token 8 | ''' 9 | GPT_SESSION = '' 10 | 11 | ''' 12 | DingTalk bot app_secret 13 | ''' 14 | APP_SECRET = '' 15 | 16 | __all__ = [ 17 | GPT_SESSION, 18 | APP_SECRET, 19 | ] 20 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hmac 3 | import hashlib 4 | import requests 5 | from pygpt import PyGPT 6 | import datetime 7 | from quart import Quart, request 8 | import asyncio 9 | 10 | from sql import init_db, query_db 11 | import config 12 | 13 | app = Quart(__name__) 14 | 15 | init_db() 16 | 17 | chat_gpt = None 18 | 19 | @app.route('/', methods=['GET', 'POST']) 20 | async def get_data(): 21 | # 第一步验证:是否是post请求 22 | if request.method == "POST": 23 | try: 24 | # 签名验证 获取headers中的Timestamp和Sign 25 | req_data = await request.get_json() 26 | timestamp = request.headers.get('Timestamp') 27 | sign = request.headers.get('Sign') 28 | print('request.data-----\n', req_data) 29 | # 第二步验证:签名是否有效 30 | if check_sig(timestamp) == sign: 31 | print('签名验证成功-----') 32 | # 调用数据处理函数 33 | await handle_info(req_data) 34 | return str(req_data) 35 | else: 36 | result = '签名验证失败-----' 37 | print(result) 38 | return result 39 | except Exception as e: 40 | result = '出错啦~~' 41 | print('error', repr(e)) 42 | return str(result) 43 | return '钉钉机器人:' + str(datetime.datetime.now()) 44 | 45 | # 处理自动回复消息 46 | async def handle_info(req_data): 47 | # 解析用户发送消息 通讯webhook_url 48 | text_info = req_data['text']['content'].strip() 49 | webhook_url = req_data['sessionWebhook'] 50 | senderid = req_data['senderId'] 51 | # 打开新聊天窗口 52 | if (text_info == '/reset'): 53 | sqlite_delete_data_query = """ DELETE FROM 'user' WHERE id = ? """ 54 | query_db(sqlite_delete_data_query, (senderid,)) 55 | send_md_msg(senderid, '聊天上下文已重置', webhook_url) 56 | return 57 | # 请求GPT回复,失败重新请求三次 58 | retry_count = 0 59 | max_retry_count = 3 60 | global chat_gpt 61 | while retry_count < max_retry_count: 62 | try: 63 | if chat_gpt is None: 64 | connect_task = asyncio.create_task(init_connect()) 65 | await connect_task 66 | answer = await chat_gpt.ask(text_info, query_db, senderid) 67 | print('answer:\n', answer) 68 | print('--------------------------') 69 | break 70 | except Exception as e: 71 | retry_count = retry_count + 1 72 | print('retry_count', retry_count) 73 | print('error\n', repr(e)) 74 | answer = '' 75 | if retry_count == 2: 76 | connect_task = asyncio.create_task(init_connect()) 77 | await connect_task 78 | continue 79 | if not answer: 80 | answer = '请求接口失败,请稍后重试' 81 | # 调用函数,发送markdown消息 82 | send_md_msg(senderid, answer, webhook_url) 83 | 84 | # 发送markdown消息 85 | def send_md_msg(userid, message, webhook_url): 86 | ''' 87 | userid: @用户 钉钉id 88 | title : 消息标题 89 | message: 消息主体内容 90 | webhook_url: 通讯url 91 | ''' 92 | message = '@%s \n\n %s' % (userid, message) 93 | title = '大聪明说' 94 | 95 | data = { 96 | "msgtype": "markdown", 97 | "markdown": { 98 | "title":title, 99 | "text": message 100 | }, 101 | # "msgtype": "text", 102 | # "text": { 103 | # "content": message 104 | # }, 105 | "at": { 106 | "atDingtalkIds": [ 107 | userid 108 | ], 109 | } 110 | } 111 | # 利用requests发送post请求 112 | req = requests.post(webhook_url, json=data) 113 | 114 | # 消息数字签名计算核对 115 | def check_sig(timestamp): 116 | app_secret = config.APP_SECRET 117 | app_secret_enc = app_secret.encode('utf-8') 118 | string_to_sign = '{}\n{}'.format(timestamp, app_secret) 119 | string_to_sign_enc = string_to_sign.encode('utf-8') 120 | hmac_code = hmac.new(app_secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest() 121 | sign = base64.b64encode(hmac_code).decode('utf-8') 122 | return sign 123 | 124 | async def init_connect(): 125 | # 建立连接 126 | retry_count = 0 127 | max_retry_count = 3 128 | 129 | while retry_count < max_retry_count: 130 | try: 131 | global chat_gpt 132 | chat_gpt = PyGPT(config.GPT_SESSION) 133 | await chat_gpt.connect() 134 | await chat_gpt.wait_for_ready() 135 | break 136 | except Exception as e: 137 | retry_count = retry_count + 1 138 | print('retry_count', retry_count) 139 | print('error\n', repr(e)) 140 | continue 141 | 142 | if __name__ == '__main__': 143 | # 指定host和port,0.0.0.0可以运行在服务器上对外访问,记得开服务器的网络防火墙端口 144 | # GCP在VPC network -> firewalls -> 增加一条 VPC firewall rules 指定端口,target填 http-server或https-server 145 | app.run(host='0.0.0.0', port=8083) -------------------------------------------------------------------------------- /pygpt.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Uses API by [PawanOsman](https://github.com/PawanOsman/PyGPT) 3 | ''' 4 | 5 | import uuid 6 | import asyncio 7 | import socketio 8 | import datetime 9 | import json 10 | import base64 11 | 12 | class PyGPT: 13 | def __init__(self, session_token, timeout=120, bypass_node='https://gpt.pawan.krd'): 14 | self.ready = False 15 | self.socket = socketio.AsyncClient() 16 | self.socket.on('connect', self.on_connect) 17 | self.socket.on('disconnect', self.on_disconnect) 18 | self.socket.on('serverMessage', print) 19 | self.session_token = session_token 20 | self.conversations = [] 21 | self.timeout = timeout 22 | self.auth = None 23 | self.expires = datetime.datetime.now() 24 | self.pause_token_checks = False 25 | self.bypass_node = bypass_node 26 | asyncio.create_task(self.cleanup_conversations()) 27 | 28 | async def connect(self): 29 | await self.socket.connect(f'{self.bypass_node}/?client=python&version=1.0.4&versionCode=104') 30 | 31 | async def disconnect(self): 32 | await self.socket.disconnect() 33 | 34 | def on_connect(self): 35 | print('Connected to server') 36 | asyncio.create_task(self.check_tokens()) 37 | 38 | def on_disconnect(self): 39 | print('Disconnected from server') 40 | self.ready = False 41 | 42 | async def check_tokens(self): 43 | while True: 44 | if self.pause_token_checks: 45 | await asyncio.sleep(0.5) 46 | continue 47 | self.pause_token_checks = True 48 | now = datetime.datetime.now() 49 | offset = datetime.timedelta(minutes=2) 50 | if self.expires < (now - offset) or not self.auth: 51 | await self.get_tokens() 52 | self.pause_token_checks = False 53 | await asyncio.sleep(0.5) 54 | 55 | async def cleanup_conversations(self): 56 | while True: 57 | await asyncio.sleep(60) 58 | now = datetime.datetime.now() 59 | self.conversations = [c for c in self.conversations if 60 | now - c['last_active'] < datetime.timedelta(minutes=60)] 61 | 62 | def add_conversation(self, id): 63 | conversation = { 64 | 'id': id, 65 | 'conversation_id': None, 66 | 'parent_id': uuid.uuid4(), 67 | 'last_active': datetime.datetime.now() 68 | } 69 | self.conversations.append(conversation) 70 | return conversation 71 | 72 | def get_conversation_by_id(self, id): 73 | conversation = next((c for c in self.conversations if c['id'] == id), None) 74 | if conversation is None: 75 | conversation = self.add_conversation(id) 76 | else: 77 | conversation['last_active'] = datetime.datetime.now() 78 | return conversation 79 | 80 | async def wait_for_ready(self): 81 | while not self.ready: 82 | await asyncio.sleep(0.025) 83 | print('Ready!!') 84 | 85 | async def ask(self, prompt, query_db, id='default'): 86 | if not self.auth or not self.validate_token(self.auth): 87 | await self.get_tokens() 88 | conversation = self.get_conversation_by_id(id) 89 | 90 | sqlite_get_data_query = """ SELECT * FROM user WHERE id = ? """ 91 | user_record = query_db(sqlite_get_data_query, (id,), True) 92 | print('user_record', user_record) 93 | 94 | # Fix for timeout issue by Ulysses0817: https://github.com/Ulysses0817 95 | data = await self.socket.call(event='askQuestion', data={ 96 | 'prompt': prompt, 97 | 'parentId': user_record['parent_id'] if user_record else str(conversation['parent_id']), 98 | 'conversationId': user_record["conversation_id"] if user_record else str(conversation['conversation_id']), 99 | 'auth': self.auth 100 | }, timeout=self.timeout) 101 | print('ask data---\n', data) 102 | if 'error' in data: 103 | print(f'Error: {data["error"]}') 104 | return f'Error: {data["error"]}' 105 | try: 106 | if user_record is None: 107 | # 插入数据 108 | sqlite_insert_data_query = """ INSERT INTO user 109 | ('id', 'name', 'conversation_id', 'parent_id', 'create_at') 110 | VALUES (?,?,?,?,?); """ 111 | query_db(sqlite_insert_data_query, (id, None, data['conversationId'], data['messageId'], datetime.datetime.now())) 112 | print('插入数据') 113 | else: 114 | # 更新数据 115 | sqlite_update_data_query = """ UPDATE user SET id = ?, name = ?, conversation_id = ?, parent_id = ?, create_at = ? WHERE id = ? """ 116 | query_db(sqlite_update_data_query, (id, None, data['conversationId'], data['messageId'], datetime.datetime.now(), id)) 117 | print('更新数据') 118 | except Exception as e: 119 | print('database error\n', repr(e)) 120 | conversation['parent_id'] = data['messageId'] 121 | conversation['conversation_id'] = data['conversationId'] 122 | return data['answer'] 123 | 124 | def validate_token(self, token): 125 | if not token: 126 | return False 127 | parsed = json.loads(base64.b64decode(f'{token.split(".")[1]}==').decode()) 128 | return datetime.datetime.now() <= datetime.datetime.fromtimestamp(parsed['exp']) 129 | 130 | async def get_tokens(self): 131 | await asyncio.sleep(1) 132 | # Fix for timeout issue by Ulysses0817: https://github.com/Ulysses0817 133 | data = await self.socket.call(event='getSession', data=self.session_token, timeout=self.timeout) 134 | 135 | if 'error' in data: 136 | print(f'Error getting session: {data["error"]}') 137 | else: 138 | self.auth = data['auth'] 139 | self.expires = datetime.datetime.strptime(data['expires'], '%Y-%m-%dT%H:%M:%S.%fZ') 140 | self.session_token = data['sessionToken'] 141 | self.ready = True -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyGPT==1.0.4 2 | quart==0.18.3 3 | requests==2.28.1 4 | -------------------------------------------------------------------------------- /sql.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | DATABASE = 'database.db' 4 | 5 | # 查询结果元组转字典 6 | def dict_factory(cursor, row): 7 | d = {} 8 | for idx, col in enumerate(cursor.description): 9 | d[col[0]] = row[idx] 10 | return d 11 | 12 | def init_db(): 13 | db = sqlite3.connect(DATABASE, check_same_thread=False) 14 | cursor = db.cursor() 15 | create_table_query = ''' CREATE TABLE IF NOT EXISTS user( 16 | id TEXT PRIMARY KEY NOT NULL, 17 | name TEXT , 18 | conversation_id TEXT NOT NULL, 19 | parent_id TEXT NOT NULL, 20 | create_at timestamp NOT NULL); ''' 21 | cursor.execute(create_table_query) 22 | cursor.close() 23 | db.close() 24 | print('数据库初始化成功') 25 | 26 | def get_db(): 27 | db = sqlite3.connect(DATABASE, check_same_thread=False) 28 | db.row_factory = dict_factory 29 | return db 30 | 31 | def query_db(query, args=(), one=False): 32 | db = get_db() 33 | cur = db.cursor() 34 | cur.execute(query, args) 35 | rv = cur.fetchall() 36 | db.commit() 37 | cur.close() 38 | db.close() 39 | return (rv[0] if rv else None) if one else rv 40 | 41 | __all__ = [ 42 | init_db, 43 | query_db 44 | ] --------------------------------------------------------------------------------