├── docs └── images │ ├── user-agent.png │ ├── session-token.png │ ├── cloudflare-token.png │ ├── railway-deployed.png │ ├── railway-deploying.png │ ├── railway-succeed.png │ └── railway-deployment.png ├── config.yaml.example ├── .env.example ├── Dockerfile ├── src ├── interface.ts ├── main.ts ├── config.ts ├── bot.ts └── chatgpt.ts ├── package.json ├── .github └── workflows │ └── publish-docker-hub.yml ├── README_ZH.md ├── .gitignore ├── .dockerignore ├── README.md └── tsconfig.json /docs/images/user-agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/WechatBot/HEAD/docs/images/user-agent.png -------------------------------------------------------------------------------- /docs/images/session-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/WechatBot/HEAD/docs/images/session-token.png -------------------------------------------------------------------------------- /docs/images/cloudflare-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/WechatBot/HEAD/docs/images/cloudflare-token.png -------------------------------------------------------------------------------- /docs/images/railway-deployed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/WechatBot/HEAD/docs/images/railway-deployed.png -------------------------------------------------------------------------------- /docs/images/railway-deploying.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/WechatBot/HEAD/docs/images/railway-deploying.png -------------------------------------------------------------------------------- /docs/images/railway-succeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/WechatBot/HEAD/docs/images/railway-succeed.png -------------------------------------------------------------------------------- /docs/images/railway-deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/WechatBot/HEAD/docs/images/railway-deployment.png -------------------------------------------------------------------------------- /config.yaml.example: -------------------------------------------------------------------------------- 1 | chatGPTAccountPool: 2 | - email: email 3 | password: password 4 | isGoogleLogin: false 5 | chatPrivateTiggerKeyword: "" 6 | openAIProxy: "" -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CHAT_GPT_EMAIL= 2 | CHAT_GPT_PASSWORD= 3 | CHAT_GPT_RETRY_TIMES= 4 | CHAT_PRIVATE_TRIGGER_KEYWORD= 5 | OPENAI_PROXY= 6 | NOPECHA_KEY= 7 | CAPTCHA_TOKEN= 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:19 AS app 2 | 3 | # We don't need the standalone Chromium 4 | RUN apt-get install -y wget \ 5 | && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 6 | && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list \ 7 | && apt-get update && apt-get -y install google-chrome-stable chromium xvfb\ 8 | && rm -rf /var/lib/apt/lists/* \ 9 | && echo "Chrome: " && google-chrome --version 10 | WORKDIR /app 11 | COPY package*.json ./ 12 | RUN npm install 13 | COPY . . 14 | ENV WECHATY_PUPPET_WECHAT_ENDPOINT=/usr/bin/google-chrome 15 | CMD xvfb-run --server-args="-screen 0 1280x800x24 -ac -nolisten tcp -dpi 96 +extension RANDR" npm run dev -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | import { ChatGPTAPIBrowser } from "chatgpt"; 2 | export interface AccountWithUserInfo { 3 | password: string; 4 | email: string; 5 | isGoogleLogin: boolean; 6 | } 7 | 8 | // Account will be one in the session token or email and password 9 | export type IAccount = AccountWithUserInfo; 10 | 11 | export interface IChatGPTItem { 12 | chatGpt: ChatGPTAPIBrowser; 13 | account: IAccount; 14 | } 15 | export interface IConversationItem { 16 | conversation: ChatGPTAPIBrowser; 17 | account: IAccount; 18 | conversationId?: string; 19 | messageId?: string; 20 | } 21 | 22 | export interface IConfig { 23 | chatGPTAccountPool: IAccount[]; 24 | chatGptRetryTimes: number; 25 | chatPrivateTiggerKeyword: string; 26 | openAIProxy?: string; 27 | clearanceToken: string; 28 | userAgent: string; 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-chatgpt", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/main.js", 6 | "export": "dist/main.js", 7 | "scripts": { 8 | "dev": "nodemon --exec node --watch config.yaml --loader ts-node/esm src/main.ts", 9 | "build": "tsc" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "async-retry": "^1.3.3", 15 | "chatgpt": "^3.5.1", 16 | "dotenv": "^16.0.3", 17 | "execa": "^6.1.0", 18 | "qrcode": "^1.5.1", 19 | "uuid": "^9.0.0", 20 | "wechaty": "^1.20.2", 21 | "wechaty-puppet-wechat": "^1.18.4", 22 | "yaml": "^2.1.3" 23 | }, 24 | "devDependencies": { 25 | "@types/async-retry": "^1.4.5", 26 | "@types/qrcode": "^1.5.0", 27 | "@types/uuid": "^9.0.0", 28 | "nodemon": "^2.0.20", 29 | "ts-node": "^10.9.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 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker-hub.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | # Test pub with dev tag 5 | push: 6 | branches: 7 | - ci/fix-muti-platform 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | push_to_registry: 13 | name: Push Docker image to Docker Hub 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out the repo 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v2 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v2 24 | 25 | - name: Log in to Docker Hub 26 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 27 | with: 28 | username: ${{ secrets.DOCKER_USERNAME }} 29 | password: ${{ secrets.DOCKER_PASSWORD }} 30 | 31 | - name: Extract metadata (tags, labels) for Docker 32 | id: meta 33 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 34 | with: 35 | images: holegots/wechat-chatgpt 36 | 37 | - name: Build and push Docker image 38 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 39 | with: 40 | context: . 41 | push: true 42 | tags: ${{ steps.meta.outputs.tags }} 43 | # FIXME: Chrome doesn't seem to install on ARM, so skip it for now 44 | # platforms: linux/amd64,linux/arm64 45 | labels: ${{ steps.meta.outputs.labels }} 46 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { WechatyBuilder } from "wechaty"; 2 | import QRCode from "qrcode"; 3 | import { ChatGPTBot } from "./bot.js"; 4 | const chatGPTBot = new ChatGPTBot(); 5 | 6 | const bot = WechatyBuilder.build({ 7 | name: "wechat-assistant", // generate xxxx.memory-card.json and save login data for the next login 8 | puppetOptions: { 9 | uos: true, // 开启uos协议 10 | }, 11 | puppet: "wechaty-puppet-wechat", 12 | }); 13 | // get a Wechaty instance 14 | 15 | async function main() { 16 | await chatGPTBot.startGPTBot(); 17 | bot 18 | .on("scan", async (qrcode, status) => { 19 | const url = `https://wechaty.js.org/qrcode/${encodeURIComponent(qrcode)}`; 20 | console.log(`Scan QR Code to login: ${status}\n${url}`); 21 | console.log( 22 | await QRCode.toString(qrcode, { type: "terminal", small: true }) 23 | ); 24 | }) 25 | .on("login", async (user) => { 26 | console.log(`User ${user} logged in`); 27 | chatGPTBot.setBotName(user.name()); 28 | }) 29 | .on("message", async (message) => { 30 | if (!chatGPTBot.ready) { 31 | return; 32 | } 33 | if (message.text().startsWith("/ping")) { 34 | await message.say("pong"); 35 | return; 36 | } 37 | try { 38 | console.log(`Message: ${message}`); 39 | await chatGPTBot.onMessage(message); 40 | } catch (e) { 41 | console.error(e); 42 | } 43 | }); 44 | try { 45 | await bot.start(); 46 | } catch (e) { 47 | console.error( 48 | `⚠️ Bot start failed, can you log in through wechat on the web?: ${e}` 49 | ); 50 | } 51 | } 52 | main(); 53 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | dotenv.config(); 3 | import { parse } from "yaml"; 4 | import fs from "fs"; 5 | import { IConfig, IAccount } from "./interface"; 6 | // If config file exist read config file. else read config from environment variables. 7 | let configFile: any = {}; 8 | if (fs.existsSync("./config.yaml")) { 9 | const file = fs.readFileSync("./config.yaml", "utf8"); 10 | configFile = parse(file); 11 | } else { 12 | configFile = { 13 | chatGPTAccountPool: [ 14 | { 15 | email: process.env.CHAT_GPT_EMAIL, 16 | password: process.env.CHAT_GPT_PASSWORD, 17 | }, 18 | ], 19 | chatGptRetryTimes: Number(process.env.CHAT_GPT_RETRY_TIMES), 20 | chatPrivateTiggerKeyword: process.env.CHAT_PRIVATE_TRIGGER_KEYWORD, 21 | openAIProxy: process.env.OPENAI_PROXY, 22 | clearanceToken: process.env.CF_CLEARANCE, 23 | userAgent: process.env.USER_AGENT, 24 | }; 25 | } 26 | dotenv.config(); 27 | 28 | export const config: IConfig = { 29 | chatGPTAccountPool: configFile.chatGPTAccountPool as Array, 30 | chatGptRetryTimes: configFile.chatGptRetryTimes || 3, 31 | chatPrivateTiggerKeyword: 32 | configFile.chatPrivateTiggerKeyword || 33 | // Try compatible with previous designs 34 | (configFile?.botConfig as Array>)?.reduce( 35 | (prev: string, curr: Map) => 36 | curr.get("trigger_keywords") || "", 37 | "" 38 | ) || 39 | "", 40 | // Support openai-js use this proxy 41 | openAIProxy: configFile.openAIProxy, 42 | clearanceToken: configFile.clearanceToken, 43 | userAgent: configFile.userAgent, 44 | }; 45 | -------------------------------------------------------------------------------- /src/bot.ts: -------------------------------------------------------------------------------- 1 | import { ChatGPTPool } from "./chatgpt.js"; 2 | import { config } from "./config.js"; 3 | import { ContactInterface, RoomInterface } from "wechaty/impls"; 4 | import { Message } from "wechaty"; 5 | enum MessageType { 6 | Unknown = 0, 7 | 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 | const SINGLE_MESSAGE_MAX_SIZE = 500; 27 | export class ChatGPTBot { 28 | // Record talkid with conversation id 29 | chatGPTPool = new ChatGPTPool(); 30 | chatPrivateTiggerKeyword = config.chatPrivateTiggerKeyword; 31 | botName: string = ""; 32 | ready = false; 33 | setBotName(botName: string) { 34 | this.botName = botName; 35 | } 36 | get chatGroupTiggerKeyword(): string { 37 | return `@${this.botName}`; 38 | } 39 | async startGPTBot() { 40 | console.debug(`Start GPT Bot Config is:${JSON.stringify(config)}`); 41 | await this.chatGPTPool.startPools(); 42 | console.debug(`🤖️ Start GPT Bot Success, ready to handle message!`); 43 | this.ready = true; 44 | } 45 | // TODO: Add reset conversation id and ping pong 46 | async command(): Promise {} 47 | // remove more times conversation and mention 48 | cleanMessage(rawText: string, privateChat: boolean = false): string { 49 | let text = rawText; 50 | const item = rawText.split("- - - - - - - - - - - - - - -"); 51 | if (item.length > 1) { 52 | text = item[item.length - 1]; 53 | } 54 | text = text.replace( 55 | privateChat ? this.chatPrivateTiggerKeyword : this.chatGroupTiggerKeyword, 56 | "" 57 | ); 58 | // remove more text via - - - - - - - - - - - - - - - 59 | return text; 60 | } 61 | async getGPTMessage(text: string, talkerId: string): Promise { 62 | return await this.chatGPTPool.sendMessage(text, talkerId); 63 | } 64 | // The message is segmented according to its size 65 | async trySay( 66 | talker: RoomInterface | ContactInterface, 67 | mesasge: string 68 | ): Promise { 69 | const messages: Array = []; 70 | let message = mesasge; 71 | while (message.length > SINGLE_MESSAGE_MAX_SIZE) { 72 | messages.push(message.slice(0, SINGLE_MESSAGE_MAX_SIZE)); 73 | message = message.slice(SINGLE_MESSAGE_MAX_SIZE); 74 | } 75 | messages.push(message); 76 | for (const msg of messages) { 77 | await talker.say(msg); 78 | } 79 | } 80 | // Check whether the ChatGPT processing can be triggered 81 | tiggerGPTMessage(text: string, privateChat: boolean = false): boolean { 82 | const chatPrivateTiggerKeyword = this.chatPrivateTiggerKeyword; 83 | let triggered = false; 84 | if (privateChat) { 85 | triggered = chatPrivateTiggerKeyword 86 | ? text.includes(chatPrivateTiggerKeyword) 87 | : true; 88 | } else { 89 | triggered = text.includes(this.chatGroupTiggerKeyword); 90 | } 91 | if (triggered) { 92 | console.log(`🎯 Triggered ChatGPT: ${text}`); 93 | } 94 | return triggered; 95 | } 96 | // Filter out the message that does not need to be processed 97 | isNonsense( 98 | talker: ContactInterface, 99 | messageType: MessageType, 100 | text: string 101 | ): boolean { 102 | return ( 103 | talker.self() || 104 | // TODO: add doc support 105 | messageType !== MessageType.Text || 106 | talker.name() == "微信团队" || 107 | // 语音(视频)消息 108 | text.includes("收到一条视频/语音聊天消息,请在手机上查看") || 109 | // 红包消息 110 | text.includes("收到红包,请在手机上查看") || 111 | // Transfer message 112 | text.includes("收到转账,请在手机上查看") || 113 | // 位置消息 114 | text.includes("/cgi-bin/mmwebwx-bin/webwxgetpubliclinkimg") 115 | ); 116 | } 117 | 118 | async onPrivateMessage(talker: ContactInterface, text: string) { 119 | const talkerId = talker.id; 120 | const gptMessage = await this.getGPTMessage(text, talkerId); 121 | await this.trySay(talker, gptMessage); 122 | } 123 | 124 | async onGroupMessage( 125 | talker: ContactInterface, 126 | text: string, 127 | room: RoomInterface 128 | ) { 129 | const talkerId = room.id + talker.id; 130 | const gptMessage = await this.getGPTMessage(text, talkerId); 131 | const result = `${text}\n ------\n ${gptMessage}`; 132 | await this.trySay(room, result); 133 | } 134 | async onMessage(message: Message) { 135 | const talker = message.talker(); 136 | const rawText = message.text(); 137 | const room = message.room(); 138 | const messageType = message.type(); 139 | const privateChat = !room; 140 | if (this.isNonsense(talker, messageType, rawText)) { 141 | return; 142 | } 143 | if (this.tiggerGPTMessage(rawText, privateChat)) { 144 | const text = this.cleanMessage(rawText, privateChat); 145 | if (privateChat) { 146 | return await this.onPrivateMessage(talker, text); 147 | } else { 148 | return await this.onGroupMessage(talker, text, room); 149 | } 150 | } else { 151 | return; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 |

