├── .github └── FUNDING.yml ├── doc └── img │ ├── demo.png │ ├── API_key.png │ ├── Railway_log.png │ ├── Railway_QRCode.png │ ├── Railway_config.png │ ├── Railway_deploy.png │ ├── computenest_qr_code.png │ ├── computenest_how_to_use.png │ ├── computenest_resource_config.png │ ├── computenest_software_config.png │ └── deploy_to_computenest.svg ├── .env.example ├── config.yaml.example ├── src ├── interface.ts ├── config.ts ├── main.ts └── chatgpt.ts ├── docker-compose.yml ├── pyproject.toml ├── poetry.lock ├── Dockerfile ├── LICENSE ├── package.json ├── .gitignore ├── .dockerignore ├── tsconfig.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: kx-Huang 2 | buy_me_a_coffee: kxhuang 3 | -------------------------------------------------------------------------------- /doc/img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/demo.png -------------------------------------------------------------------------------- /doc/img/API_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/API_key.png -------------------------------------------------------------------------------- /doc/img/Railway_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/Railway_log.png -------------------------------------------------------------------------------- /doc/img/Railway_QRCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/Railway_QRCode.png -------------------------------------------------------------------------------- /doc/img/Railway_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/Railway_config.png -------------------------------------------------------------------------------- /doc/img/Railway_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/Railway_deploy.png -------------------------------------------------------------------------------- /doc/img/computenest_qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/computenest_qr_code.png -------------------------------------------------------------------------------- /doc/img/computenest_how_to_use.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/computenest_how_to_use.png -------------------------------------------------------------------------------- /doc/img/computenest_resource_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/computenest_resource_config.png -------------------------------------------------------------------------------- /doc/img/computenest_software_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kx-Huang/ChatGPT-on-WeChat/HEAD/doc/img/computenest_software_config.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 2 | OPENAI_ORGANIZATION_KEY="org-XXXXXXXXXXXXXXX" 3 | CHATGPT_TRIGGER_KEYWORD="Hi bot:" -------------------------------------------------------------------------------- /config.yaml.example: -------------------------------------------------------------------------------- 1 | openaiApiKey: "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 2 | openaiOrganizationID: "org-XXXXXXXXXXXXXXX" 3 | chatgptTriggerKeyword: "Hi bot:" -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | export interface IConfig { 2 | openaiApiKey: string; 3 | openaiOrganizationID?: string; 4 | chatgptTriggerKeyword: string; 5 | } 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | openai-on-wechat: 5 | image: openai-on-wechat 6 | build: . 7 | restart: always 8 | logging: 9 | driver: "json-file" 10 | options: 11 | max-size: "10m" 12 | max-file: "3" 13 | volumes: 14 | - ./config.yaml:/app/config.yaml 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "chatgpt-on-wechat" 3 | version = "0.0.1" 4 | description = "WeChat Bot with Chatgpt" 5 | authors = ["Michael Huang "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | 10 | [tool.poetry.dev-dependencies] 11 | 12 | [build-system] 13 | requires = ["poetry-core>=1.0.0"] 14 | build-backend = "poetry.core.masonry.api" 15 | [experimental] 16 | new-installer = false 17 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "certifi" 3 | version = "2022.12.7" 4 | description = "Python package for providing Mozilla's CA Bundle." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [metadata] 10 | lock-version = "1.1" 11 | python-versions = "^3.8" 12 | content-hash = "fafb334cb038533f851c23d0b63254223abf72ce4f02987e7064b0c95566699a" 13 | 14 | [metadata.files] 15 | certifi = [ 16 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 17 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 18 | ] 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | WORKDIR /app 3 | ARG POETRY_VERSION=1.2.2 4 | 5 | # 更新并安装 nodejs 6 | RUN apt-get update && \ 7 | curl -sL https://deb.nodesource.com/setup_16.x | bash - && \ 8 | apt-get install -y nodejs && \ 9 | rm -rf /var/lib/apt/lists/* # 正确的清理 apt 缓存命令 10 | 11 | # 安装 Poetry 12 | RUN pip3 install --no-cache-dir poetry==${POETRY_VERSION} && \ 13 | rm -rf ~/.cache/pip/ 14 | 15 | COPY package*.json ./ 16 | COPY pyproject.toml ./ 17 | COPY poetry.lock ./ 18 | 19 | # 设置环境变量以跳过 Puppeteer 的 Chromium 下载 20 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true 21 | 22 | # 安装 Python 和 Node.js 依赖 23 | RUN poetry install && \ 24 | npm install && \ 25 | rm -rf ~/.npm/ 26 | 27 | COPY . . 28 | 29 | CMD ["npm", "run", "dev"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2023, Kexuan (Michael) Huang 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-on-wechat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/main.ts", 6 | "export": "src/main.ts", 7 | "scripts": { 8 | "dev": "nodemon --exec node --watch config.yaml --loader ts-node/esm src/main.ts", 9 | "build": "tsc" 10 | }, 11 | "author": "Michael Huang", 12 | "license": "ISC", 13 | "dependencies": { 14 | "openai": "^3.2.1", 15 | "@dqbd/tiktoken": "^0.4.0", 16 | "dotenv": "^16.0.3", 17 | "qrcode": "^1.5.1", 18 | "uuid": "^9.0.0", 19 | "wechaty": "^1.20.2", 20 | "wechaty-puppet-wechat": "^1.18.4", 21 | "yaml": "^2.1.3", 22 | "nth-check": ">=2.0.1" 23 | }, 24 | "devDependencies": { 25 | "@types/qrcode": "^1.5.0", 26 | "@types/uuid": "^9.0.0", 27 | "nodemon": "^2.0.20", 28 | "ts-node": "^10.9.1", 29 | "nth-check": ">=2.0.1" 30 | }, 31 | "nodemonConfig": { 32 | "watch": "src", 33 | "ext": "ts", 34 | "exec": "node --loader ts-node/esm src/main.ts", 35 | "delay": 500 36 | }, 37 | "type": "module" 38 | } 39 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | dotenv.config(); 3 | import fs from "fs"; 4 | import { parse } from "yaml"; 5 | import { IConfig } from "./interface"; 6 | 7 | let configFile: any = {}; 8 | 9 | // get configurations from 'config.yaml' first 10 | if (fs.existsSync("./config.yaml")) { 11 | const file = fs.readFileSync("./config.yaml", "utf8"); 12 | configFile = parse(file); 13 | } 14 | // if 'config.yaml' not exist, read them from env 15 | else { 16 | configFile = { 17 | openaiApiKey: process.env.OPENAI_API_KEY, 18 | openaiOrganizationID: process.env.OPENAI_ORGANIZATION_KEY, 19 | chatgptTriggerKeyword: process.env.CHATGPT_TRIGGER_KEYWORD, 20 | }; 21 | } 22 | 23 | // warning if no OpenAI API key found 24 | if (configFile.openaiApiKey === undefined) { 25 | console.error( 26 | "⚠️ No OPENAI_API_KEY found in env, please export to env or configure in config.yaml" 27 | ); 28 | } 29 | 30 | export const Config: IConfig = { 31 | openaiApiKey: configFile.openaiApiKey, 32 | openaiOrganizationID: configFile.openaiOrganizationID || "", 33 | chatgptTriggerKeyword: configFile.chatgptTriggerKeyword || "", 34 | }; 35 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import QRCode from "qrcode"; 2 | import { WechatyBuilder } from "wechaty"; 3 | import { ChatGPTBot } from "./chatgpt.js"; 4 | 5 | // Wechaty instance 6 | const weChatBot = WechatyBuilder.build({ 7 | name: "my-wechat-bot", 8 | }); 9 | // ChatGPTBot instance 10 | const chatGPTBot = new ChatGPTBot(); 11 | 12 | async function main() { 13 | weChatBot 14 | // scan QR code for login 15 | .on("scan", async (qrcode, status) => { 16 | const url = `https://wechaty.js.org/qrcode/${encodeURIComponent(qrcode)}`; 17 | console.log(`💡 Scan QR Code in WeChat to login: ${status}\n${url}`); 18 | console.log( 19 | await QRCode.toString(qrcode, { type: "terminal", small: true }) 20 | ); 21 | }) 22 | // login to WeChat desktop account 23 | .on("login", async (user: any) => { 24 | console.log(`✅ User ${user} has logged in`); 25 | chatGPTBot.setBotName(user.name()); 26 | await chatGPTBot.startGPTBot(); 27 | }) 28 | // message handler 29 | .on("message", async (message: any) => { 30 | try { 31 | // prevent accidentally respond to history chat on restart 32 | // only respond to message later than chatbot start time 33 | const msgDate = message.date(); 34 | if (msgDate.getTime() <= chatGPTBot.startTime.getTime()) { 35 | return; 36 | } 37 | console.log(`📨 ${message}`); 38 | // handle message for customized task handlers 39 | await chatGPTBot.onCustimzedTask(message); 40 | // handle message for chatGPT bot 41 | await chatGPTBot.onMessage(message); 42 | } catch (e) { 43 | console.error(`❌ ${e}`); 44 | } 45 | }); 46 | 47 | try { 48 | await weChatBot.start(); 49 | } catch (e) { 50 | console.error(`❌ Your Bot failed to start: ${e}`); 51 | console.log( 52 | "🤔 Can you login WeChat in browser? The bot works on the desktop WeChat" 53 | ); 54 | } 55 | } 56 | main(); 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/node 145 | n 146 | *memory-card.json 147 | # Created by https://www.toptal.com/developers/gitignore/api/python 148 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 149 | 150 | ### Python ### 151 | # Byte-compiled / optimized / DLL files 152 | __pycache__/ 153 | *.py[cod] 154 | *$py.class 155 | 156 | # C extensions 157 | *.so 158 | 159 | # Distribution / packaging 160 | .Python 161 | build/ 162 | develop-eggs/ 163 | dist/ 164 | downloads/ 165 | eggs/ 166 | .eggs/ 167 | lib/ 168 | lib64/ 169 | parts/ 170 | sdist/ 171 | var/ 172 | wheels/ 173 | share/python-wheels/ 174 | *.egg-info/ 175 | .installed.cfg 176 | *.egg 177 | MANIFEST 178 | 179 | # PyInstaller 180 | # Usually these files are written by a python script from a template 181 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 182 | *.manifest 183 | *.spec 184 | 185 | # Installer logs 186 | pip-log.txt 187 | pip-delete-this-directory.txt 188 | 189 | # Unit test / coverage reports 190 | htmlcov/ 191 | .tox/ 192 | .nox/ 193 | .coverage 194 | .coverage.* 195 | .cache 196 | nosetests.xml 197 | coverage.xml 198 | *.cover 199 | *.py,cover 200 | .hypothesis/ 201 | .pytest_cache/ 202 | cover/ 203 | 204 | # Translations 205 | *.mo 206 | *.pot 207 | 208 | # Django stuff: 209 | *.log 210 | local_settings.py 211 | db.sqlite3 212 | db.sqlite3-journal 213 | 214 | # Flask stuff: 215 | instance/ 216 | .webassets-cache 217 | 218 | # Scrapy stuff: 219 | .scrapy 220 | 221 | # Sphinx documentation 222 | docs/_build/ 223 | 224 | # PyBuilder 225 | .pybuilder/ 226 | target/ 227 | 228 | # Jupyter Notebook 229 | .ipynb_checkpoints 230 | 231 | # IPython 232 | profile_default/ 233 | ipython_config.py 234 | 235 | # pyenv 236 | # For a library or package, you might want to ignore these files since the code is 237 | # intended to run in multiple environments; otherwise, check them in: 238 | # .python-version 239 | 240 | # pipenv 241 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 242 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 243 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 244 | # install all needed dependencies. 245 | #Pipfile.lock 246 | 247 | # poetry 248 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 249 | # This is especially recommended for binary packages to ensure reproducibility, and is more 250 | # commonly ignored for libraries. 251 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 252 | #poetry.lock 253 | 254 | # pdm 255 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 256 | #pdm.lock 257 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 258 | # in version control. 259 | # https://pdm.fming.dev/#use-with-ide 260 | .pdm.toml 261 | 262 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 263 | __pypackages__/ 264 | 265 | # Celery stuff 266 | celerybeat-schedule 267 | celerybeat.pid 268 | 269 | # SageMath parsed files 270 | *.sage.py 271 | 272 | # Environments 273 | .env 274 | .venv 275 | env/ 276 | venv/ 277 | ENV/ 278 | env.bak/ 279 | venv.bak/ 280 | 281 | # Spyder project settings 282 | .spyderproject 283 | .spyproject 284 | 285 | # Rope project settings 286 | .ropeproject 287 | 288 | # mkdocs documentation 289 | /site 290 | 291 | # mypy 292 | .mypy_cache/ 293 | .dmypy.json 294 | dmypy.json 295 | 296 | # Pyre type checker 297 | .pyre/ 298 | 299 | # pytype static type analyzer 300 | .pytype/ 301 | 302 | # Cython debug symbols 303 | cython_debug/ 304 | 305 | # PyCharm 306 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 307 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 308 | # and can be added to the global gitignore or merged into this file. For a more nuclear 309 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 310 | .idea/ 311 | 312 | ### Python Patch ### 313 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 314 | poetry.toml 315 | 316 | 317 | # End of https://www.toptal.com/developers/gitignore/api/python 318 | n 319 | config.json 320 | cache.json 321 | config.yaml 322 | .vscode -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/node 145 | n 146 | *memory-card.json 147 | # Created by https://www.toptal.com/developers/gitignore/api/python 148 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 149 | 150 | ### Python ### 151 | # Byte-compiled / optimized / DLL files 152 | __pycache__/ 153 | *.py[cod] 154 | *$py.class 155 | 156 | # C extensions 157 | *.so 158 | 159 | # Distribution / packaging 160 | .Python 161 | build/ 162 | develop-eggs/ 163 | dist/ 164 | downloads/ 165 | eggs/ 166 | .eggs/ 167 | lib/ 168 | lib64/ 169 | parts/ 170 | sdist/ 171 | var/ 172 | wheels/ 173 | share/python-wheels/ 174 | *.egg-info/ 175 | .installed.cfg 176 | *.egg 177 | MANIFEST 178 | 179 | # PyInstaller 180 | # Usually these files are written by a python script from a template 181 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 182 | *.manifest 183 | *.spec 184 | 185 | # Installer logs 186 | pip-log.txt 187 | pip-delete-this-directory.txt 188 | 189 | # Unit test / coverage reports 190 | htmlcov/ 191 | .tox/ 192 | .nox/ 193 | .coverage 194 | .coverage.* 195 | .cache 196 | nosetests.xml 197 | coverage.xml 198 | *.cover 199 | *.py,cover 200 | .hypothesis/ 201 | .pytest_cache/ 202 | cover/ 203 | 204 | # Translations 205 | *.mo 206 | *.pot 207 | 208 | # Django stuff: 209 | *.log 210 | local_settings.py 211 | db.sqlite3 212 | db.sqlite3-journal 213 | 214 | # Flask stuff: 215 | instance/ 216 | .webassets-cache 217 | 218 | # Scrapy stuff: 219 | .scrapy 220 | 221 | # Sphinx documentation 222 | docs/_build/ 223 | 224 | # PyBuilder 225 | .pybuilder/ 226 | target/ 227 | 228 | # Jupyter Notebook 229 | .ipynb_checkpoints 230 | 231 | # IPython 232 | profile_default/ 233 | ipython_config.py 234 | 235 | # pyenv 236 | # For a library or package, you might want to ignore these files since the code is 237 | # intended to run in multiple environments; otherwise, check them in: 238 | # .python-version 239 | 240 | # pipenv 241 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 242 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 243 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 244 | # install all needed dependencies. 245 | #Pipfile.lock 246 | 247 | # poetry 248 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 249 | # This is especially recommended for binary packages to ensure reproducibility, and is more 250 | # commonly ignored for libraries. 251 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 252 | #poetry.lock 253 | 254 | # pdm 255 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 256 | #pdm.lock 257 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 258 | # in version control. 259 | # https://pdm.fming.dev/#use-with-ide 260 | .pdm.toml 261 | 262 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 263 | __pypackages__/ 264 | 265 | # Celery stuff 266 | celerybeat-schedule 267 | celerybeat.pid 268 | 269 | # SageMath parsed files 270 | *.sage.py 271 | 272 | # Environments 273 | .env 274 | .venv 275 | env/ 276 | venv/ 277 | ENV/ 278 | env.bak/ 279 | venv.bak/ 280 | 281 | # Spyder project settings 282 | .spyderproject 283 | .spyproject 284 | 285 | # Rope project settings 286 | .ropeproject 287 | 288 | # mkdocs documentation 289 | /site 290 | 291 | # mypy 292 | .mypy_cache/ 293 | .dmypy.json 294 | dmypy.json 295 | 296 | # Pyre type checker 297 | .pyre/ 298 | 299 | # pytype static type analyzer 300 | .pytype/ 301 | 302 | # Cython debug symbols 303 | cython_debug/ 304 | 305 | # PyCharm 306 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 307 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 308 | # and can be added to the global gitignore or merged into this file. For a more nuclear 309 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 310 | #.idea/ 311 | 312 | ### Python Patch ### 313 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 314 | poetry.toml 315 | 316 | 317 | # End of https://www.toptal.com/developers/gitignore/api/python 318 | n 319 | config.json 320 | cache.json 321 | config.yaml 322 | .vscode -------------------------------------------------------------------------------- /src/chatgpt.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "./config.js"; 2 | import { Message } from "wechaty"; 3 | import { ContactInterface, RoomInterface } from "wechaty/impls"; 4 | import { Configuration, OpenAIApi } from "openai"; 5 | 6 | enum MessageType { 7 | Unknown = 0, 8 | Attachment = 1, // Attach(6), 9 | Audio = 2, // Audio(1), Voice(34) 10 | Contact = 3, // ShareCard(42) 11 | ChatHistory = 4, // ChatHistory(19) 12 | Emoticon = 5, // Sticker: Emoticon(15), Emoticon(47) 13 | Image = 6, // Img(2), Image(3) 14 | Text = 7, // Text(1) 15 | Location = 8, // Location(48) 16 | MiniProgram = 9, // MiniProgram(33) 17 | GroupNote = 10, // GroupNote(53) 18 | Transfer = 11, // Transfers(2000) 19 | RedEnvelope = 12, // RedEnvelopes(2001) 20 | Recalled = 13, // Recalled(10002) 21 | Url = 14, // Url(5) 22 | Video = 15, // Video(4), Video(43) 23 | Post = 16, // Moment, Channel, Tweet, etc 24 | } 25 | 26 | export class ChatGPTBot { 27 | // chatbot name (WeChat account name) 28 | botName: string = ""; 29 | 30 | // chatbot start time (prevent duplicate response on restart) 31 | startTime: Date = new Date(); 32 | 33 | // self-chat may cause some issue for some WeChat Account 34 | // please set to true if self-chat cause some errors 35 | disableSelfChat: boolean = false; 36 | 37 | // chatbot trigger keyword 38 | chatgptTriggerKeyword: string = Config.chatgptTriggerKeyword; 39 | 40 | // ChatGPT error response 41 | chatgptErrorMessage: string = "🤖️:ChatGPT摆烂了,请稍后再试~"; 42 | 43 | // ChatGPT model configuration 44 | // please refer to the OpenAI API doc: https://beta.openai.com/docs/api-reference/introduction 45 | chatgptModelConfig: object = { 46 | // this model field is required 47 | model: "gpt-4o", 48 | // add your ChatGPT model parameters below 49 | temperature: 0.8, 50 | // max_tokens: 2000, 51 | }; 52 | 53 | // ChatGPT system content configuration (guided by OpenAI official document) 54 | currentDate: string = new Date().toISOString().split("T")[0]; 55 | chatgptSystemContent: string = `You are ChatGPT, a large language model trained by OpenAI. Answer in user's language as concisely as possible.\nKnowledge cutoff: October 2023\nCurrent date: ${this.currentDate}`; 56 | 57 | // message size for a single reply by the bot 58 | SINGLE_MESSAGE_MAX_SIZE: number = 500; 59 | 60 | // OpenAI API 61 | private openaiAccountConfig: any; // OpenAI API key (required) and organization key (optional) 62 | private openaiApiInstance: any; // OpenAI API instance 63 | 64 | // set bot name during login stage 65 | setBotName(botName: string) { 66 | this.botName = botName; 67 | } 68 | 69 | // get trigger keyword in group chat: (@Name ) 70 | // in group chat, replace the special character after "@username" to space 71 | // to prevent cross-platfrom mention issue 72 | private get chatGroupTriggerKeyword(): string { 73 | return `@${this.botName} ${this.chatgptTriggerKeyword || ""}`; 74 | } 75 | 76 | // configure API with model API keys and run an initial test 77 | async startGPTBot() { 78 | try { 79 | // OpenAI account configuration 80 | this.openaiAccountConfig = new Configuration({ 81 | organization: Config.openaiOrganizationID, 82 | apiKey: Config.openaiApiKey, 83 | }); 84 | // OpenAI API instance 85 | this.openaiApiInstance = new OpenAIApi(this.openaiAccountConfig); 86 | // Hint user the trigger keyword in private chat and group chat 87 | console.log(`🤖️ ChatGPT name is: ${this.botName}`); 88 | console.log( 89 | `🎯 Trigger keyword in private chat is: ${this.chatgptTriggerKeyword}` 90 | ); 91 | console.log( 92 | `🎯 Trigger keyword in group chat is: ${this.chatGroupTriggerKeyword}` 93 | ); 94 | // Run an initial test to confirm API works fine 95 | await this.onChatGPT("Say Hello World"); 96 | console.log(`✅ ChatGPT starts success, ready to handle message!`); 97 | } catch (e) { 98 | console.error(`❌ ${e}`); 99 | } 100 | } 101 | 102 | // get clean message by removing reply separater and group mention characters 103 | private cleanMessage( 104 | rawText: string, 105 | isPrivateChat: boolean = false 106 | ): string { 107 | let text = rawText; 108 | const item = rawText.split("- - - - - - - - - - - - - - -"); 109 | if (item.length > 1) { 110 | text = item[item.length - 1]; 111 | } 112 | return text.slice( 113 | isPrivateChat 114 | ? this.chatgptTriggerKeyword.length 115 | : this.chatGroupTriggerKeyword.length 116 | ); 117 | } 118 | 119 | // check whether ChatGPT bot can be triggered 120 | private triggerGPTMessage( 121 | text: string, 122 | isPrivateChat: boolean = false 123 | ): boolean { 124 | const chatgptTriggerKeyword = this.chatgptTriggerKeyword; 125 | let triggered = false; 126 | if (isPrivateChat) { 127 | triggered = chatgptTriggerKeyword 128 | ? text.startsWith(chatgptTriggerKeyword) 129 | : true; 130 | } else { 131 | // due to un-unified @ lagging character, ignore it and just match: 132 | // 1. the "@username" (mention) 133 | // 2. trigger keyword 134 | // start with @username 135 | const textMention = `@${this.botName}`; 136 | const startsWithMention = text.startsWith(textMention); 137 | const textWithoutMention = text.slice(textMention.length + 1); 138 | const followByTriggerKeyword = textWithoutMention.startsWith( 139 | this.chatgptTriggerKeyword 140 | ); 141 | triggered = startsWithMention && followByTriggerKeyword; 142 | } 143 | if (triggered) { 144 | console.log(`🎯 ChatGPT triggered: ${text}`); 145 | } 146 | return triggered; 147 | } 148 | 149 | // filter out the message that does not need to be processed 150 | private isNonsense( 151 | talker: ContactInterface, 152 | messageType: MessageType, 153 | text: string 154 | ): boolean { 155 | return ( 156 | (this.disableSelfChat && talker.self()) || 157 | messageType != MessageType.Text || 158 | talker.name() == "微信团队" || 159 | // video or voice reminder 160 | text.includes("收到一条视频/语音聊天消息,请在手机上查看") || 161 | // red pocket reminder 162 | text.includes("收到红包,请在手机上查看") || 163 | // location information 164 | text.includes("/cgi-bin/mmwebwx-bin/webwxgetpubliclinkimg") 165 | ); 166 | } 167 | 168 | // create messages for ChatGPT API request 169 | // TODO: store history chats for supporting context chat 170 | private createMessages(text: string): Array { 171 | const messages = [ 172 | { 173 | role: "system", 174 | content: this.chatgptSystemContent, 175 | }, 176 | { 177 | role: "user", 178 | content: text, 179 | }, 180 | ]; 181 | return messages; 182 | } 183 | 184 | // send question to ChatGPT with OpenAI API and get answer 185 | private async onChatGPT(text: string): Promise { 186 | const inputMessages = this.createMessages(text); 187 | try { 188 | // config OpenAI API request body 189 | const response = await this.openaiApiInstance.createChatCompletion({ 190 | ...this.chatgptModelConfig, 191 | messages: inputMessages, 192 | }); 193 | // use OpenAI API to get ChatGPT reply message 194 | const chatgptReplyMessage = 195 | response?.data?.choices[0]?.message?.content?.trim(); 196 | console.log(`🤖️ ChatGPT says: ${chatgptReplyMessage}`); 197 | return chatgptReplyMessage; 198 | } catch (e: any) { 199 | console.error(`❌ ${e}`); 200 | const errorResponse = e?.response; 201 | const errorCode = errorResponse?.status; 202 | const errorStatus = errorResponse?.statusText; 203 | const errorMessage = errorResponse?.data?.error?.message; 204 | if (errorCode && errorStatus) { 205 | const errorLog = `Code ${errorCode}: ${errorStatus}`; 206 | console.error(`❌ ${errorLog}`); 207 | } 208 | if (errorMessage) { 209 | console.error(`❌ ${errorMessage}`); 210 | } 211 | return this.chatgptErrorMessage; 212 | } 213 | } 214 | 215 | // reply with the segmented messages from a single-long message 216 | private async reply( 217 | talker: RoomInterface | ContactInterface, 218 | mesasge: string 219 | ): Promise { 220 | const messages: Array = []; 221 | let message = mesasge; 222 | while (message.length > this.SINGLE_MESSAGE_MAX_SIZE) { 223 | messages.push(message.slice(0, this.SINGLE_MESSAGE_MAX_SIZE)); 224 | message = message.slice(this.SINGLE_MESSAGE_MAX_SIZE); 225 | } 226 | messages.push(message); 227 | for (const msg of messages) { 228 | await talker.say(msg); 229 | } 230 | } 231 | 232 | // reply to private message 233 | private async onPrivateMessage(talker: ContactInterface, text: string) { 234 | // get reply from ChatGPT 235 | const chatgptReplyMessage = await this.onChatGPT(text); 236 | // send the ChatGPT reply to chat 237 | await this.reply(talker, chatgptReplyMessage); 238 | } 239 | 240 | // reply to group message 241 | private async onGroupMessage(room: RoomInterface, text: string) { 242 | // get reply from ChatGPT 243 | const chatgptReplyMessage = await this.onChatGPT(text); 244 | // the whole reply consist of: original text and bot reply 245 | const wholeReplyMessage = `${text}\n----------\n${chatgptReplyMessage}`; 246 | await this.reply(room, wholeReplyMessage); 247 | } 248 | 249 | // receive a message (main entry) 250 | async onMessage(message: Message) { 251 | const talker = message.talker(); 252 | const rawText = message.text(); 253 | const room = message.room(); 254 | const messageType = message.type(); 255 | const isPrivateChat = !room; 256 | // do nothing if the message: 257 | // 1. is irrelevant (e.g. voice, video, location...), or 258 | // 2. doesn't trigger bot (e.g. wrong trigger-word) 259 | if ( 260 | this.isNonsense(talker, messageType, rawText) || 261 | !this.triggerGPTMessage(rawText, isPrivateChat) 262 | ) { 263 | return; 264 | } 265 | // clean the message for ChatGPT input 266 | const text = this.cleanMessage(rawText, isPrivateChat); 267 | // reply to private or group chat 268 | if (isPrivateChat) { 269 | return await this.onPrivateMessage(talker, text); 270 | } else { 271 | return await this.onGroupMessage(room, text); 272 | } 273 | } 274 | 275 | // handle message for customized task handlers 276 | async onCustimzedTask(message: Message) { 277 | // e.g. if a message starts with "麦扣", the bot sends "🤖️:call我做咩啊大佬!" 278 | const myKeyword = "麦扣"; 279 | if (message.text().includes(myKeyword)) { 280 | const myTaskContent = `回复所有含有"${myKeyword}"的消息`; 281 | const myReply = "🤖️:call我做咩啊大佬"; 282 | await message.say(myReply); 283 | console.log(`🎯 Customized task triggered: ${myTaskContent}`); 284 | console.log(`🤖️ ChatGPT says: ${myReply}`); 285 | return; 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "esnext", /* Specify what module code is generated. */ 28 | "rootDir": "src", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "dist", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT on WeChat ![GitHub License](https://img.shields.io/github/license/kx-huang/chatgpt-on-wechat?label=License&color=orange) [![wakatime](https://wakatime.com/badge/github/kx-Huang/ChatGPT-on-WeChat.svg)](https://wakatime.com/badge/github/kx-Huang/ChatGPT-on-WeChat) ![Railway Deploy](https://img.shields.io/github/checks-status/kx-huang/chatgpt-on-wechat/master?logo=railway&style=flat&label=Deploy) ![GitHub Repo stars](https://img.shields.io/github/stars/kx-huang/chatgpt-on-wechat?style=social) 2 | 3 | 4 | 5 | 🤖️ Turn your WeChat into ChatGPT [**within only 2 steps!**](#12-deploy-on-cloud) 🤖️ 6 | 7 |

8 | Group chat demo for @kx-Huang/ChatGPT-on-WeChat 9 |

10 | 11 | ## Features 12 | 13 | This project is implemented based on [this amazing project](https://github.com/fuergaosi233/wechat-chatgpt) that I contibuted before, with [`Wechaty SDK`](https://github.com/wechaty/wechaty) and `OpenAI API`, we achieve: 14 | 15 | - fast and robust connection to a set of AI models with different features, typically `gpt-4o` and `gpt-3.5-turbo` which powers `ChatGPT` 16 | - stable, persistent and rapid deployment on cloud servers `Railway` 17 | 18 | ## 0. Table of Content 19 | 20 | - [ChatGPT on WeChat ](#chatgpt-on-wechat----) 21 | - [1. How to Deploy this Bot?](#1-how-to-deploy-this-bot) 22 | - [1.1 Deploy in Local](#11-deploy-in-local) 23 | - [1.1.1 Get your OpenAI API Keys](#111-get-your-openai-api-keys) 24 | - [1.1.2 Configure Environment Variables](#112-configure-environment-variables) 25 | - [1.1.3 Setup the Docker](#113-setup-the-docker) 26 | - [1.1.4 Login your WeChat](#114-login-your-wechat) 27 | - [1.2 Deploy on Railway](#12-deploy-on-railway) 28 | - [1.2.1 Configure on `Railway`](#121-configure-on-railway) 29 | - [1.2.2 Deploy \& Login on `Railway`](#122-deploy--login-on-railway) 30 | - [1.3 Deploy on Alibaba Cloud ComputeNest](#13-deploy-on-alibaba-cloud-computenest) 31 | - [2. Any Fancy Advanced Settings?](#2-any-fancy-advanced-settings) 32 | - [2.1 Config Reply in Error](#21-config-reply-in-error) 33 | - [2.2 Config `OpenAI` Models](#22-config-openai-models) 34 | - [2.3 Config Model Features](#23-config-model-features) 35 | - [2.4 Add Customized Task Handler](#24-add-customized-task-handler) 36 | - [3. Common Errors and Troubleshooting](#3-common-errors-and-troubleshooting) 37 | - [3.1 Assertion Error during Login or Self-chat 🤯](#31-assertion-error-during-login-or-self-chat-) 38 | - [3.2 I can't trigger auto reply 🤔](#32-i-cant-trigger-auto-reply-) 39 | - [4. How to Contribute to this Project?](#4-how-to-contribute-to-this-project) 40 | - [5. Acknowledgement](#5-acknowledgement) 41 | - [Thanks for your support by starring this project!](#thanks-for-your-support-by-starring-this-project) 42 | 43 | ## 1. How to Deploy this Bot? 44 | 45 | You can deploy **in local** or **on cloud**, whatever you want. 46 | 47 | The [deploy on cloud](#12-deploy-on-cloud) method is recommended. 48 | 49 | ### 1.1 Deploy in Local 50 | 51 | #### 1.1.1 Get your OpenAI API Keys 52 | 53 | - `openaiApiKey` can be generated in the [**API Keys Page** in your OpenAI account](https://beta.openai.com/account/api-keys) 54 | - `openaiOrganizationID` is optional, which can be found in the [**Settings Page** in your Open AI account](https://beta.openai.com/account/org-settings) 55 | 56 | --- 57 | 58 | #### 1.1.2 Configure Environment Variables 59 | 60 | You can copy the template `config.yaml.example` into a new file `config.yaml`, and paste the configurations: 61 | 62 | ```yaml 63 | openaiApiKey: "" 64 | openaiOrganizationID: "" 65 | chatgptTriggerKeyword: "" 66 | ``` 67 | 68 | Or you can export the environment variables listed in `.env.example` to your system, which is a more encouraged method to keep your `OpenAI API Key` safe: 69 | 70 | ```bash 71 | export OPENAI_API_KEY="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 72 | export OPENAI_ORGANIZATION_KEY="org-XXXXXXXXXXXXXXX" 73 | export CHATGPT_TRIGGER_KEYWORD="Hi bot:" 74 | ``` 75 | 76 | **Please note:** 77 | 78 | - `chatgptTriggerKeyword` is the keyword which can trigger auto-reply: 79 | - In private chat, the message **starts with** it will trigger auto-reply 80 | - In group chat, the message **starts with** `@Name ` will trigger auto-reply 81 | - `chatgptTriggerKeyword` can be **empty string**, which means: 82 | - In private chat, **every messages** will trigger auto-reply 83 | - In group chat, only **"@ the bot"** will trigger auto-reply 84 | 85 | --- 86 | 87 | #### 1.1.3 Setup the Docker 88 | 89 | 1. Setup Docker Image 90 | 91 | ```bash 92 | docker build -t chatgpt-on-wechat . 93 | ``` 94 | 95 | 2. Setup Docker Container 96 | 97 | ```bash 98 | docker run -v $(pwd)/config.yaml:/app/config.yaml chatgpt-on-wechat 99 | ``` 100 | 101 | You can also build with Docker Compose: 102 | 103 | 1. Start the container 104 | 105 | ```bash 106 | docker-compose up -d 107 | ``` 108 | 109 | 2. View the QR code to log in to wechat 110 | 111 | ```bash 112 | docker-compose logs -f 113 | ``` 114 | 115 | --- 116 | 117 | #### 1.1.4 Login your WeChat 118 | 119 | Once you deploy the bot successfully, just follow the `terminal` or `Logs` in Docker container prompt carefully: 120 | 121 | 1. Scan the QR Code with mobile WeChat 122 | 2. Click "Log in" to allow desktop login (where our bot stays) 123 | 3. Wait a few seconds and start chatting! 124 | 125 | 🤖 **Enjoy your powerful chatbot!** 🤖 126 | 127 | --- 128 | 129 | ### 1.2 Deploy on Railway 130 | 131 | Click the button below to fork this repo and deploy with Railway! 132 | 133 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/zKIfYk?referralCode=D6wD0x) 134 | 135 | --- 136 | 137 | #### 1.2.1 Configure on `Railway` 138 | 139 | Fill in the following blanks: 140 | 141 | 1. Your forked repo name (can be any name you like) 142 | 2. Choose make it private or not (also up to you) 143 | 3. Environment variables (for how to get OpenAI API keys, please refer to [1.1.1 Get your OpenAI API Keys](#111-get-your-openai-api-keys)) 144 | 145 | ![Railway Config](doc/img/Railway_config.png) 146 | 147 | **Please note:** 148 | 149 | Make sure the environment variables are set in RailWay instead of writing directly in `config.yaml`. It's really **NOT** recommended to implicitly write out your `OpenAI API Key` in public repo. Anyone with your key can get access to the OpenAI API services, and it's possbile for you to lose money if you pay for that. 150 | 151 | --- 152 | 153 | #### 1.2.2 Deploy & Login on `Railway` 154 | 155 | The deploy process is automatic. It may take a few minutes for the first time. As you see the `Success`, click the tab to see the details. (which is your secret WeChat console!) 156 | 157 | ![Railway Deploy](doc/img/Railway_deploy.png) 158 | 159 | Click `Deply Logs` and you will see everything is setting up, wait for a QR Code to pop up. Scan it as if you are login to your desktop WeChat, and click "Log in" on your mobile WeChat. 160 | 161 | ![Railway Scan QR Code](doc/img/Railway_QRCode.png) 162 | 163 | Finally, everything is good to go! You will see the logs when people sending you messagem, and whenever the chatbot is auto-triggered to reply. 164 | 165 | ### 1.3 Deploy on Alibaba Cloud ComputeNest 166 | 167 | One-click deployment on Alibaba Cloud ComputeNest: 168 | 169 | [![Deploy on AlibabaCloud ComputeNest](doc/img/deploy_to_computenest.svg)](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=ChatGPT-on-WeChat社区版) 170 | 171 | Follow the deployment guide to deploy ChatGPT-on-WeChat on Alibaba Cloud. Both domestic site and internationl sites are supported. 172 | - [Deployment Guide (domestic site)](https://computenest.console.aliyun.com/service/detail/cn-hangzhou/service-a81e49ab7dd24520a365?isInstance=true) 173 | - [Deployment Guide (internationl site)](https://computenest.console.aliyun.com/service/detail/ap-southeast-1/service-37a1f9f9b9e1482ba61b?isInstance=true) 174 | Switch Alibaba Cloud console's language to see guide in different language. 175 | 176 | First, provides cloud resource configurations such as ECS instance type and network configurations. 177 | ![ECS instance configuration](doc/img/computenest_resource_config.png) 178 | Also needs to set ChatGPT-On-WeChat software configuration. 179 | ![ChatGPT-On-WeChat software configuration](doc/img/computenest_software_config.png) 180 | 181 | When you confirm to deploy, Alibaba Cloud ComputeNest creates ECS instance in your owner Alibaba Cloud account, deploys ChatGPT-on-WeChat application and starts it on ECS instance automatically. 182 | 183 | After ComputeNest service instance is deployed, check "How to use" about how to login to ECS instance. 184 | 185 | ![How to use](doc/img/computenest_how_to_use.png) 186 | 187 | Run command in ECS workbench to get the QR code. 188 | ![QR code](doc/img/computenest_qr_code.png) 189 | 190 | Scan it as if you are login to your desktop WeChat, and click "Log in" on your mobile WeChat. 191 | 192 | Finally, everything is good to go! You will see the logs when people sending you messagem, and whenever the chatbot is auto-triggered to reply. 193 | 194 | 195 | ## 2. Any Fancy Advanced Settings? 196 | 197 | ### 2.1 Config Reply in Error 198 | 199 | When the OpenAI API encounters some errors (e.g. over-crowded traffic, no authorization, ...), the chatbot will auto-reply the pre-configured message. 200 | 201 | You can change it in `src/chatgpt.js`: 202 | 203 | ```typescript 204 | const chatgptErrorMessage = "🤖️:ChatGPT摆烂了,请稍后再试~"; 205 | ``` 206 | 207 | --- 208 | 209 | ### 2.2 Config `OpenAI` Models 210 | 211 | You can change whatever `OpenAI` Models you like to handle task at different capability, time-consumption and expense trade-off. (e.g. model with better capability costs more time to respond) 212 | 213 | **Currently, the latest `GPT-4o` model is up and running!** 214 | 215 | ~~Since the latest `gpt-4` model is currently in a limited beta and only accessible to those who have been granted access, currently we use the `gpt-3.5-turbo` model as default. Of course, if you have the access to `gpt-4` API, you can just change the model to `gpt-4` without any other modification.~~ 216 | 217 | According to OpenAI doc, 218 | 219 | > GPT-4o (“o” for “omni”) is our most advanced model. It is multimodal (accepting text or image inputs and outputting text), and it has the same high intelligence as GPT-4 Turbo but is much more efficient—it generates text 2x faster and is 50% cheaper. Additionally, GPT-4o has the best vision and performance across non-English languages of any of our models. 220 | 221 | > ~~GPT-3.5 models can understand and generate natural language or code. Our most capable and cost effective model in the GPT-3.5 family is `gpt-3.5-turbo` which has been optimized for chat but works well for traditional completions tasks as well.~~ 222 | 223 | Also, for the same model, we can configure dozens of parameter (e.g. answer randomness, maximum word limit...). For example, for the `temperature` field: 224 | 225 | > Higher values like **0.8** will make the output more random, while lower values like **0.2** will make it more focused and deterministic. 226 | 227 | You can configure all of them in `src/chatgpt.js`: 228 | 229 | ```typescript 230 | chatgptModelConfig: object = { 231 | // this model field is required 232 | model: "gpt-4o", 233 | // add your ChatGPT model parameters below 234 | temperature: 0.8, 235 | // max_tokens: 2000, 236 | }; 237 | ``` 238 | 239 | For more details, please refer to [OpenAI Models Doc](https://beta.openai.com/docs/models/overview). 240 | 241 | --- 242 | 243 | ### 2.3 Config Model Features 244 | 245 | You can change whatever features you like to handle different types of tasks. (e.g. complete text, edit text, generate code...) 246 | 247 | Currently, we use `createChatCompletion()` powered by `gpt-4o` model, which: 248 | 249 | > take a series of messages as input, and return a model-generated message as output. 250 | 251 | You can configure in `src/chatgpt.js`: 252 | 253 | ```typescript 254 | const response = await this.openaiApiInstance.createChatCompletion({ 255 | ...this.chatgptModelConfig, 256 | messages: inputMessages, 257 | }); 258 | ``` 259 | 260 | For more details, please refer to [OpenAI API Doc](https://beta.openai.com/docs/api-reference/introduction). 261 | 262 | --- 263 | 264 | ### 2.4 Add Customized Task Handler 265 | 266 | You can add your own task handlers to expand the ability of this chatbot! 267 | 268 | In `src/chatgpt.ts` `ChatGPTBot.onCustimzedTask()`, write your own task handler: 269 | 270 | ```typescript 271 | // e.g. if a message starts with "Hello", the bot sends "World!" 272 | if (message.text().startsWith("Hello")) { 273 | await message.say("World!"); 274 | return; 275 | } 276 | ``` 277 | 278 | ## 3. Common Errors and Troubleshooting 279 | 280 | ### 3.1 Assertion Error during Login or Self-chat 🤯 281 | 282 | - Error Log: 283 | 284 | ```log 285 | uncaughtException AssertionError [ERR_ASSERTION]: 1 == 0 286 | at Object.equal (/app/node_modules/wechat4u/src/util/global.js:53:14) 287 | at /app/node_modules/wechat4u/src/core.js:195:16 288 | at processTicksAndRejections (node:internal/process/task_queues:96:5) { 289 | code: 2, 290 | details: 'AssertionError [ERR_ASSERTION]: 1 == 0\n' + 291 | ' at Object.equal (/app/node_modules/wechat4u/src/util/global.js:53:14)\n' + 292 | ' at /app/node_modules/wechat4u/src/core.js:195:16\n' + 293 | ' at processTicksAndRejections (node:internal/process/task_queues:96:5)' 294 | } 295 | ``` 296 | 297 | - Solution: 298 | - If see this error during login, please check [issue #8](https://github.com/kx-Huang/ChatGPT-on-WeChat/issues/8) 299 | - If see this error during self-chat, please check [issue #38](https://github.com/kx-Huang/ChatGPT-on-WeChat/issues/38) 300 | 301 | ### 3.2 I can't trigger auto reply 🤔 302 | - Solution: 303 | - Before deployment, read the trigger conditions in [1.1.2 Configure Environment Variables](#112-configure-environment-variables) 304 | - After deployment, check the console logs for following lines: 305 | - 🎯 Trigger keyword in private chat is: `` 306 | - 🎯 Trigger keyword in group chat is: `@Name ` 307 | 308 | ## 4. How to Contribute to this Project? 309 | 310 | You are more than welcome to raise some issues, fork this repo, commit your code and submit pull request. And after code review, we can merge your contribution. I'm really looking forward to develop more interesting features! 311 | 312 | Also, there're something in the to-do list for future enhancement: 313 | 314 | 1. Chat with context (integrate with [`LangChain`](https://github.com/langchain-ai/langchain)): 315 | - Keep track of every on-going conversation for each private chat or group chat 316 | - Dynamic drop or summarize the history conversation sent throught API in case the token gets oversized 317 | - Set time-out for a conversation when users stop chatting for a while 318 | 2. More AI capability: 319 | - Integrate OpenAI `DALL·E` model for AI image creation. Triggered by customized keyword (e.g. Hi bot, draw...) 320 | - Integrate OpenAi `Whisper` model for speech recognition. Triggered by voice messages and do transcription or translation 321 | 3. More flexible depolyment: 322 | - Make deployment templates on other cloud platforms 323 | - Optimize depolyment process to be more robust and compatible on different OS 324 | 325 | ## 5. Acknowledgement 326 | 327 | Great thanks to: 328 | 329 | - [@leoncsyang](https://github.com/leoncsyang) for fixing [Issue #81](https://github.com/kx-Huang/ChatGPT-on-WeChat/issues/81) 330 | - [@hdfk7](https://github.com/hdfk7) for fixing [Issue #67](https://github.com/kx-Huang/ChatGPT-on-WeChat/issues/67) 331 | - [@jichangfeng](https://github.com/jichangfeng) for merging [PR #61](https://github.com/kx-Huang/ChatGPT-on-WeChat/pull/61) 332 | - [@wenle](https://github.com/wenle) for merging [PR #92](https://github.com/kx-Huang/ChatGPT-on-WeChat/pull/92), [PR #93](https://github.com/kx-Huang/ChatGPT-on-WeChat/pull/93) 333 | 334 | ## Thanks for your support by starring this project! 335 | 336 |

337 | 338 | Stargazers repo roster for @kx-Huang/ChatGPT-on-WeChat 339 | 340 | 341 | Star history chart for @kx-Huang/ChatGPT-on-WeChat 342 | 343 |

344 | -------------------------------------------------------------------------------- /doc/img/deploy_to_computenest.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 编组 52备份 4 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------