diff --git a/.gitignore b/.gitignore index 408f35a..9459d1b 100644 --- a/.gitignore +++ b/.gitignore @@ -322,3 +322,4 @@ config.yaml .vscode data/ +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 97666c6..3df223f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,55 +1,60 @@ FROM node:18.16.0 -WORKDIR /app +RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list && \ + sed -i 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list + +RUN apt-get update \ + && apt-get install -y ca-certificates \ + && apt-get install -y fonts-liberation \ + libappindicator3-1 \ + libasound2 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libc6 \ + libcairo2 \ + libcups2 \ + libdbus-1-3 \ + libexpat1 \ + libfontconfig1 \ + libgbm-dev \ + libgcc1 \ + libgconf-2-4 \ + libgdk-pixbuf2.0-0 \ + libglib2.0-0 \ + libgtk-3-0 \ + libnspr4 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libstdc++6 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcomposite1 \ + libxcursor1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxi6 \ + libxrandr2 \ + libxrender1 \ + libxss1 \ + libxtst6 \ + lsb-release \ + wget \ + xdg-utils \ + libnss3 \ + && rm -rf /var/lib/apt/lists/* -RUN apt-get update && apt-get install -y \ - ca-certificates \ - fonts-liberation \ - libappindicator3-1 \ - libasound2 \ - libatk-bridge2.0-0 \ - libatk1.0-0 \ - libc6 \ - libcairo2 \ - libcups2 \ - libdbus-1-3 \ - libexpat1 \ - libfontconfig1 \ - libgbm-dev \ - libgcc1 \ - libgconf-2-4 \ - libgdk-pixbuf2.0-0 \ - libglib2.0-0 \ - libgtk-3-0 \ - libnspr4 \ - libpango-1.0-0 \ - libpangocairo-1.0-0 \ - libstdc++6 \ - libx11-6 \ - libx11-xcb1 \ - libxcb1 \ - libxcomposite1 \ - libxcursor1 \ - libxdamage1 \ - libxext6 \ - libxfixes3 \ - libxi6 \ - libxrandr2 \ - libxrender1 \ - libxss1 \ - libxtst6 \ - lsb-release \ - wget \ - xdg-utils \ - libnss3 \ - && rm -rf /var/lib/apt/lists/* +WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm config set registry https://registry.npm.taobao.org && npm install COPY . . -RUN cp .env.example .env +RUN cp /app/config/.env.example /app/config/.env + +VOLUME ["/app/config"] EXPOSE 4120 diff --git a/README.md b/README.md index 3b1993b..bc4e5a8 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,33 @@ ## 注意事项 - 依赖 [midjourney-proxy](https://github.com/novicezk/midjourney-proxy) 提供的api接口 +- 仅作为midjourney-proxy项目的示例应用场景,有问题需自行解决 - 推荐使用docker启动;mac M或其他arm架构电脑,暂时使用npm启动 ## 快速启动 1. 下载镜像 ```shell -docker pull novicezk/wechat-midjourney:1.0.1 +docker pull novicezk/wechat-midjourney:2.0 ``` -2. 启动容器,并设置参数 +2. 启动容器 ```shell +# /xxx/xxx/config目录下创建.env和sensitive_words.txt +docker run -d --name wechat-midjourney \ + -p 4120:4120 \ + -v /xxx/xxx/config:/app/config \ + --restart=always \ + novicezk/wechat-midjourney:2.0 + +# 或启动时添加配置 docker run -d --name wechat-midjourney \ -p 4120:4120 \ -e MJ_PROXY_ENDPOINT=http://172.17.0.1:8080/mj \ + -e MJ_NOFIFY_HOOK=http://172.17.0.1:4120/notify \ --restart=always \ - novicezk/wechat-midjourney:1.0.1 + novicezk/wechat-midjourney:2.0 ``` + 3. 查看启动日志,微信扫描二维码,若二维码无法扫码,复制二维码链接浏览器打开扫码 ```shell docker logs -f -n 200 wechat-midjourney @@ -28,10 +39,10 @@ docker logs -f -n 200 wechat-midjourney ## npm启动 ```shell git clone git@github.com:novicezk/wechat-midjourney.git -cd wechat-midjourney.git +cd wechat-midjourney npm install # 可能执行错误,缺少library,按提示解决 -cp .env.example .env +cp config/.env.example config/.env # 更改配置项,启动服务 npm run serve ``` @@ -41,5 +52,5 @@ npm run serve | 变量名 | 非空 | 描述 | | :-----| :----: | :---- | | MJ_PROXY_ENDPOINT | 是 | midjourney代理服务的地址 | -| BLOCK_WORDS | 否 | 敏感词,英文逗号分隔 | - +| MJ_NOFIFY_HOOK | 是 | 当前服务的回调接收地址 | +| HTTP_PROXY | 否 | http代理地址 | \ No newline at end of file diff --git a/config/.env.example b/config/.env.example new file mode 100644 index 0000000..b619a5b --- /dev/null +++ b/config/.env.example @@ -0,0 +1,2 @@ +MJ_PROXY_ENDPOINT="http://localhost:8080/mj" +MJ_NOFIFY_HOOK="http://localhost:4120/notify" \ No newline at end of file diff --git a/config/sensitive_words.txt b/config/sensitive_words.txt new file mode 100644 index 0000000..4d55a90 --- /dev/null +++ b/config/sensitive_words.txt @@ -0,0 +1,5 @@ +转法轮 +自焚 +吸毒 +贩毒 +赌博 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eeedc99..13baf39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "wechat-midjourney", - "version": "1.0.1", + "version": "2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wechat-midjourney", - "version": "1.0.1", - "license": "ISC", + "version": "2.0", + "license": "Apache 2", "dependencies": { "axios": "^1.3.6", - "dayjs": "^1.11.7", "dotenv": "^16.0.3", "express": "^4.18.2", "file-box": "^1.4.15", "https-proxy-agent": "^6.1.0", + "log4js": "^6.9.1", "qrcode": "^1.5.1", "wechaty": "^1.20.2", "wechaty-puppet-wechat": "^1.18.4" @@ -1576,10 +1576,13 @@ "node": ">=0.10" } }, - "node_modules/dayjs": { - "version": "1.11.7", - "resolved": "https://registry.lawyerpass.com/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.lawyerpass.com/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "engines": { + "node": ">=4.0" + } }, "node_modules/debug": { "version": "4.3.4", @@ -2066,6 +2069,11 @@ "nop": "^1.0.0" } }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.lawyerpass.com/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -3019,6 +3027,21 @@ "resolved": "https://registry.lawyerpass.com/lodash.some/-/lodash.some-4.6.0.tgz", "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.lawyerpass.com/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.lawyerpass.com/long/-/long-4.0.0.tgz", @@ -4049,6 +4072,11 @@ "node": ">= 4" } }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.lawyerpass.com/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4361,6 +4389,48 @@ "node": ">= 0.8" } }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.lawyerpass.com/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.lawyerpass.com/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.lawyerpass.com/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.lawyerpass.com/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -6494,10 +6564,10 @@ "assert-plus": "^1.0.0" } }, - "dayjs": { - "version": "1.11.7", - "resolved": "https://registry.lawyerpass.com/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + "date-format": { + "version": "4.0.14", + "resolved": "https://registry.lawyerpass.com/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==" }, "debug": { "version": "4.3.4", @@ -6891,6 +6961,11 @@ } } }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.lawyerpass.com/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -7609,6 +7684,18 @@ "resolved": "https://registry.lawyerpass.com/lodash.some/-/lodash.some-4.6.0.tgz", "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" }, + "log4js": { + "version": "6.9.1", + "resolved": "https://registry.lawyerpass.com/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "requires": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + } + }, "long": { "version": "4.0.0", "resolved": "https://registry.lawyerpass.com/long/-/long-4.0.0.tgz", @@ -8334,6 +8421,11 @@ "resolved": "https://registry.lawyerpass.com/retry/-/retry-0.12.0.tgz", "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.lawyerpass.com/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -8557,6 +8649,41 @@ "resolved": "https://registry.lawyerpass.com/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "streamroller": { + "version": "3.1.5", + "resolved": "https://registry.lawyerpass.com/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "requires": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.lawyerpass.com/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.lawyerpass.com/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.lawyerpass.com/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/package.json b/package.json index 7278c52..db9424e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wechat-midjourney", - "version": "1.0.1", + "version": "2.0", "description": "", "main": "dist/main.js", "export": "dist/main.js", @@ -9,15 +9,15 @@ "serve": "node --loader ts-node/esm src/main.ts" }, "author": "novicezk", - "license": "ISC", + "license": "Apache 2", "dependencies": { "axios": "^1.3.6", - "dayjs": "^1.11.7", "dotenv": "^16.0.3", "express": "^4.18.2", "file-box": "^1.4.15", - "https-proxy-agent": "^6.1.0", + "log4js": "^6.9.1", "qrcode": "^1.5.1", + "https-proxy-agent": "^6.1.0", "wechaty": "^1.20.2", "wechaty-puppet-wechat": "^1.18.4" }, diff --git a/src/bot.ts b/src/bot.ts index 0c18a49..7e47768 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,15 +1,49 @@ import { Message } from "wechaty"; -import { isNonsense, isProhibited, formatDateStandard } from "./utils.js"; -import { submitTask } from "./mj-api.js"; +import { WechatyInterface, ContactInterface } from 'wechaty/impls'; +import * as PUPPET from 'wechaty-puppet'; +import QRCode from "qrcode"; +import { logger } from "./utils.js"; +import { MJApi, SubmitResult } from "./mj-api.js"; +import { Sensitive } from "./sensitive.js"; export class Bot { - botName: string = "MJBOT"; - setBotName(botName: string) { - this.botName = botName; + botName: string = "MJ-BOT"; + createTime: number; + wechaty: WechatyInterface; + mjApi: MJApi; + sensitive: Sensitive; + + constructor(wechaty: WechatyInterface, mjApi: MJApi) { + this.createTime = Date.now(); + this.wechaty = wechaty; + this.mjApi = mjApi; + this.sensitive = new Sensitive(); + } + + public async start() { + this.wechaty.on("scan", async qrcode => { + logger.info(`Scan qrcode to login: https://wechaty.js.org/qrcode/${encodeURIComponent(qrcode)}`); + console.log(await QRCode.toString(qrcode, { type: "terminal", small: true })); + }).on("login", user => { + logger.info("User %s login success", user.name()); + this.botName = user.name(); + }).on("message", async message => { + if (message.date().getTime() < this.createTime) { + return; + } + if (!message.room()) { + return; + } + try { + await this.handle(message); + } catch (e) { + logger.error("Handle message error", e); + } + }); + await this.wechaty.start(); } - async onMessage(message: Message) { - const date = message.date(); + private async handle(message: Message) { const rawText = message.text(); const talker = message.talker(); const room = message.room(); @@ -17,72 +51,82 @@ export class Bot { return; } const topic = await room.topic(); - if (isNonsense(talker, message.type(), rawText)) { + if (this.isNonsense(talker, message.type(), rawText)) { return; } if (rawText == '/help') { - const result = "欢迎使用MJ机器人\n" + - "------------------------------\n" - + "🎨 生成图片命令\n" - + "输入: /imagine prompt\n" - + "prompt 即你向mj提的绘画需求\n" - + "------------------------------\n" - + "🌈 变换图片命令\n" - + "输入: /up 3214528596600076 U1\n" - + "输入: /up 3214528596600076 V1\n" - + "3214528596600076代表任务ID,U代表放大,V代表细致变化,1代表第1张图\n" - + "------------------------------\n" - + "📕 附加参数 \n" - + "1.解释:附加参数指的是在prompt后携带的参数,可以使你的绘画更加别具一格\n" - + "· 输入 /imagine prompt --v 5 --ar 16:9\n" - + "2.使用:需要使用--key value ,key和value之间需要空格隔开,每个附加参数之间也需要空格隔开\n" - + "------------------------------\n" - + "📗 附加参数列表\n" - + "1.(--version) 或 (--v) 《版本》 参数 1,2,3,4,5 默认5,不可与niji同用\n" - + "2.(--niji)《卡通版本》 参数 空或 5 默认空,不可与版本同用\n" - + "3.(--aspect) 或 (--ar) 《横纵比》 参数 n:n ,默认1:1\n" - + "4.(--chaos) 或 (--c) 《噪点》参数 0-100 默认0\n" - + "5.(--quality) 或 (--q) 《清晰度》参数 .25 .5 1 2 分别代表,一般,清晰,高清,超高清,默认1\n" - + "6.(--style) 《风格》参数 4a,4b,4c (v4)版本可用,参数 expressive,cute (niji5)版本可用\n" - + "7.(--stylize) 或 (--s)) 《风格化》参数 1-1000 v3 625-60000\n" - + "8.(--seed) 《种子》参数 0-4294967295 可自定义一个数值配合(sameseed)使用\n" - + "9.(--sameseed) 《相同种子》参数 0-4294967295 可自定义一个数值配合(seed)使用\n" - + "10.(--tile) 《重复模式》参数 空"; + const result = this.getHelpText(); await room.say(result); return; } const talkerName = talker.name(); - console.log(`${formatDateStandard(date)} - [${topic}] ${talkerName}: ${rawText}`); + logger.info("[%s] %s: %s", topic, talkerName, rawText); if (!rawText.startsWith('/imagine ') && !rawText.startsWith('/up ')) { return; } - if (isProhibited(rawText)) { - const content = `@${talkerName} \n❌ 任务被拒绝,可能包含违禁词`; - await room.say(content); - console.log(`${formatDateStandard(date)} - [${topic}] ${this.botName}: ${content}`); + if (this.sensitive.hasSensitiveWord(rawText)) { + await room.say(`@${talkerName} \n⚠ 可能包含违禁词, 请检查`); return; } - let errorMsg; - if (rawText.startsWith('/up ')) { - const content = rawText.substring(4); - errorMsg = await submitTask({ - state: topic + ':' + talkerName, - action: "UV", - content: content - }); - } else if (rawText.startsWith('/imagine ')) { + // 调用mj绘图 + let result; + if (rawText.startsWith('/imagine ')) { const prompt = rawText.substring(9); - errorMsg = await submitTask({ + result = await this.mjApi.submitTask("/submit/imagine", { state: topic + ':' + talkerName, - action: "IMAGINE", prompt: prompt }); + } else { + const content = rawText.substring(4); + result = await this.mjApi.submitTask("/submit/simple-change", { + state: topic + ':' + talkerName, + content: content + }); + } + if (!result) { + return; + } + let msg; + if (result.code == 22) { + msg = `@${talkerName} \n⏰ ${result.description}`; + } else if (result.code != 1) { + msg = `@${talkerName} \n❌ ${result.description}`; } - if (errorMsg) { - const content = `@${talkerName} \n❌ ${errorMsg}`; - await room.say(content); - console.log(`${formatDateStandard(date)} - [${topic}] ${this.botName}: ${content}`); + if (msg) { + await room.say(msg); + logger.info("[%s] %s: %s", topic, this.botName, msg); } } + private getHelpText(): string { + return "欢迎使用MJ机器人\n" + + "------------------------------\n" + + "🎨 AI绘图命令\n" + + "输入: /imagine prompt\n" + + "prompt 即你提的绘画需求\n" + + "------------------------------\n" + + "📕 prompt附加参数 \n" + + "1.解释: 在prompt后携带的参数, 可以使你的绘画更别具一格\n" + + "2.示例: /imagine prompt --ar 16:9\n" + + "3.使用: 需要使用--key value, key和value空格隔开, 多个附加参数空格隔开\n" + + "------------------------------\n" + + "📗 附加参数列表\n" + + "1. --v 版本 1,2,3,4,5 默认5, 不可与niji同用\n" + + "2. --niji 卡通版本 空或5 默认空, 不可与v同用\n" + + "3. --ar 横纵比 n:n 默认1:1\n" + + "4. --q 清晰度 .25 .5 1 2 分别代表: 一般,清晰,高清,超高清,默认1\n" + + "5. --style 风格 (4a,4b,4c)v4可用 (expressive,cute)niji5可用\n" + + "6. --s 风格化 1-1000 (625-60000)v3"; + } + + private isNonsense(talker: ContactInterface, messageType: PUPPET.types.Message, text: string): boolean { + return messageType != PUPPET.types.Message.Text || + // talker.self() || + talker.name() === "微信团队" || + text.includes("收到一条视频/语音聊天消息,请在手机上查看") || + text.includes("收到红包,请在手机上查看") || + text.includes("收到转账,请在手机上查看") || + text.includes("/cgi-bin/mmwebwx-bin/webwxgetpubliclinkimg"); + } + } \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 2e0e738..434b3db 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,16 +1,16 @@ import * as dotenv from "dotenv"; -dotenv.config(); +dotenv.config({ path: "config/.env" }); export interface IConfig { mjProxyEndpoint: string; - blockWords: string[]; + notifyHook: string; httpProxy: string; - imagesPath: String; + imagesPath: string; } export const config: IConfig = { mjProxyEndpoint: process.env.MJ_PROXY_ENDPOINT || "http://localhost:8022/mj", - blockWords: process.env.BLOCK_WORDS?.split(",") || [], + notifyHook: process.env.MJ_NOFIFY_HOOK || "http://localhost:4120/notify", httpProxy: process.env.HTTP_PROXY || "", - imagesPath: process.env.IMAGE_PATH || "", -}; + imagesPath: process.env.IMAGE_PATH || "" +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index a5b8729..d48d1e8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,102 +1,17 @@ import { WechatyBuilder } from "wechaty"; -import QRCode from "qrcode"; +import { MJApi } from "./mj-api.js"; import { Bot } from "./bot.js"; -import { displayMilliseconds } from "./utils.js"; -import { downloadImage } from "./mj-api.js"; -import express, { Application, Request, Response } from "express"; - -const app: Application = express(); -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -const port = 4120; - -const bot = new Bot(); - -const client = WechatyBuilder.build({ - name: "wechat-assistant", +const wechaty = WechatyBuilder.build({ + name: "wechat-midjourney", puppet: "wechaty-puppet-wechat", puppetOptions: { uos: true } }); -async function main() { - const initializedAt = Date.now(); - client.on("scan", async (qrcode) => { - const url = `https://wechaty.js.org/qrcode/${encodeURIComponent(qrcode)}`; - console.log(`scan qrcode to login: ${url}`); - console.log(await QRCode.toString(qrcode, { type: "terminal", small: true })); - }).on("login", async (user) => { - console.log(`user ${user.name()} login success`); - bot.setBotName(user.name()); - }).on("message", async (message) => { - if (message.date().getTime() < initializedAt) { - return; - } - if (!message.room()) { - // 暂不处理私聊信息 - return; - } - try { - bot.onMessage(message); - } catch (e) { - console.error(`bot on message error: ${e}`); - } - }); - try { - await client.start(); - } catch (e) { - console.error(`wechat client start failed: ${e}`); - } -} -main(); - -app.post("/notify", async (req: Request, res: Response): Promise => { - try { - const state = req.body.state; - const i = state.indexOf(":"); - const roomName = state.substring(0, i); - const userName = state.substring(i + 1); - const room = await client.Room.find({ topic: roomName }); - if (!room) { - return res.status(404).send("room not found"); - } - const action = req.body.action; - const status = req.body.status; - const description = req.body.description; - if (status == 'IN_PROGRESS') { - room.say(`@${userName} \n✅ 您的任务已提交\n✨ ${description}\n🚀 正在快速处理中,请稍后`); - } else if (status == 'FAILURE') { - room.say(`@${userName} \n❌ 任务执行失败\n✨ ${description}`); - } else if (status == 'SUCCESS') { - const time = req.body.finishTime - req.body.submitTime; - if (action == 'UPSCALE') { - await room.say(`@${userName} \n🎨 图片放大,用时: ${displayMilliseconds(time)}\n✨ ${description}`); - - const image = await downloadImage(req.body.imageUrl); - room.say(image); - - } else { - const taskId = req.body.id; - const prompt = req.body.prompt; - await room.say(`@${userName} \n🎨 ${action == 'IMAGINE' ? '绘图' : '变换'}成功,用时 ${displayMilliseconds(time)}\n✨ Prompt: ${prompt}\n📨 任务ID: ${taskId}\n🪄 放大 U1~U4 ,变换 V1~V4\n✏️ 使用[/up 任务ID 操作]\n/up ${taskId} U1`); - - const image = await downloadImage(req.body.imageUrl); - room.say(image); - } - } - return res.status(200).send({ code: 1 }); - } catch (e) { - console.error(`notify callback failed: ${e}`); - return res.status(500).send({ code: -9 }); - } -}); +const mjApi = new MJApi(wechaty); +await mjApi.listenerNotify(); -try { - app.listen(port, (): void => { - console.log(`Notify server start success on port ${port}`); - }); -} catch (e) { - console.error(`Notify server start failed: ${e}`); -} +const bot = new Bot(wechaty, mjApi); +await bot.start(); \ No newline at end of file diff --git a/src/mj-api.ts b/src/mj-api.ts index 5aecc37..dae1d2e 100644 --- a/src/mj-api.ts +++ b/src/mj-api.ts @@ -1,52 +1,124 @@ -import { config } from "./config.js"; -import axios from 'axios'; +import express, { Request, Response } from "express"; +import { WechatyInterface } from 'wechaty/impls'; import { FileBox } from 'file-box'; -import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { logger, displayMilliseconds } from "./utils.js"; +import { config } from "./config.js"; import { HttpsProxyAgent } from "https-proxy-agent" +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import axios from 'axios'; import * as fs from 'fs'; -import { Request } from "./request.js"; +export class SubmitResult { + code: number; + description: string; + result: string = ""; -const request = new Request({}) + constructor(code: number, description: string) { + this.code = code; + this.description = description; + } +}; -export async function submitTask(params: any): Promise { - let url = "/trigger/submit"; - if (params.action == 'UV') { - url = "/trigger/submit-uv"; - } +export class MJApi { + listenerPort: number = 4120; + wechaty: WechatyInterface; + axiosInstance: AxiosInstance; + + constructor(wechaty: WechatyInterface) { + this.wechaty = wechaty; + this.axiosInstance = axios.create({ + baseURL: config.mjProxyEndpoint, + timeout: 60000 + }); + } + + public async listenerNotify() { + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + app.post("/notify", async (req: Request, res: Response): Promise => { + return this.handle(req, res); + }); + app.listen(this.listenerPort, (): void => { + logger.info("mj listener start success on port %d", this.listenerPort); + }); + } + + public async submitTask(url: string, params: any): Promise { + const notifyHook = config.notifyHook ? { notifyHook: config.notifyHook } : {}; try { - const response = await request.post(url, params); - if (response.status !== 200) { - console.log(`提交任务错误: ${response.status}, ${response.statusText}`); - return "MJ服务异常,请稍后再试"; - } - const message = response.data; - if (message.code != 1) { - console.log(`提交任务错误: ${message.code}, ${message.description}`); - return `提交任务失败\n - ${message.description}`; - } - return ""; + const response = await this.axiosInstance.post(url, { ...params, ...notifyHook }); + if (response.status === 200) { + return response.data; + } + logger.error("submit mj task failed, %d: %s", response.status, response.statusText); + return new SubmitResult(response.status, response.statusText); } catch (e) { - console.error(`submit task failed: ${e}`); - return "系统异常,请稍后再试"; + logger.error("submit mj error", e); + return new SubmitResult(-9, "MJ服务异常, 请稍后再试"); } -} + } -export async function downloadImage(url: string): Promise { + private async proxyDownloadImage(url: string): Promise { const response: AxiosResponse = await axios({ - method: 'GET', - url: url, - responseType: 'arraybuffer', - httpsAgent: config.httpProxy != "" ? new HttpsProxyAgent(config.httpProxy) : undefined, - timeout: 10000, + method: 'GET', + url: url, + responseType: 'arraybuffer', + httpsAgent: new HttpsProxyAgent(config.httpProxy), + timeout: 10000, }); - const filename = url.split('/')!.pop()!; - if (config.imagesPath!="") { + if (config.imagesPath != '') { fs.writeFileSync(config.imagesPath + '/' + filename, response.data, 'binary'); } const fileBuffer = Buffer.from(response.data, 'binary'); + return FileBox.fromBuffer(fileBuffer, filename); + } - return FileBox.fromBuffer(fileBuffer, filename); - + private async handle(req: Request, res: Response) { + try { + const state = req.body.state; + const i = state.indexOf(":"); + const roomName = state.substring(0, i); + const userName = state.substring(i + 1); + const room = await this.wechaty.Room.find({ topic: roomName }); + if (!room) { + return res.status(404).send("room not found"); + } + const action = req.body.action; + const status = req.body.status; + const description = req.body.description; + if (status == 'SUBMITTED') { + room.say(`@${userName} \n✅ 您的任务已提交\n✨ ${description}\n🚀 正在快速处理中,请稍后`); + } else if (status == 'FAILURE') { + room.say(`@${userName} \n❌ 任务执行失败\n✨ ${description}`); + } else if (status == 'SUCCESS') { + const time = req.body.finishTime - req.body.submitTime; + if (action == 'UPSCALE') { + await room.say(`@${userName} \n🎨 图片放大,用时: ${displayMilliseconds(time)}\n✨ ${description}`); + let image; + if (config.httpProxy) { + image = await this.proxyDownloadImage(req.body.imageUrl); + } else { + image = FileBox.fromUrl(req.body.imageUrl); + } + room.say(image); + } else { + const taskId = req.body.id; + const prompt = req.body.prompt; + await room.say(`@${userName} \n🎨 ${action == 'IMAGINE' ? '绘图' : '变换'}成功,用时 ${displayMilliseconds(time)}\n✨ Prompt: ${prompt}\n📨 任务ID: ${taskId}\n🪄 放大 U1~U4 ,变换 V1~V4\n✏️ 使用[/up 任务ID 操作]\n/up ${taskId} U1`); + let image; + if (config.httpProxy) { + image = await this.proxyDownloadImage(req.body.imageUrl); + } else { + image = FileBox.fromUrl(req.body.imageUrl); + } + } + } + return res.status(200).send({ code: 1 }); + } catch (e) { + logger.error("mj listener handle error", e); + return res.status(500).send({ code: -9 }); + } + } } diff --git a/src/request.ts b/src/request.ts deleted file mode 100644 index 9d1dbce..0000000 --- a/src/request.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { config } from "./config.js"; -import axios from 'axios'; -import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; - -export type Message = { - code: number; - description: string; - result: T; -}; - -export class Request { - instance: AxiosInstance; - baseConfig: AxiosRequestConfig = { baseURL: config.mjProxyEndpoint, timeout: 60000 }; - - constructor(config: AxiosRequestConfig) { - this.instance = axios.create(Object.assign(this.baseConfig, config)); - } - - public request(config: AxiosRequestConfig): Promise { - return this.instance.request(config); - } - - public get( - url: string, - config?: AxiosRequestConfig - ): Promise>> { - return this.instance.get(url, config); - } - - public post( - url: string, - data?: any, - config?: AxiosRequestConfig - ): Promise>> { - return this.instance.post(url, data, config); - } - - public put( - url: string, - data?: any, - config?: AxiosRequestConfig - ): Promise>> { - return this.instance.put(url, data, config); - } - - public delete( - url: string, - config?: AxiosRequestConfig - ): Promise>> { - return this.instance.delete(url, config); - } - -} diff --git a/src/sensitive.ts b/src/sensitive.ts new file mode 100644 index 0000000..8cba0a0 --- /dev/null +++ b/src/sensitive.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs'; + +export class Sensitive { + filePath: string = 'config/sensitive_words.txt'; + blockWords: string[] = []; + + constructor() { + const content = fs.readFileSync(this.filePath, 'utf8'); + this.blockWords = content.split("\n"); + } + + public hasSensitiveWord(text: string): boolean { + if (this.blockWords.length == 0) { + return false; + } + return this.blockWords.some((word) => text.includes(word)); + } +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 28f2ccc..2b21ceb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,35 +1,19 @@ -import dayjs from "dayjs"; -import { ContactInterface } from "wechaty/impls"; -import * as PUPPET from 'wechaty-puppet'; -import { config } from "./config.js"; +import log4js from "log4js"; + +log4js.configure({ + appenders: { + fileAppender: { type: "file", filename: "wechat-midjourney.log" }, + stdout: { type: "stdout", layout: { type: "pattern", pattern: "%d [%p] - %m%n" } } + }, + categories: { + default: { appenders: ["fileAppender", "stdout"], level: "info" } + } +}); + +export const logger = log4js.getLogger(); export function displayMilliseconds(millisecond: number): string { const minute = Math.floor(millisecond / 1000 / 60); const second = Math.floor((millisecond - minute * 1000 * 60) / 1000); return minute == 0 ? (second + '秒') : (minute + '分' + second + '秒'); -} - -export function formatDateStandard(date: Date): string { - return formatDate(date, "YYYY-MM-DD HH:mm:ss"); -} - -export function formatDate(date: Date, pattern: string): string { - return dayjs(date).format(pattern); -} - -export function isProhibited(text: string): boolean { - if (config.blockWords.length == 0) { - return false; - } - return config.blockWords.some((word) => text.includes(word)); -} - -export function isNonsense(talker: ContactInterface, messageType: PUPPET.types.Message, text: string): boolean { - return messageType != PUPPET.types.Message.Text || - // talker.self() || - talker.name() === "微信团队" || - text.includes("收到一条视频/语音聊天消息,请在手机上查看") || - text.includes("收到红包,请在手机上查看") || - text.includes("收到转账,请在手机上查看") || - text.includes("/cgi-bin/mmwebwx-bin/webwxgetpubliclinkimg"); } \ No newline at end of file