欢迎使用 wechat-chatgpt 👋

2 |

3 | Version 4 | 5 | License: ISC 6 | 7 | 8 | Twitter: fuergaosi 9 | 10 | 11 | join discord community of github profile readme generator 12 | 13 |

14 | 15 | > 在微信上迅速接入 ChatGPT,让它成为你最好的助手! 16 | > [English](README.md) | 中文文档 17 | 18 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/BHJD6L?referralCode=FaJtD_) 19 | 20 | 如果你没有自己的服务器或者想体验快速部署,可使用 Railway 进行部署,参见 [Railway 部署](#railway-部署)。 21 | 22 | ### 2022.12.27 更新 23 | 目前, 使用 Docker 或 Railway 部署, 会出现意料之外的问题, 我们正在努力解决。 24 | 25 | ### 2022.12.20 更新 26 | 27 | 感谢 @transitive-bullshit 的工作, 使得ChatGPT API可以自动完成这项工作。 28 | 现在可以使用密码的用户名来登录, 并配置打码 [CAPTCHAs](#CAPTCHAS) 来使得整个流程全自动化. 29 | 30 | 31 | ## 🌟 功能点 32 | 33 | - [x] 通过 wechaty,将 ChatGPT 接入微信 34 | - [x] 创建 OpenAI 的账户池 35 | - [x] 支持通过代理登陆 OpenAI 36 | - [x] 加入了持续对话的功能 37 | - [x] 加入 Dockerfile 38 | - [x] 发布到 Docker.hub 39 | - [x] 通过 Railway 进行部署 40 | - [x] 实现 OpenAI 账户池的热加载 41 | - [X] 当 OpenAI 返回码为 429/503 时自动重试 42 | 43 | ## 在Linux上通过Docker使用(✅ 推荐) 44 | 45 | ```sh 46 | cp config.yaml.example config.yaml 47 | # 在当前目录创建并修改config.yaml 48 | # 在Linux或WindowsPowerShell上运行如下命令 49 | docker run -d --name wechat-chatgpt -v $(pwd)/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 50 | # 使用二维码登陆 51 | docker logs -f wechat-chatgpt 52 | ``` 53 | 54 | ## 在Windows上通过Docker使用 55 | 56 | ```sh 57 | # 在当前目录创建并修改config.yaml 58 | # 在WindowsPowerShell中运行如下命令 59 | docker run -d --name wechat-chatgpt -v $(pwd)/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 60 | # 在Windows command line (cmd)中, 您需要像这样修改上述代码的挂载目录: 61 | docker run -d --name wechat-chatgpt -v %cd%/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 62 | # 通过二维码登录 63 | docker logs -f wechat-chatgpt 64 | ``` 65 | 66 | ## 更新Docker镜像版本 67 | 68 | ```sh 69 | docker pull holegots/wechat-chatgpt:latest 70 | docker stop wechat-chatgpt 71 | docker rm wechat-chatgpt 72 | # 在Linux或WindowsPowerShell上运行如下命令 73 | docker run -d --name wechat-chatgpt -v $(pwd)/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 74 | # 在Windows command line (cmd)中, 您需要像这样修改上述代码的挂载目录: 75 | docker run -d --name wechat-chatgpt -v %cd%/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 76 | # 通过二维码登录 77 | docker logs -f wechat-chatgpt 78 | ``` 79 | 80 | ## 安装 81 | 82 | ```sh 83 | npm install 84 | ``` 85 | > 请确认安装的NodeJS版本为18.0.0以上 86 | 87 | ## 配置 88 | 89 | ### 复制配置文件 90 | 91 | 将配置文件复制一份以配置您的项目 92 | 93 | ```sh 94 | cp config.yaml.example config.yaml 95 | ``` 96 | 97 | ### 获取 OpenAI 的账户并配置到项目中 98 | 99 | > 如果你没有 OpenAI 的账号,并且您在无法访问 OpenAI 的国家或地区,你可以查看[here](https://mirror.xyz/boxchen.eth/9O9CSqyKDj4BKUIil7NC1Sa1LJM-3hsPqaeW_QjfFBc). 100 | 101 | #### 配置方法 A:使用账号密码 102 | 103 | 可以在配置文件中输入你的账号密码,格式如下 104 | 105 | ```yaml 106 | chatGPTAccountPool: 107 | - email: 108 | password: 109 | # 如果你希望只有一些关键字可以在私人聊天中触发chatgpt,你可以这样设置: 110 | chatPrivateTiggerKeyword: "" 111 | ``` 112 | 113 | ⚠️ 触发关键字必须出现在接收到的消息的第一个位置⚠️ 114 | 115 | 请确保您的终端网络可以登陆 OpenAI。如果登陆失败,请尝试使用代理或使用 SessionToken 方法配置 116 | 117 | **设置代理:** 118 | 编辑配置文件 `config.yaml` 119 | ```yaml 120 | openAIProxy: <代理地址> 121 | ``` 122 | 123 | ### CAPTCHAS 124 | > 该版本使用Puppeteer来尽可能的实现全自动化, 包括验证码 🔥 125 | > Cloudflare CAPTCHAs是默认处理的, 但如果你想自动处理 邮箱+密码 的Recaptchas, 则需要使用付费的打码平台 126 | > - [nopecha](https://nopecha.com/) - Uses AI to solve CAPTCHAS 127 | > - Faster and cheaper 128 | > - Set the `NOPECHA_KEY` env var to your nopecha API key 129 | > - [Demo video](https://user-images.githubusercontent.com/552829/208235991-de4890f2-e7ba-4b42-bf55-4fcd792d4b19.mp4) of nopecha solving the login Recaptcha (41 seconds) 130 | > - [2captcha](https://2captcha.com) - Uses real people to solve CAPTCHAS 131 | > - More well-known solution that's been around longer 132 | > - Set the `CAPTCHA_TOKEN` env var to your 2captcha API token 133 | 134 | 如果你需要实现全自动化, 则需要配置`NOPECHA_KEY`或`CAPTCHA_TOKEN`。 135 | 136 | ### 启动项目 137 | 138 | ```sh 139 | npm run dev 140 | ``` 141 | 142 | 如果您是初次登陆,那么需要扫描二维码 143 | 144 | ## 使用 Railway 部署 145 | 146 | [Railway](https://railway.app/) 是一个部署平台,您可以在其上配置基础架构,在本地使用该基础架构进行开发,然后将其部署到云端。本部分将描述如何快速使用 Railway 部署一个 wechat-chatgpt 项目。 147 | 148 | 首先,您需要注册一个 Railway 帐户,并使用 GitHub 验证登录。 149 | 150 | 然后点击下面的一键部署按钮进行部署。 151 | 152 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/BHJD6L?referralCode=FaJtD_) 153 | 154 | 完成一些验证操作后,就可以开始部署了。您将看到以下界面: 155 | 156 | ![railway-deployment](docs/images/railway-deployment.png) 157 | 158 | 您需要配置一些环境变量: 159 | 160 | - **CHAT_GPT_EMAIL** :您的 OpenAI 帐户电子邮件。 161 | 162 | - **CHAT_GPT_PASSWORD** :您的 OpenAI 帐户密码。 163 | 164 | - **CHAT_GPT_RETRY_TIMES** :当 OpenAI API 返回 429 或 503 时重试的次数。 165 | 166 | - **CHAT_PRIVATE_TRIGGER_KEYWORD** :如果您希望只有一些关键字才能在私人聊天中触发 ChatGPT,则可以设置它。 167 | 168 | 点击“部署”按钮,您的服务将立即开始部署。以下界面出现表示部署已经开始: 169 | 170 | ![railway-deploying](docs/images/railway-deploying.png) 171 | 172 | 当部署过程显示为成功后,点击查看日志,在部署日志中找到微信登录链接: 173 | 174 | ![railway-deployed](docs/images/railway-deployed.png) 175 | 176 | 点击链接,使用准备好的微信扫码登录。 177 | 178 | 成功登录并开始发送和接收消息(此过程可能需要几分钟): 179 | 180 | ![railway-success](docs/images/railway-succeed.png) 181 | 182 | 此外,在部署中,您可能会遇到以下问题: 183 | 184 | - **Error: ⚠️ No chatgpt item in pool**:此错误表示验证信息有问题。您可以从以下几个方面解决此问题:1.检查 token 或 openAI 账号和密码是否正确填写。2. 重新部署当前服务。请注意,应在铁路仪表板的 **Variables** 页面上修改上述内容。 3. 请确认是否出现了CloudFlare人机验证, 如果出现了CloudFlare的人机验证, 则可能导致 Headless 浏览器无法成功模拟登录。 185 | - **部署完成后,不会生成二维码**。尝试**刷新**页面,再次查看 Deploy Logs 面板是否生成了链接和二维码。 186 | - **生成的二维码无法扫描**。在生成的二维码上,有一个链接可以点击扫描二维码。 187 | - **消息反馈缓慢**。由于 Railway 的服务器部署在海外,消息反馈延迟会有所增加,但仍在可接受范围内。如果您对时间敏感,则可以使用自己的服务器部署。 188 | 189 | ## ✨ Contributor 190 | 191 | 192 | 193 | 194 | 195 | ## 🤝 为项目添砖加瓦 196 | 197 | 欢迎提出 Contributions, issues 与 feature requests!
随时查看 [issues page](https://github.com/fuergaosi233/wechat-chatgpt/issues). 198 | 199 | ## 感谢支持 🙏 200 | 201 | 如果这个项目对你产生了一点的帮助,请为这个项目点上一颗 ⭐️ 202 | -------------------------------------------------------------------------------- /src/chatgpt.ts: -------------------------------------------------------------------------------- 1 | import { ChatGPTAPI, ChatGPTAPIBrowser } from "chatgpt"; 2 | 3 | import { config } from "./config.js"; 4 | import AsyncRetry from "async-retry"; 5 | import { 6 | IChatGPTItem, 7 | IConversationItem, 8 | AccountWithUserInfo, 9 | IAccount, 10 | } from "./interface.js"; 11 | 12 | const ErrorCode2Message: Record = { 13 | "503": 14 | "OpenAI 服务器繁忙,请稍后再试| The OpenAI server is busy, please try again later", 15 | "429": 16 | "OpenAI 服务器限流,请稍后再试| The OpenAI server was limited, please try again later", 17 | "500": 18 | "OpenAI 服务器繁忙,请稍后再试| The OpenAI server is busy, please try again later", 19 | "403": 20 | "OpenAI 服务器拒绝访问,请稍后再试| The OpenAI server refused to access, please try again later", 21 | unknown: "未知错误,请看日志 | Error unknown, please see the log", 22 | }; 23 | const Commands = ["/reset", "/help"] as const; 24 | export class ChatGPTPool { 25 | chatGPTPools: Array | [] = []; 26 | conversationsPool: Map = new Map(); 27 | async resetAccount(account: IAccount) { 28 | // Remove all conversation information 29 | this.conversationsPool.forEach((item, key) => { 30 | if ((item.account as AccountWithUserInfo)?.email === account.email) { 31 | this.conversationsPool.delete(key); 32 | } 33 | }); 34 | // Relogin and generate a new session token 35 | const chatGPTItem = this.chatGPTPools.find( 36 | ( 37 | item: any 38 | ): item is IChatGPTItem & { 39 | account: AccountWithUserInfo; 40 | chatGpt: ChatGPTAPI; 41 | } => item.account.email === account.email 42 | ); 43 | if (chatGPTItem) { 44 | const account = chatGPTItem.account; 45 | try { 46 | chatGPTItem.chatGpt = new ChatGPTAPIBrowser({ 47 | ...account, 48 | proxyServer: config.openAIProxy, 49 | }); 50 | } catch (err) { 51 | //remove this object 52 | this.chatGPTPools = this.chatGPTPools.filter( 53 | (item) => 54 | (item.account as AccountWithUserInfo)?.email !== account.email 55 | ); 56 | console.error( 57 | `Try reset account: ${account.email} failed: ${err}, remove it from pool` 58 | ); 59 | } 60 | } 61 | } 62 | resetConversation(talkid: string) { 63 | this.conversationsPool.delete(talkid); 64 | } 65 | async startPools() { 66 | const chatGPTPools = []; 67 | for (const account of config.chatGPTAccountPool) { 68 | const chatGpt = new ChatGPTAPIBrowser({ 69 | ...account, 70 | proxyServer: config.openAIProxy, 71 | }); 72 | try { 73 | await AsyncRetry( 74 | async () => { 75 | await chatGpt.initSession(); 76 | }, 77 | { retries: 3 } 78 | ); 79 | chatGPTPools.push({ 80 | chatGpt: chatGpt, 81 | account: account, 82 | }); 83 | } catch { 84 | console.error( 85 | `Try init account: ${account.email} failed, remove it from pool` 86 | ); 87 | } 88 | } 89 | // this.chatGPTPools = await Promise.all( 90 | // config.chatGPTAccountPool.map(async (account) => { 91 | // const chatGpt = new ChatGPTAPIBrowser({ 92 | // ...account, 93 | // proxyServer: config.openAIProxy, 94 | // }); 95 | // await chatGpt.initSession(); 96 | // return { 97 | // chatGpt: chatGpt, 98 | // account: account, 99 | // }; 100 | // }) 101 | // ); 102 | this.chatGPTPools = chatGPTPools; 103 | if (this.chatGPTPools.length === 0) { 104 | throw new Error("⚠️ No chatgpt account in pool"); 105 | } 106 | console.log(`ChatGPTPools: ${this.chatGPTPools.length}`); 107 | } 108 | async command(cmd: typeof Commands[number], talkid: string): Promise { 109 | console.log(`command: ${cmd} talkid: ${talkid}`); 110 | if (cmd == "/reset") { 111 | this.resetConversation(talkid); 112 | return "♻️ 已重置对话 | Conversation reset"; 113 | } 114 | if (cmd == "/help") { 115 | return `🧾 支持的命令|Support command:${Commands.join(",")}`; 116 | } 117 | return "❓ 未知命令|Unknow Command"; 118 | } 119 | // Randome get chatgpt item form pool 120 | get chatGPTAPI(): IChatGPTItem { 121 | return this.chatGPTPools[ 122 | Math.floor(Math.random() * this.chatGPTPools.length) 123 | ]; 124 | } 125 | // Randome get conversation item form pool 126 | getConversation(talkid: string): IConversationItem { 127 | if (this.conversationsPool.has(talkid)) { 128 | return this.conversationsPool.get(talkid) as IConversationItem; 129 | } 130 | const chatGPT = this.chatGPTAPI; 131 | if (!chatGPT) { 132 | throw new Error("⚠️ No chatgpt item in pool"); 133 | } 134 | //TODO: Add conversation implementation 135 | const conversation = chatGPT.chatGpt; 136 | const conversationItem = { 137 | conversation, 138 | account: chatGPT.account, 139 | }; 140 | this.conversationsPool.set(talkid, conversationItem); 141 | return conversationItem; 142 | } 143 | setConversation(talkid: string, conversationId: string, messageId: string) { 144 | const conversationItem = this.getConversation(talkid); 145 | this.conversationsPool.set(talkid, { 146 | ...conversationItem, 147 | conversationId, 148 | messageId, 149 | }); 150 | } 151 | // send message with talkid 152 | async sendMessage(message: string, talkid: string): Promise { 153 | if ( 154 | Commands.some((cmd) => { 155 | return message.startsWith(cmd); 156 | }) 157 | ) { 158 | return this.command(message as typeof Commands[number], talkid); 159 | } 160 | const conversationItem = this.getConversation(talkid); 161 | const { conversation, account, conversationId, messageId } = 162 | conversationItem; 163 | try { 164 | // TODO: Add Retry logic 165 | const { 166 | response, 167 | conversationId: newConversationId, 168 | messageId: newMessageId, 169 | } = await conversation.sendMessage(message, { 170 | conversationId, 171 | parentMessageId: messageId, 172 | }); 173 | // Update conversation information 174 | this.setConversation(talkid, newConversationId, newMessageId); 175 | return response; 176 | } catch (err: any) { 177 | if (err.message.includes("ChatGPT failed to refresh auth token")) { 178 | // If refresh token failed, we will remove the conversation from pool 179 | await this.resetAccount(account); 180 | console.log(`Refresh token failed, account ${JSON.stringify(account)}`); 181 | return this.sendMessage(message, talkid); 182 | } 183 | console.error( 184 | `err is ${err.message}, account ${JSON.stringify(account)}` 185 | ); 186 | // If send message failed, we will remove the conversation from pool 187 | this.conversationsPool.delete(talkid); 188 | // Retry 189 | return this.error2msg(err); 190 | } 191 | } 192 | // Make error code to more human readable message. 193 | error2msg(err: Error): string { 194 | for (const code in ErrorCode2Message) { 195 | if (err.message.includes(code)) { 196 | return ErrorCode2Message[code]; 197 | } 198 | } 199 | return ErrorCode2Message.unknown; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to wechat-chatgpt 👋

2 |

3 | Version 4 | 5 | License: ISC 6 | 7 | 8 | Twitter: fuergaosi 9 | 10 | 11 | 12 | join discord community of github profile readme generator 13 | 14 |

15 | 16 | > Use ChatGPT On Wechat via wechaty 17 | English | [中文文档](README_ZH.md) 18 | 19 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/BHJD6L?referralCode=FaJtD_) 20 | 21 | If you don't have a server or want to experience rapid deployment, you can use Railway to do so, see [Usage with Railway](#usage-with-railway). 22 | 23 | ### Update Decomber 27, 2022 24 | Using railway & docker deployment, there may be problems that cannot be solved, we are working on it. 25 | 26 | ### Update December 20, 2022 27 | 28 | Thanks @transitive-bullshit, The ChatGPT API automates the work. 29 | You should use password & username to login, and config [CAPTCHAs](#CAPTCHAS). 30 | ⚠️ There may be a problem with the Docker image because I don't have an X86 device and Qume doesn't work. 31 | 32 | ## 🌟 Feature 33 | 34 | - [x] Use ChatGPT On Wechat via wechaty 35 | - [x] Support OpenAI Accounts Pool 36 | - [x] Support use proxy to login 37 | - [x] Add conversation Support 38 | - [x] Add Dockerfile 39 | - [x] Publish to Docker.hub 40 | - [x] Add Railway deploy 41 | - [x] Auto Reload OpenAI Accounts Pool 42 | - [X] Add sendmessage retry for 429/503 43 | 44 | ## Use with docker in Linux(recommended) 45 | 46 | ```sh 47 | cp config.yaml.example config.yaml 48 | # Change Config.yaml 49 | # run docker command in Linux or WindowsPowerShell 50 | docker run -d --name wechat-chatgpt -v $(pwd)/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 51 | # login with qrcode 52 | docker logs -f wechat-chatgpt 53 | ``` 54 | 55 | ## Use with docker in Windows 56 | 57 | ```sh 58 | # Create and modify config.yaml in the current directory 59 | # run docker command in WindowsPowerShell 60 | docker run -d --name wechat-chatgpt -v $(pwd)/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 61 | # In the Windows command line (cmd) environment, you may mount the current directory like this: 62 | docker run -d --name wechat-chatgpt -v %cd%/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 63 | # login with qrcode 64 | docker logs -f wechat-chatgpt 65 | ``` 66 | 67 | ## Upgrade docker image version 68 | 69 | ```sh 70 | docker pull holegots/wechat-chatgpt:latest 71 | docker stop wechat-chatgpt 72 | docker rm wechat-chatgpt 73 | # run docker command in Linux or WindowsPowerShell 74 | docker run -d --name wechat-chatgpt -v $(pwd)/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 75 | # In the Windows command line (cmd) environment, you may mount the current directory like this: 76 | docker run -d --name wechat-chatgpt -v %cd%/config.yaml:/app/config.yaml holegots/wechat-chatgpt:latest 77 | # login with qrcode 78 | docker logs -f wechat-chatgpt 79 | ``` 80 | 81 | ## Install 82 | 83 | ```sh 84 | npm install 85 | ``` 86 | > NodeJS Version >= 18.0.0 87 | 88 | ## Config 89 | 90 | ### Copy config 91 | 92 | You need copy config file for setting up your project. 93 | 94 | ```sh 95 | cp config.yaml.example config.yaml 96 | ``` 97 | 98 | ### Get and config Openai account 99 | 100 | > If you don't have this OpenAI account and you live in China, you can get it [here](https://mirror.xyz/boxchen.eth/9O9CSqyKDj4BKUIil7NC1Sa1LJM-3hsPqaeW_QjfFBc). 101 | 102 | #### Use account and password 103 | 104 | You need get OpenAI account and password. 105 | Your config.yaml should be like this: 106 | 107 | ```yaml 108 | chatGPTAccountPool: 109 | - email: 110 | password: 111 | # if you hope only some keywords can trigger chatgpt on private chat, you can set it like this: 112 | chatPrivateTiggerKeyword: "" 113 | ``` 114 | 115 | ⚠️ Trigger keywords must appear in the first position of the received message. 116 | ⚠️ Pls make sure your network can log in to OpenAI, and if you fail to login in try setting up a proxy or using SessionToken. 117 | 118 | **Setup proxy:** 119 | 120 | You can configure in `config.yaml`: 121 | 122 | ```yaml 123 | openAIProxy: 124 | ``` 125 | 126 | ### CAPTCHAS 127 | 128 | > The browser portions of this package use Puppeteer to automate as much as possible, including solving all CAPTCHAs. 🔥 129 | 130 | > Basic Cloudflare CAPTCHAs are handled by default, but if you want to automate the email + password Recaptchas, you'll need to sign up for one of these paid providers: 131 | 132 | > - [nopecha](https://nopecha.com/) - Uses AI to solve CAPTCHAS 133 | > - Faster and cheaper 134 | > - Set the `NOPECHA_KEY` env var to your nopecha API key 135 | > - [Demo video](https://user-images.githubusercontent.com/552829/208235991-de4890f2-e7ba-4b42-bf55-4fcd792d4b19.mp4) of nopecha solving the login Recaptcha (41 seconds) 136 | > - [2captcha](https://2captcha.com) - Uses real people to solve CAPTCHAS 137 | > - More well-known solution that's been around longer 138 | > - Set the `CAPTCHA_TOKEN` env var to your 2captcha API token 139 | 140 | So you should config `NOPECHA_KEY` or `CAPTCHA_TOKEN` in your Environment Variables. 141 | 142 | ## Start Project 143 | 144 | ```sh 145 | npm run dev 146 | ``` 147 | ## Usage with Railway 148 | 149 | [Railway](https://railway.app/) is a deployment platform where you can provision infrastructure, develop with that infrastructure locally, and then deploy to the cloud.This section describes how to quickly deploy a wechat-chatgpt project using Railway. 150 | 151 | Firstly, you'll need to sign up for a Railway account and sign in using GitHub verification. 152 | 153 | Then click the one-click deployment button below to deploy. 154 | 155 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/BHJD6L?referralCode=FaJtD_) 156 | 157 | After some validation is complete, you can begin the deployment.You will see the following interface: 158 | 159 | ![railway-deployment](docs/images/railway-deployment.png) 160 | 161 | Some environment variables need to be configured: 162 | 163 | - **CHAT_GPT_EMAIL** : Your OpenAI Account email. 164 | 165 | - **CHAT_GPT_PASSWORD** : Your OpenAI Account password. 166 | 167 | - **CHAT_GPT_RETRY_TIMES** : The number of times to retry when the OpenAI API returns 429 or 503. 168 | 169 | - **CHAT_PRIVATE_TRIGGER_KEYWORD** : If you hope only some keywords can trigger chatgpt on private chat, you can set it. 170 | 171 | Click the Deploy button and your service will start deploying shortly.The following interface appears to indicate that the deployment has begun: 172 | 173 | ![railway-deploying](docs/images/railway-deploying.png) 174 | 175 | When the deployment is displayed successfully, click to view the logs and find the WeChat login link in Deploy Logs. 176 | 177 | ![railway-deployed](docs/images/railway-deployed.png) 178 | 179 | Click to enter and use your prepared WeChat to scan the code to log in. 180 | 181 | Log in successfully and start sending and receiving messages(This process can take several minutes): 182 | 183 | ![railway-success](docs/images/railway-succeed.png) 184 | 185 | Besides, in deployment, you may encounter the following issues: 186 | 187 | - **Error: ⚠️ No chatgpt item in pool** : This error means that you have not configured the OpenAI account information correctly. You can solve this problem from the following aspects:1. Check whether the token or openAI account and password are filled in correctly. 2. Redeploy Current Services.Note that the above should be modified on the Variables page in Railway Dashboard. 3. Please make sure that CloudFlare human authentication is present, if it is, Headless browser may not be able to simulate logging into OpenAI. 188 | - **After the deployment is complete, the QR code is not generated**.Try **refreshing** the page to see again if the Deploy Logs panel generated a link and QR code. 189 | - **The generated QR code cannot be scanned**.On the generated QR code, there is a link that can be clicked to scan the QR code. 190 | - **Message feedback is very slow**.Because Railway's servers are deployed overseas, there is an increase in message feedback latency, but it is still within the acceptance range. If you are time sensitive, you can use your own server deployment. 191 | 192 | If you are logging in for the first time, then you need to scan the qrcode. 193 | 194 | ## ✨ Contributor 195 | 196 | 197 | 198 | 199 | 200 | 201 | ## 🤝 Contributing 202 | 203 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/fuergaosi233/wechat-chatgpt/issues). 204 | 205 | ## Show your support 206 | 207 | Give a ⭐️ if this project helped you! 208 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------