├── wechat_auto_reply ├── message_handler │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── admin.py │ ├── tests.py │ ├── urls.py │ ├── apps.py │ ├── utils.py │ ├── views.py │ ├── tasks.py │ └── WXBizMsg.py ├── wechat_auto_reply │ ├── __init__.py │ ├── urls.py │ ├── celery.py │ ├── asgi.py │ ├── wsgi.py │ └── settings.py └── manage.py ├── demo.jpg ├── nginx ├── Dockerfile └── default.conf ├── requirements.txt ├── Dockerfile ├── test.py ├── docker-compose.yml ├── README.md ├── .gitignore └── LICENSE /wechat_auto_reply/message_handler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaabarty/wecom-chatgpt-bot/HEAD/demo.jpg -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | COPY ./default.conf /etc/nginx/conf.d/default.conf 4 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path('wechat/', views.wechat, name='wechat'), 6 | ] 7 | -------------------------------------------------------------------------------- /wechat_auto_reply/wechat_auto_reply/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from .celery import app as celery_app 3 | 4 | __all__ = ('celery_app',) 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==4.1.7 2 | wechatpy==1.8.18 3 | revChatGPT==4.0.6.1 4 | redis==4.5.3 5 | celery==5.2.7 6 | cryptography==3.4.8 7 | googlesearch-python==1.2.3 8 | trafilatura==1.5.0 9 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MessageHandlerConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'message_handler' 7 | -------------------------------------------------------------------------------- /wechat_auto_reply/wechat_auto_reply/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('', include('message_handler.urls')), 7 | ] 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN mkdir /app 6 | WORKDIR /app 7 | 8 | COPY requirements.txt /app/ 9 | RUN pip install --no-cache-dir -r requirements.txt 10 | 11 | COPY ./wechat_auto_reply/ /app/ 12 | -------------------------------------------------------------------------------- /wechat_auto_reply/wechat_auto_reply/celery.py: -------------------------------------------------------------------------------- 1 | # celery.py 2 | from __future__ import absolute_import, unicode_literals 3 | import os 4 | from celery import Celery 5 | 6 | # 设置Django项目的默认配置模块 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wechat_auto_reply.settings') 8 | 9 | app = Celery('wechat_auto_reply') 10 | 11 | # 从Django的设置中加载Celery配置 12 | app.config_from_object('django.conf:settings', namespace='CELERY') 13 | 14 | # 自动发现Celery任务 15 | app.autodiscover_tasks() -------------------------------------------------------------------------------- /wechat_auto_reply/wechat_auto_reply/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for wechat_auto_reply project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wechat_auto_reply.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /wechat_auto_reply/wechat_auto_reply/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for wechat_auto_reply project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wechat_auto_reply.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /nginx/default.conf: -------------------------------------------------------------------------------- 1 | upstream django { 2 | server django:8000; 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name www.mybot233.com; 8 | 9 | location /wechat/ { 10 | proxy_pass http://django; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header Host $host; 13 | proxy_redirect off; 14 | } 15 | 16 | location /static/ { 17 | alias /app/static/; 18 | } 19 | 20 | location /media/ { 21 | alias /app/media/; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | access_token = "Hf-Z5zTUySuvsC9rRITl8wJxqccnbJCriDsVfC-ToEq7QX7Xut7Ud9ACUE3ZRQJ9N5OXpovEeiBLB8Mfr-ZbrD7kCDp_h1R-Bgq3xWp5MzPPi38yU0YQ0XSpNNXrKwhcH2fFa5CY3H8lP6VBphnq0jpqIs2mfQcsmhoZuo_9ERnyBgKTnwl77jRgto7dT56RduKcfEX-RXFGLflaJaIrEA" 4 | 5 | url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 6 | data = { 7 | "touser": "YangBoYang", 8 | "msgtype": "text", 9 | "agentid": 1000002, # 应用的AgentID 10 | "text": { 11 | "content": "您的消息内容" 12 | }, 13 | "safe": 0, 14 | "enable_id_trans": 0, 15 | "enable_duplicate_check": 0, 16 | } 17 | 18 | response = requests.post(url, json=data) 19 | print(response.content) -------------------------------------------------------------------------------- /wechat_auto_reply/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wechat_auto_reply.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | django: 5 | build: . 6 | command: python manage.py runserver 0.0.0.0:8000 7 | volumes: 8 | - ./wechat_auto_reply:/app 9 | ports: 10 | - "8000:8000" 11 | depends_on: 12 | - db 13 | - redis 14 | 15 | db: 16 | image: postgres:latest 17 | environment: 18 | - POSTGRES_USER=myuser 19 | - POSTGRES_PASSWORD=mypassword 20 | - POSTGRES_DB=mydb 21 | 22 | redis: 23 | image: redis:latest 24 | 25 | celery: 26 | build: . 27 | command: celery -A wechat_auto_reply worker --loglevel=info -c 1 28 | volumes: 29 | - ./wechat_auto_reply:/app 30 | depends_on: 31 | - redis 32 | 33 | nginx: 34 | build: ./nginx 35 | ports: 36 | - "80:80" 37 | depends_on: 38 | - django 39 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/utils.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | class ThreadSafeDict(dict): 5 | def __init__(self, *args, **kwargs): 6 | super(ThreadSafeDict, self).__init__(*args, **kwargs) 7 | self._lock = threading.Lock() 8 | 9 | def __setitem__(self, key, value): 10 | with self._lock: 11 | super(ThreadSafeDict, self).__setitem__(key, value) 12 | 13 | def __getitem__(self, key): 14 | with self._lock: 15 | return super(ThreadSafeDict, self).__getitem__(key) 16 | 17 | class ExpiringDict: 18 | def __init__(self, expiration_time): 19 | self.expiration_time = expiration_time 20 | self.data = ThreadSafeDict() 21 | self.timestamps = ThreadSafeDict() 22 | 23 | def set(self, key, value): 24 | self.data[key] = value 25 | self.timestamps[key] = time.time() 26 | 27 | def get(self, key): 28 | if key in self.data: 29 | if time.time() - self.timestamps[key] < self.expiration_time: 30 | return self.data[key] 31 | else: 32 | self.delete(key) 33 | return None 34 | 35 | def delete(self, key): 36 | if key in self.data: 37 | del self.data[key] 38 | del self.timestamps[key] 39 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.views.decorators.csrf import csrf_exempt 3 | from django.conf import settings 4 | from wechatpy.enterprise.crypto import WeChatCrypto 5 | import hashlib 6 | from wechatpy.enterprise import parse_message 7 | from wechatpy.exceptions import InvalidSignatureException 8 | import xml.etree.ElementTree as ET 9 | from .WXBizMsg import WXBizMsgCrypt 10 | from .tasks import send_message_to_wechat 11 | 12 | def check_signature(token, signature, timestamp, nonce): 13 | sorted_params = sorted([token, timestamp, nonce]) 14 | sha1 = hashlib.sha1() 15 | sha1.update(''.join(sorted_params).encode('utf-8')) 16 | return sha1.hexdigest() == signature 17 | 18 | def get_from_username(xml_string): 19 | root = ET.fromstring(xml_string) 20 | from_username = root.find('FromUserName').text 21 | return from_username 22 | 23 | @csrf_exempt 24 | def wechat(request): 25 | signature = request.GET.get('msg_signature') 26 | timestamp = request.GET.get('timestamp') 27 | nonce = request.GET.get('nonce') 28 | echostr = request.GET.get('echostr') 29 | wxcpt = WXBizMsgCrypt(settings.WEWORK_TOKEN, settings.WEWORK_AES_KEY, settings.WEWORK_CORP_ID) 30 | try: 31 | check_signature(settings.WEWORK_TOKEN, signature, timestamp, nonce) 32 | except InvalidSignatureException: 33 | return HttpResponse(status = 403) 34 | if request.method == 'GET': 35 | _, sEchoStr = wxcpt.VerifyURL(signature, timestamp, nonce, echostr) 36 | return HttpResponse(sEchoStr, content_type="text/plain") 37 | elif request.method == 'POST': 38 | crypto = WeChatCrypto(settings.WEWORK_TOKEN, settings.WEWORK_AES_KEY, settings.WEWORK_CORP_ID) 39 | decrypted_xml = crypto.decrypt_message(request.body, signature, timestamp, nonce) 40 | msg = parse_message(decrypted_xml) 41 | send_message_to_wechat.delay(msg.content, get_from_username(decrypted_xml)) 42 | return HttpResponse("", content_type="text/plain") 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 企业微信 ChatGPT 聊天机器人 2 | 3 | 为什么使用企业微信接入 ChatGPT 聊天机器人? 4 | - 企业微信相比其他平台,聊天功能与微信更接近,使用更习惯 5 | - 可把服务部署在海外机房,相比于网页版更为安全稳定 6 | - 可以自行增加频次限制、聊天内容留存等功能,方便团队/企业内部多人同时使用 7 | 8 | 本项目适配 gpt-3.5/4 接口,使用的均为 OpenAI 及企业微信提供的官方 API,保证安全性。 9 | 10 | 如果你有新功能需求,可以在 [issues](https://github.com/buaabarty/wecom-chatgpt-bot/issues) 里提出;如果你希望为项目做贡献,可以直接提交 pull request。 11 | 12 | 0. 前置准备: 13 | - OPENAI API 账号申请 14 | - 企业认证的企业微信账号 15 | - 企业名下备案的域名 16 | - 部署所在机器的系统内安装`git`、`docker`、`docker-compose`。 17 | 18 | > 如果你不是在企业/单位内进行配置,并且未来有长期使用 ChatGPT 的打算,建议自己或联合朋友们注册一个公司并进行企业微信认证,以及注册个域名并在企业名下进行域名备案,这样长期来看是最安全的。如果来不及,可以借用朋友的企业进行开发。非常不建议用非官方的方式配置企业微信机器人,存在被封号的风险。 19 | > 20 | > 未来本项目考虑实现独立 web 页面功能,敬请期待。 21 | 22 | 1. 修改配置: 23 | 24 | 首先修改`nginx/default.conf`中的`server_name`为你准备好的域名。 25 | 26 | 并在`settings.py`中填入对应的信息,具体如下表所示: 27 | 28 | | 字段 | 含义 | 是否必须修改 | 29 | |-|-|-| 30 | |`WEWORK_TOKEN`|企业微信创建应用内随机生成的`Token`|是| 31 | |`WEWORK_AES_KEY`|企业微信创建应用内随机生成的`EncodingAESKey`|是| 32 | |`WEWORK_CORP_ID`|企业微信后台显示的企业 ID|是| 33 | |`WEWORK_CORP_SECRET`|企业微信创建应用内获取的`Secret`|是| 34 | |`OPENAI_API_KEY`|OpenAI 后台获取的 `API_KEY`|是| 35 | |`OPENAI_GPT_ENGINE`|使用的 OpenAI 模型,可选列表详见`settings.py`文件|否| 36 | |`OPENAI_SYSTEM_PROMPT`|发送的 system prompt 信息,可用来定制企业专属的 chatbot|否| 37 | |`CHAT_RESET_MESSAGE`|触发重置对话的消息内容,全文匹配|否| 38 | |`CHAT_RESET_MESSAGE_RESULT`|重置对话后返回的结果|否| 39 | |`CHAT_ERROR_MESSAGE`|请求错误(如超出频率限制、欠费等)后返回的结果|否| 40 | |`CHAT_GOOGLE_ERROR_MESSAGE`|谷歌搜索过程异常后返回的结果|否| 41 | |`GOOGLE_API_KEY`|如何设置请参考 [文档](https://significant-gravitas.github.io/Auto-GPT/configuration/search/)|是| 42 | |`CUSTOM_SEARCH_ENGINE_ID`|如何设置请参考 [文档](https://significant-gravitas.github.io/Auto-GPT/configuration/search/)|是| 43 | |`SEARCH_OPTIONS_COUNT`|每次搜索结果的数量,默认为 $3$|否| 44 | |`SEARCH_PROMPT`|在搜索模式下的首句提示信息|否| 45 | |`SEARCH_CONCLUTION_PROMPT`|在搜索模式下的末句提示信息|否| 46 | |`SEARCH_TOKEN`|触发搜索模式的单词,包含则触发|否| 47 | 48 | 2. 启动服务: 49 | ``` 50 | docker-compose up -d 51 | ``` 52 | 53 | 3. 修改你的域名 DNS 解析,到部署机器所在 IP。 54 | 4. 进入企业微信应用后台的「API 接收消息」设置,填入对应信息,点击保存。注:如果 nginx 中配置的域名是 `a.yourdomain.com`,则接收消息中设置的 url 为 `a.yourdomain.com/wechat/` 55 | 5. 进入企业微信应用后台的「企业可信 IP」设置,填入部署机器所在 IP。 56 | 6. 在企业微信中找到对应的应用,发送消息,测试部署效果。 57 | 58 | ![](demo.jpg) -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | import requests 3 | from redis import Redis 4 | from revChatGPT.V3 import Chatbot 5 | from django.conf import settings 6 | from .utils import ExpiringDict 7 | from googleapiclient.discovery import build 8 | from trafilatura import fetch_url, extract 9 | 10 | 11 | cache = ExpiringDict(1800) # set the expiring time of access_token as 1800 seconds 12 | redis_client = Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT) 13 | chatbots = ExpiringDict(1800) # set the expiring time of conversation as 1800 seconds 14 | 15 | def set_access_token(token, expires_in): 16 | redis_client.set('wecom_access_token_bot', token, ex=expires_in) 17 | 18 | def get_access_token(): 19 | return redis_client.get('wecom_access_token_bot') 20 | 21 | def request_access_token(corp_id, corp_secret): 22 | url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corp_id}&corpsecret={corp_secret}' 23 | response = requests.get(url) 24 | if response.status_code == 200: 25 | result = response.json() 26 | if 'access_token' in result: 27 | return result['access_token'], result['expires_in'] 28 | else: 29 | print(f"Error: {result.get('errmsg')}") 30 | else: 31 | print(f"Request failed with status code {response.status_code}") 32 | 33 | def fetch_access_token(corp_id, corp_secret): 34 | access_token = get_access_token() 35 | if access_token: 36 | return access_token.decode() 37 | else: 38 | token, expires_in = request_access_token(corp_id, corp_secret) 39 | if token: 40 | set_access_token(token, expires_in) 41 | return token 42 | else: 43 | print("Failed to get access token.") 44 | 45 | def send_with_content(chatbot, contents, prompt): 46 | chatbot.add_to_conversation(settings.SEARCH_PROMPT + '\n' + contents[0][:1024], 'user') 47 | for content in contents[1:]: 48 | chatbot.add_to_conversation(content[:1024], 'user') 49 | response = "" 50 | for query in chatbot.ask_stream(settings.SEARCH_CONCLUTION_PROMPT + '\n' + prompt): 51 | response += query 52 | return response 53 | 54 | def extract_main_content(url): 55 | try: 56 | downloaded = fetch_url(url) 57 | result = extract(downloaded) 58 | return result 59 | except Exception as e: 60 | print(e) 61 | return None 62 | 63 | def calculate(chatbot, query): 64 | service = build("customsearch", "v1", developerKey=settings.GOOGLE_API_KEY) 65 | result = ( 66 | service.cse() 67 | .list(q = query, cx = settings.CUSTOM_SEARCH_ENGINE_ID, num = settings.SEARCH_OPTIONS_COUNT) 68 | .execute() 69 | ) 70 | search_results = result.get("items", []) 71 | search_results_links = [item["link"].strip() for item in search_results] 72 | contents = [] 73 | for url in search_results_links: 74 | ret = extract_main_content(url) 75 | if ret is not None: 76 | contents.append(ret) 77 | if len(contents) > 0: 78 | return send_with_content(chatbot, contents, query) 79 | else: 80 | return '' 81 | 82 | @shared_task 83 | def send_message_to_wechat(message, user): 84 | if any(substring in message for substring in settings.SEARCH_TOKEN): 85 | for substring in settings.SEARCH_TOKEN: 86 | message = message.replace(substring, "") 87 | chatbots.set(user, Chatbot(temperature=0.1, api_key=settings.OPENAI_API_KEY, engine=settings.OPENAI_GPT_ENGINE, timeout=120, system_prompt=settings.OPENAI_SEARCH_PROMPT)) 88 | chatbot = chatbots.get(user) 89 | try: 90 | chatres = calculate(chatbot, message) 91 | except Exception as e: 92 | print(e) 93 | chatres = settings.CHAT_GOOGLE_ERROR_MESSAGE 94 | elif message == settings.CHAT_RESET_MESSAGE: # restart a new conversation after recieved specific message 95 | if chatbots.get(user) is not None: 96 | chatbots.set(user, Chatbot(api_key=settings.OPENAI_API_KEY, timeout=60, engine=settings.OPENAI_GPT_ENGINE, system_prompt=settings.OPENAI_SYSTEM_PROMPT)) 97 | chatres = settings.CHAT_RESET_MESSAGE_RESULT 98 | else: 99 | chatbot = chatbots.get(user) 100 | if chatbot is None: 101 | chatbots.set(user, Chatbot(api_key=settings.OPENAI_API_KEY, timeout=60, engine=settings.OPENAI_GPT_ENGINE, system_prompt=settings.OPENAI_SYSTEM_PROMPT)) 102 | chatbot = chatbots.get(user) 103 | try: 104 | chatres = chatbot.ask(message) 105 | except Exception as e: 106 | print(f"exception: {str(e)}") 107 | chatres = settings.CHAT_ERROR_MESSAGE 108 | access_token = fetch_access_token(settings.WEWORK_CORP_ID, settings.WEWORK_CORP_SECRET) 109 | url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 110 | data = { 111 | "touser": user, 112 | "msgtype": "text", 113 | "agentid": settings.WEWORK_AGENT_ID, # 应用的AgentID 114 | "text": { 115 | "content": chatres 116 | }, 117 | "safe": 0, 118 | "enable_id_trans": 0, 119 | "enable_duplicate_check": 0, 120 | } 121 | requests.post(url, json=data, timeout=20) 122 | -------------------------------------------------------------------------------- /wechat_auto_reply/wechat_auto_reply/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for wechat_auto_reply project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.13. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | # tecent wework configurations 19 | WEWORK_TOKEN = 'abcdef' # change to your own token 20 | WEWORK_AES_KEY = '123456' # change to your own aes key 21 | WEWORK_CORP_ID = 'abcxyz' # change to your own corp id 22 | WEWORK_CORP_SECRET = '123890' # change to your own corp secret 23 | WEWORK_AGENT_ID = 1000000 24 | 25 | # gpt configurations 26 | OPENAI_API_KEY = '123456' # fill in your openai api key 27 | OPENAI_GPT_ENGINE = 'gpt-4' # gpt-4, gpt-4-0314, gpt-4-32k, gpt-4-32k-0314, gpt-3.5-turbo, gpt-3.5-turbo-0310 28 | OPENAI_SYSTEM_PROMPT = '你是江南皮革厂自研的JNGPT,基于国产自研大模型实现,接下来用中文进行对话,适量使用emoji,并用尽可能简短的文字来回答。' # change to your own system prompt 29 | OPENAI_SEARCH_PROMPT = '你需要结合一些网络上检索到的信息和你已经掌握的知识库,对一个问题给出尽可能简洁、思考再三后的解答。' 30 | 31 | # chat configurations 32 | CHAT_RESET_MESSAGE = '重置对话' # change to your own reset message 33 | CHAT_RESET_MESSAGE_RESULT = '重置对话成功!' # change to your own reset message result 34 | CHAT_ERROR_MESSAGE = 'GPT API 压力大,请稍后再试' # change to your own error message 35 | CHAT_GOOGLE_ERROR_MESSAGE = '联网计算失败,请稍后重试!' # change to your own error message after google search 36 | 37 | # google api configurations 38 | # 可以参考 https://significant-gravitas.github.io/Auto-GPT/configuration/search/ 39 | GOOGLE_API_KEY = 'zbxlkjselgjkxb0s9dgljejk23' 40 | CUSTOM_SEARCH_ENGINE_ID = '8b23bsdo' 41 | SEARCH_OPTIONS_COUNT = 3 42 | SEARCH_PROMPT = '请先阅读以下几段网络上搜索到的材料,然后回答问题:' 43 | SEARCH_CONCLUTION_PROMPT = '请根据以上信息,并结合你自己目前掌握的知识库,回答一个问题:' 44 | SEARCH_TOKEN = ['搜一下', '搜一搜', '联网', '连网'] 45 | 46 | # Quick-start development settings - unsuitable for production 47 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 48 | 49 | # SECURITY WARNING: keep the secret key used in production secret! 50 | SECRET_KEY = 'django-insecure-oiizqyaszf8ha+t#=8720&3tw7&=6r=txlfe)=$p)gfkv6+w-w' 51 | CELERY_BROKER_URL = 'redis://redis:6379/0' 52 | CELERY_RESULT_BACKEND = 'redis://redis:6379/0' 53 | REDIS_HOST = 'redis' 54 | REDIS_PORT = 6379 55 | 56 | # SECURITY WARNING: don't run with debug turned on in production! 57 | DEBUG = True 58 | 59 | ALLOWED_HOSTS = ['*'] 60 | 61 | # Application definition 62 | 63 | INSTALLED_APPS = [ 64 | 'django.contrib.admin', 65 | 'django.contrib.auth', 66 | 'django.contrib.contenttypes', 67 | 'django.contrib.sessions', 68 | 'django.contrib.messages', 69 | 'django.contrib.staticfiles', 70 | 'message_handler', 71 | ] 72 | 73 | MIDDLEWARE = [ 74 | 'django.middleware.security.SecurityMiddleware', 75 | 'django.contrib.sessions.middleware.SessionMiddleware', 76 | 'django.middleware.common.CommonMiddleware', 77 | 'django.middleware.csrf.CsrfViewMiddleware', 78 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 79 | 'django.contrib.messages.middleware.MessageMiddleware', 80 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 81 | ] 82 | 83 | ROOT_URLCONF = 'wechat_auto_reply.urls' 84 | 85 | TEMPLATES = [ 86 | { 87 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 88 | 'DIRS': [], 89 | 'APP_DIRS': True, 90 | 'OPTIONS': { 91 | 'context_processors': [ 92 | 'django.template.context_processors.debug', 93 | 'django.template.context_processors.request', 94 | 'django.contrib.auth.context_processors.auth', 95 | 'django.contrib.messages.context_processors.messages', 96 | ], 97 | }, 98 | }, 99 | ] 100 | 101 | WSGI_APPLICATION = 'wechat_auto_reply.wsgi.application' 102 | 103 | 104 | # Database 105 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 106 | 107 | DATABASES = { 108 | 'default': { 109 | 'ENGINE': 'django.db.backends.sqlite3', 110 | 'NAME': BASE_DIR / 'db.sqlite3', 111 | } 112 | } 113 | 114 | 115 | # Password validation 116 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 117 | 118 | AUTH_PASSWORD_VALIDATORS = [ 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 121 | }, 122 | { 123 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 124 | }, 125 | { 126 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 127 | }, 128 | { 129 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 130 | }, 131 | ] 132 | 133 | 134 | # Internationalization 135 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 136 | 137 | LANGUAGE_CODE = 'en-us' 138 | 139 | TIME_ZONE = 'UTC' 140 | 141 | USE_I18N = True 142 | 143 | USE_L10N = True 144 | 145 | USE_TZ = True 146 | 147 | 148 | # Static files (CSS, JavaScript, Images) 149 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 150 | 151 | STATIC_URL = '/static/' 152 | 153 | # Default primary key field type 154 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 155 | 156 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 157 | -------------------------------------------------------------------------------- /wechat_auto_reply/message_handler/WXBizMsg.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import string 3 | import random 4 | import hashlib 5 | import time 6 | import struct 7 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 8 | from cryptography.hazmat.backends import default_backend 9 | import xml.etree.cElementTree as ET 10 | import socket 11 | import six 12 | 13 | WXBizMsgCrypt_OK = 0 14 | WXBizMsgCrypt_ValidateSignature_Error = -40001 15 | WXBizMsgCrypt_ParseXml_Error = -40002 16 | WXBizMsgCrypt_ComputeSignature_Error = -40003 17 | WXBizMsgCrypt_IllegalAesKey = -40004 18 | WXBizMsgCrypt_ValidateCorpid_Error = -40005 19 | WXBizMsgCrypt_EncryptAES_Error = -40006 20 | WXBizMsgCrypt_DecryptAES_Error = -40007 21 | WXBizMsgCrypt_IllegalBuffer = -40008 22 | WXBizMsgCrypt_EncodeBase64_Error = -40009 23 | WXBizMsgCrypt_DecodeBase64_Error = -40010 24 | WXBizMsgCrypt_GenReturnXml_Error = -40011 25 | 26 | class FormatException(Exception): 27 | pass 28 | 29 | def throw_exception(message, exception_class=FormatException): 30 | raise exception_class(message) 31 | 32 | def to_text(value, encoding='utf-8'): 33 | if not value: 34 | return '' 35 | if isinstance(value, six.text_type): 36 | return value 37 | if isinstance(value, six.binary_type): 38 | return value.decode(encoding) 39 | return six.text_type(value) 40 | 41 | def to_binary(value, encoding='utf-8'): 42 | if not value: 43 | return b'' 44 | if isinstance(value, six.binary_type): 45 | return value 46 | if isinstance(value, six.text_type): 47 | return value.encode(encoding) 48 | return to_text(value).encode(encoding) 49 | 50 | def byte2int(c): 51 | if six.PY2: 52 | return ord(c) 53 | return c 54 | 55 | class SHA1: 56 | def getSHA1(self, token, timestamp, nonce, encrypt): 57 | try: 58 | sortlist = [token, timestamp, nonce, to_text(encrypt)] 59 | sortlist.sort() 60 | sha = hashlib.sha1() 61 | sha.update("".join(sortlist).encode("ascii")) 62 | return WXBizMsgCrypt_OK, sha.hexdigest() 63 | except Exception as e: 64 | print(e) 65 | return WXBizMsgCrypt_ComputeSignature_Error, None 66 | 67 | 68 | class XMLParse: 69 | AES_TEXT_RESPONSE_TEMPLATE = """ 70 | 71 | 72 | %(timestamp)s 73 | 74 | """ 75 | 76 | def extract(self, xmltext): 77 | try: 78 | xml_tree = ET.fromstring(xmltext) 79 | encrypt = xml_tree.find("Encrypt") 80 | touser_name = xml_tree.find("ToUserName") 81 | return WXBizMsgCrypt_OK, encrypt.text, touser_name.text 82 | except Exception as e: 83 | print(e) 84 | return WXBizMsgCrypt_ParseXml_Error, None, None 85 | 86 | def generate(self, encrypt, signature, timestamp, nonce): 87 | resp_dict = { 88 | 'msg_encrypt': encrypt, 89 | 'msg_signaturet': signature, 90 | 'timestamp': timestamp, 91 | 'nonce': nonce, 92 | } 93 | resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict 94 | return resp_xml 95 | 96 | 97 | class PKCS7Encoder(): 98 | block_size = 32 99 | 100 | def encode(self, text): 101 | text_length = len(text) 102 | amount_to_pad = self.block_size - (text_length % self.block_size) 103 | if amount_to_pad == 0: 104 | amount_to_pad = self.block_size 105 | pad = to_binary(chr(amount_to_pad)) 106 | return text + pad * amount_to_pad 107 | 108 | def decode(self, decrypted): 109 | pad = byte2int(decrypted[-1]) 110 | if pad < 1 or pad > 32: 111 | pad = 0 112 | return decrypted[:-pad] 113 | 114 | 115 | class Prpcrypt(object): 116 | def __init__(self, key): 117 | self.key = key 118 | backend = default_backend() 119 | self.cipher = Cipher( 120 | algorithms.AES(key), 121 | modes.CBC(key[:16]), 122 | backend=backend 123 | ) 124 | 125 | def encrypt(self, text, corpid): 126 | text = to_binary(text) 127 | tmp_list = [] 128 | tmp_list.append(to_binary(self.get_random_str())) 129 | length = struct.pack(b'I', socket.htonl(len(text))) 130 | tmp_list.append(length) 131 | tmp_list.append(text) 132 | tmp_list.append(to_binary(corpid)) 133 | text = b''.join(tmp_list) 134 | pkcs7 = PKCS7Encoder() 135 | text = pkcs7.encode(text) 136 | try: 137 | encryptor = self.cipher.encryptor() 138 | ciphertext = to_binary(encryptor.update(text) + encryptor.finalize()) 139 | return WXBizMsgCrypt_OK, base64.b64encode(ciphertext) 140 | except Exception as e: 141 | print(e) 142 | return WXBizMsgCrypt_EncryptAES_Error, None 143 | 144 | def decrypt(self, text, corpid): 145 | try: 146 | decryptor = self.cipher.decryptor() 147 | plain_text = decryptor.update(base64.b64decode(text)) + decryptor.finalize() 148 | except Exception as e: 149 | print(e) 150 | return WXBizMsgCrypt_DecryptAES_Error, None 151 | try: 152 | pad = plain_text[-1] 153 | content = plain_text[16:-pad] 154 | xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0]) 155 | xml_content = content[4: xml_len + 4] 156 | from_corpid = content[xml_len + 4:].decode("utf8") 157 | except Exception as e: 158 | print(e) 159 | return WXBizMsgCrypt_IllegalBuffer, None 160 | if from_corpid != corpid: 161 | return WXBizMsgCrypt_ValidateCorpid_Error, None 162 | return 0, xml_content 163 | 164 | def get_random_str(self): 165 | rule = string.ascii_letters + string.digits 166 | str = random.sample(rule, 16) 167 | return "".join(str) 168 | 169 | class WXBizMsgCrypt(object): 170 | def __init__(self, sToken, sEncodingAESKey, sCorpId): 171 | try: 172 | self.key = base64.b64decode(sEncodingAESKey + "=") 173 | assert len(self.key) == 32 174 | except: 175 | throw_exception("[error]: EncodingAESKey unvalid !", FormatException) 176 | self.m_sToken = sToken 177 | self.m_sCorpid = sCorpId 178 | 179 | def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): 180 | sha1 = SHA1() 181 | ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr) 182 | if ret != 0: 183 | return ret, None 184 | if not signature == sMsgSignature: 185 | return WXBizMsgCrypt_ValidateSignature_Error, None 186 | pc = Prpcrypt(self.key) 187 | ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sCorpid) 188 | return ret, sReplyEchoStr 189 | 190 | def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): 191 | pc = Prpcrypt(self.key) 192 | ret, encrypt = pc.encrypt(sReplyMsg, self.m_sCorpid) 193 | if ret != 0: 194 | return ret, None 195 | if timestamp is None: 196 | timestamp = str(int(time.time())) 197 | sha1 = SHA1() 198 | ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt) 199 | if ret != 0: 200 | return ret, None 201 | xmlParse = XMLParse() 202 | return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce) 203 | 204 | def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): 205 | xmlParse = XMLParse() 206 | ret, encrypt, touser_name = xmlParse.extract(sPostData) 207 | if ret != 0: 208 | return ret, None 209 | sha1 = SHA1() 210 | ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) 211 | if ret != 0: 212 | return ret, None 213 | if not signature == sMsgSignature: 214 | return WXBizMsgCrypt_ValidateSignature_Error, None 215 | pc = Prpcrypt(self.key) 216 | ret, xml_content = pc.decrypt(encrypt, self.m_sCorpid) 217 | return ret, xml_content 218 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------