├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .prettierrc.yaml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── README.md ├── build ├── entitlements.mac.plist ├── icon.png └── notarize.js ├── dev-app-update.yml ├── electron-builder.yml ├── electron.vite.config.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── resources ├── icon.png ├── macTray@2x.png └── windowTray.png ├── src ├── main │ ├── index.ts │ ├── ipcMain │ │ ├── axios │ │ │ ├── axios.ts │ │ │ └── index.ts │ │ ├── composables │ │ │ ├── useBlackWord.ts │ │ │ ├── useFriendPlan.ts │ │ │ ├── useGroupMsg.ts │ │ │ ├── useGroupReply.ts │ │ │ ├── useMsgShare.ts │ │ │ ├── usePeopleReply.ts │ │ │ ├── usePermission.ts │ │ │ └── useStorage.ts │ │ ├── index.ts │ │ └── wechatyBot │ │ │ ├── index.ts │ │ │ └── plugins │ │ │ ├── blackWord.ts │ │ │ ├── friendPlan.ts │ │ │ ├── groupMsg.ts │ │ │ ├── groupReply.ts │ │ │ ├── msgShare.ts │ │ │ └── peopleReply.ts │ └── tray.ts ├── preload │ ├── index.d.ts │ └── index.ts └── renderer │ ├── index.html │ ├── src │ ├── App.vue │ ├── assets │ │ ├── global.scss │ │ └── logo.jpg │ ├── components │ │ └── Navbar.vue │ ├── composables │ │ ├── useAuth.ts │ │ ├── useBlackWord.ts │ │ ├── useFriendPlan.ts │ │ ├── useGroupReply.ts │ │ ├── useGroupShare.ts │ │ ├── useMsgShare.ts │ │ ├── usePeopleReply.ts │ │ ├── usePermission.ts │ │ └── useStorage.ts │ ├── config.ts │ ├── enum │ │ ├── CacheEnum.ts │ │ └── HttpCodeEnum.ts │ ├── env.d.ts │ ├── layouts │ │ └── admin │ │ │ ├── index.vue │ │ │ └── leftMenu.vue │ ├── main.ts │ ├── middleware │ │ └── appMiddleware.ts │ ├── plugins │ │ ├── axios │ │ │ ├── axios.ts │ │ │ └── index.ts │ │ ├── dayjs │ │ │ └── index.ts │ │ ├── elementui │ │ │ └── index.ts │ │ ├── iconpark │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── pinia │ │ │ └── index.ts │ │ ├── router │ │ │ ├── guard.ts │ │ │ └── index.ts │ │ └── tailwindcss │ │ │ ├── index.ts │ │ │ └── tailwindcss.css │ ├── routes │ │ ├── admin.ts │ │ └── index.ts │ ├── store │ │ ├── useConfig.ts │ │ └── useUserStore.ts │ ├── utils │ │ ├── index.ts │ │ └── listenMain │ │ │ └── user.ts │ └── views │ │ └── admin │ │ ├── blackWord.vue │ │ ├── friend.vue │ │ ├── groupMsg.vue │ │ ├── groupReply.vue │ │ ├── home.vue │ │ ├── msgLogs.vue │ │ ├── msgShare.vue │ │ └── peopleReply.vue │ └── types │ ├── api.d.ts │ └── model.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── tsconfig.web.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:vue/vue3-recommended', 8 | '@electron-toolkit', 9 | '@electron-toolkit/eslint-config-ts/eslint-recommended', 10 | '@vue/eslint-config-typescript/recommended', 11 | '@vue/eslint-config-prettier' 12 | ], 13 | rules: { 14 | 'vue/require-default-prop': 'off', 15 | 'vue/multi-word-component-names': 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .DS_Store 5 | *.log* 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://npmmirror.com/mirrors/electron/ 2 | electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": true, 4 | "bracketSpacing": true, 5 | "embeddedLanguageFormatting": "auto", 6 | "htmlWhitespaceSensitivity": "css", 7 | "insertPragma": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 120, 10 | "proseWrap": "never", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": false, 14 | "singleQuote": true, 15 | "tabWidth": 2, 16 | "trailingComma": "all", 17 | "useTabs": false, 18 | "vueIndentScriptAndStyle": false, 19 | "singleAttributePerLine": false 20 | } 21 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | printWidth: 100 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" 12 | }, 13 | "runtimeArgs": ["--sourcemap"], 14 | "env": { 15 | "REMOTE_DEBUGGING_PORT": "9222" 16 | } 17 | }, 18 | { 19 | "name": "Debug Renderer Process", 20 | "port": 9222, 21 | "request": "attach", 22 | "type": "chrome", 23 | "webRoot": "${workspaceFolder}/src/renderer", 24 | "timeout": 60000, 25 | "presentation": { 26 | "hidden": true 27 | } 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Debug All", 33 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 34 | "presentation": { 35 | "order": 1 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bh-wechaty-help 2 | 3 | ## 项目介绍 4 | 5 | 北恒微信助手,解放您的双手,多种方案可同时运行,亲测稳定运行24小时不掉线。 6 | 7 | - [x] 好友申请 8 | - [x] 个人回复 9 | - [x] 群聊回复 10 | - [x] 消息转发 11 | - [x] 屏蔽检测 12 | - [x] 群发消息 13 | - [x] 日志列表 14 | 15 | 此项目基于 **Wechaty** 以及 **Electron** 进行编写: 16 | 17 | 18 | ## 使用说明 19 | 20 | > 直接体验 21 | 22 | 点击[立即下载体验](https://github.com/Abeiheng/bh-wechaty-help/releases),下载程序,即可直接使用,如有不懂的地方可以提交issues,或者联系绿泡泡:bhsr922,备注来意。 23 | 24 | > 自行部署 25 | 26 | 本github仅提供了electron源码,如需自行部署联系绿泡泡:bhsr922,发你后端接口源码,仅供娱乐。 27 | 28 | 29 | 30 | ## 功能说明 31 | 32 | > 好友申请 33 | 34 | 可自定义用户申请的备注内容,当用户验证备注与你设置的一样时,自动通过好友申请,并且可以发送一段打招呼语,多种方案可以同时进行,如果方案一样,排序高者优先。 35 | 36 | > 个人回复 37 | 38 | 可自定义私聊关键词,当关键词与您设定的一样时,自动触发回复,多种方案可以同时进行,如果方案一样,排序高者优先。 39 | 40 | > 群聊回复 41 | 42 | 可自定义群聊发送内容的关键词,当群里内容有与您关键词设定的一样时,自动触发回复,多种方案可以同时进行,如果方案一样,排序高者优先。(禁止与消息转发群聊套娃使用,否则会触发死循环导致账号被封禁,正常使用无任何风险。) 43 | 44 | > 消息转发 45 | 46 | 可自定义主群聊与副群聊,当主群聊有消息会自动转发到副群聊里,多种方案可以同时进行,如果方案一样,排序高者优先。(禁止与群聊回复套娃使用,否则会触发死循环导致账号被封禁,正常使用无任何风险。) 47 | 48 | > 屏蔽检测 49 | 50 | 可自定义群聊屏蔽条件,当有群聊内容触发,会自动记录在日志列表了,如果方案一样,排序高者优先。目前支持功能比较少,大家如果有更好的想法可以给我说。 51 | 52 | > 群发消息 53 | 54 | 可自定义群聊与发送内容,每次最多定义3条,设置完成之后点击方案列表绿色的小图标进行启动。 55 | 56 | 57 | ## star 趋势图 58 | 59 | ![Stargazers over time](https://starchart.cc/Abeiheng/bh-wechaty-help.svg) 60 | 61 | 62 | ## 贡献代码 63 | 64 | 若您有好的想法,发现一些 **BUG** 并修复了,欢迎提交 **Pull Request** 参与开源贡献 65 | 发起 pull request 请求,提交到 master 分支,等待作者合并 66 | 67 | 68 | ## 声明 69 | 70 | - 本项目涉及的数据由使用的个人或组织自行填写,本项目不对数据内容负责,包括但不限于数据的真实性、准确性、合法性。使用本项目所造成的一切后果,与本项目的所有贡献者无关,由使用的个人或组织完全承担。 71 | - 本项目中涉及的第三方硬件、软件等,与本项目没有任何直接或间接的关系。本项目仅对部署和使用过程进行客观描述,不代表支持使用任何第三方硬件、软件。使用任何第三方硬件、软件,所造成的一切后果由使用的个人或组织承担,与本项目无关。 72 | - 本项目中所有内容只供学习和研究使用,不得将本项目中任何内容用于违反国家/地区/组织等的法律法规或相关规定的其他用途。 73 | - 所有基于本项目源代码,进行的任何修改,为其他个人或组织的自发行为,与本项目没有任何直接或间接的关系,所造成的一切后果亦与本项目无关。 74 | - 所有直接或间接使用本项目的个人和组织,应24小时内完成学习和研究,并及时删除本项目中的所有内容。如对本项目的功能有需求,应自行开发相关功能。 75 | - 本项目保留随时对免责声明进行补充或更改的权利,直接或间接使用本项目内容的个人或组织,视为接受本项目的特别声明。 76 | 77 | 78 | ## 演示图 79 | 80 | | 北恒微信助手 | | 81 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 82 | | ![3141723947182 .pic](https://img.picgo.net/2024/08/18/3141723947182_.picb4c9d219115bc945.jpg) | ![3131723947165 .pic](https://img.picgo.net/2024/08/18/3131723947165_.pic9ed23653304662c8.jpg) | 83 | | | | 84 | | ![3121723947154 .pic](https://img.picgo.net/2024/08/18/3121723947154_.pic2f9e272bd06f36d0.jpg) | ![3111723947148 .pic](https://img.picgo.net/2024/08/18/3111723947148_.pica4598c5024a97b39.jpg) | 85 | | | | 86 | | ![3101723947141 .pic](https://img.picgo.net/2024/08/18/3101723947141_.pic0ba23e40e0db917a.jpg) | ![3091723947133 .pic](https://img.picgo.net/2024/08/18/3091723947133_.pic6a79898438e15d1e.jpg) | 87 | | | | 88 | | ![3081723947124 .pic](https://img.picgo.net/2024/08/18/3081723947124_.pic612b93a07b04205c.jpg) | ![3071723947102 .pic](https://img.picgo.net/2024/08/18/3071723947102_.pic6e8f37129de2736d.png) | -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abeiheng/bh-wechaty-help/099ff920ddcf885fb8e7f7b5740a6929517ded12/build/icon.png -------------------------------------------------------------------------------- /build/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize') 2 | 3 | module.exports = async (context) => { 4 | if (process.platform !== 'darwin') return 5 | 6 | console.log('aftersign hook triggered, start to notarize app.') 7 | 8 | if (!process.env.CI) { 9 | console.log(`skipping notarizing, not in CI.`) 10 | return 11 | } 12 | 13 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 14 | console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.') 15 | return 16 | } 17 | 18 | const appId = 'com.electron.app' 19 | 20 | const { appOutDir } = context 21 | 22 | const appName = context.packager.appInfo.productFilename 23 | 24 | try { 25 | await notarize({ 26 | appBundleId: appId, 27 | appPath: `${appOutDir}/${appName}.app`, 28 | appleId: process.env.APPLE_ID, 29 | appleIdPassword: process.env.APPLEIDPASS 30 | }) 31 | } catch (error) { 32 | console.error(error) 33 | } 34 | 35 | console.log(`done notarizing ${appId}.`) 36 | } 37 | -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | provider: generic 2 | url: https://example.com/auto-updates 3 | updaterCacheDirName: wechatybot-updater 4 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: cn.bhsr922.bot 2 | productName: bh-wechaty-help 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' 12 | asarUnpack: 13 | - resources/** 14 | win: 15 | executableName: wechatybot 16 | nsis: 17 | artifactName: ${name}-${version}.${ext} 18 | shortcutName: ${productName} 19 | uninstallDisplayName: ${productName} 20 | createDesktopShortcut: always 21 | oneClick: false 22 | allowToChangeInstallationDirectory: true 23 | mac: 24 | entitlementsInherit: build/entitlements.mac.plist 25 | extendInfo: 26 | - NSCameraUsageDescription: Application requests access to the device's camera. 27 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 28 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 29 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 30 | notarize: false 31 | dmg: 32 | artifactName: ${name}-${version}.${ext} 33 | linux: 34 | target: 35 | - AppImage 36 | - snap 37 | - deb 38 | maintainer: electronjs.org 39 | category: Utility 40 | appImage: 41 | artifactName: ${name}-${version}.${ext} 42 | npmRebuild: false 43 | publish: 44 | provider: generic 45 | url: https://example.com/auto-updates 46 | electronDownload: 47 | mirror: https://npmmirror.com/mirrors/electron/ 48 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import vue from '@vitejs/plugin-vue' 4 | 5 | export default defineConfig({ 6 | main: { 7 | plugins: [externalizeDepsPlugin()] 8 | }, 9 | preload: { 10 | plugins: [externalizeDepsPlugin()] 11 | }, 12 | renderer: { 13 | resolve: { 14 | alias: { 15 | '@renderer': resolve('src/renderer/src') 16 | } 17 | }, 18 | plugins: [vue()] 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bh-wechaty-help", 3 | "version": "1.0.0", 4 | "description": "北恒微信助手,解放您的双手,多种方案可同时运行,亲测稳定运行24小时不掉线。", 5 | "main": "./out/main/index.js", 6 | "author": "北恒", 7 | "homepage": "https://bot.bhsr922.cn/", 8 | "scripts": { 9 | "format": "prettier --write .", 10 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", 11 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 12 | "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", 13 | "typecheck": "npm run typecheck:node && npm run typecheck:web", 14 | "start": "electron-vite preview", 15 | "dev": "electron-vite dev --watch", 16 | "build": "npm run typecheck && electron-vite build", 17 | "postinstall": "electron-builder install-app-deps", 18 | "build:unpack": "npm run build && electron-builder --dir", 19 | "build:win": "npm run build && electron-builder --win", 20 | "build:mac": "npm run build && electron-builder --mac", 21 | "build:linux": "npm run build && electron-builder --linux" 22 | }, 23 | "dependencies": { 24 | "@electron-toolkit/preload": "^3.0.0", 25 | "@electron-toolkit/utils": "^3.0.0", 26 | "@element-plus/icons-vue": "^2.3.1", 27 | "@fingerprintjs/fingerprintjs": "^4.4.3", 28 | "@icon-park/vue-next": "^1.4.2", 29 | "aws-sdk": "^2.1672.0", 30 | "axios": "^1.7.2", 31 | "dayjs": "^1.11.12", 32 | "electron-conf": "^1.1.0", 33 | "electron-updater": "^6.1.7", 34 | "element-plus": "^2.7.8", 35 | "esdk-obs-nodejs": "^3.24.3", 36 | "etcd3": "^1.1.2", 37 | "jimp": "^0.22.12", 38 | "pinia": "^2.2.0", 39 | "pinia-plugin-persistedstate": "^3.2.1", 40 | "qrcode-reader": "^1.0.4", 41 | "vue-router": "4", 42 | "wechaty": "^1.20.2" 43 | }, 44 | "devDependencies": { 45 | "@electron-toolkit/eslint-config": "^1.0.2", 46 | "@electron-toolkit/eslint-config-ts": "^2.0.0", 47 | "@electron-toolkit/tsconfig": "^1.0.1", 48 | "@rushstack/eslint-patch": "^1.10.3", 49 | "@types/node": "^20.14.8", 50 | "@vitejs/plugin-vue": "^5.0.5", 51 | "@vue/eslint-config-prettier": "^9.0.0", 52 | "@vue/eslint-config-typescript": "^13.0.0", 53 | "autoprefixer": "^10.4.19", 54 | "electron": "^31.0.2", 55 | "electron-builder": "^24.13.3", 56 | "electron-vite": "^2.3.0", 57 | "eslint": "^8.57.0", 58 | "eslint-plugin-vue": "^9.26.0", 59 | "postcss": "^8.4.40", 60 | "prettier": "^3.3.2", 61 | "sass": "^1.77.8", 62 | "tailwindcss": "^3.4.7", 63 | "typescript": "^5.5.2", 64 | "vite": "^5.3.1", 65 | "vue": "^3.4.30", 66 | "vue-tsc": "^2.0.22" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abeiheng/bh-wechaty-help/099ff920ddcf885fb8e7f7b5740a6929517ded12/resources/icon.png -------------------------------------------------------------------------------- /resources/macTray@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abeiheng/bh-wechaty-help/099ff920ddcf885fb8e7f7b5740a6929517ded12/resources/macTray@2x.png -------------------------------------------------------------------------------- /resources/windowTray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abeiheng/bh-wechaty-help/099ff920ddcf885fb8e7f7b5740a6929517ded12/resources/windowTray.png -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { electronApp, optimizer } from '@electron-toolkit/utils' 2 | import { app, BrowserWindow, shell } from 'electron' 3 | import path, { join } from 'path' 4 | import icon from '../../resources/icon.png?asset' 5 | import { createTray } from './tray' 6 | import './ipcMain' 7 | import { Conf } from 'electron-conf/main' 8 | const conf = new Conf() 9 | conf.registerRendererListener() 10 | function createWindow(): BrowserWindow { 11 | const mainWindow = new BrowserWindow({ 12 | width: 1400, 13 | height: 900, 14 | show: false, 15 | alwaysOnTop: false, 16 | resizable: false, 17 | frame: false, 18 | autoHideMenuBar: false, 19 | skipTaskbar: false, 20 | ...(process.platform === 'linux' ? { icon } : {}), 21 | webPreferences: { 22 | preload: join(__dirname, '../preload/index.js'), 23 | sandbox: false, 24 | }, 25 | }) 26 | 27 | mainWindow.on('ready-to-show', () => { 28 | mainWindow.show() 29 | }) 30 | mainWindow.webContents.on('before-input-event', (event, input) => { 31 | if (input.key === 'F12' || (input.control && input.key === 'KeyI')) { 32 | event.preventDefault() 33 | } 34 | }) 35 | mainWindow.webContents.setWindowOpenHandler((details) => { 36 | shell.openExternal(details.url) 37 | return { action: 'deny' } 38 | }) 39 | if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) { 40 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) 41 | } else { 42 | mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')) 43 | } 44 | return mainWindow 45 | } 46 | app.whenReady().then(() => { 47 | if (process.platform == 'darwin') app.dock.hide() 48 | electronApp.setAppUserModelId('com.electron') 49 | app.on('browser-window-created', (_, window) => { 50 | optimizer.watchWindowShortcuts(window) 51 | }) 52 | const win = createWindow() 53 | createTray(win) 54 | }) 55 | app.on('window-all-closed', () => { 56 | if (process.platform !== 'darwin') { 57 | app.quit() 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/main/ipcMain/axios/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' 2 | import useStorage from '../composables/useStorage' 3 | export default class Axios { 4 | private instance 5 | constructor(config: AxiosRequestConfig) { 6 | this.instance = axios.create(config) 7 | this.interceptors() 8 | } 9 | 10 | public async request(config: AxiosRequestConfig) { 11 | return new Promise(async (resolve) => { 12 | try { 13 | const response = await this.instance.request(config) 14 | resolve(response.data) 15 | } catch (error) { 16 | console.log('报错了') 17 | } 18 | }) as Promise 19 | } 20 | 21 | private interceptors() { 22 | this.interceptorsRequest() 23 | this.interceptorsResponse() 24 | } 25 | 26 | private interceptorsRequest() { 27 | this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => { 28 | const storage = useStorage() 29 | config.headers.Accept = 'application/json' 30 | config.headers.Authorization = `Bearer ${storage.get('login_token')}` 31 | return config 32 | }) 33 | } 34 | private interceptorsResponse() { 35 | this.instance.interceptors.response.use( 36 | (response): AxiosResponse => { 37 | return response 38 | }, 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/ipcMain/axios/index.ts: -------------------------------------------------------------------------------- 1 | import Axios from './axios' 2 | 3 | const http = new Axios({ 4 | baseURL: 'https://bot.bhsr922.cn/api', 5 | timeout: 10000, 6 | }) 7 | export { http} -------------------------------------------------------------------------------- /src/main/ipcMain/composables/useBlackWord.ts: -------------------------------------------------------------------------------- 1 | import { http } from "../axios"; 2 | 3 | export default () => { 4 | const getMsgResult = ( 5 | msgType: string, 6 | roomTopic: string, 7 | msgContact: string, 8 | msgContent: string 9 | ) => { 10 | return http.request({ 11 | url: "/blackWord/getMsgResult", 12 | method: "GET", 13 | params: { 14 | msgType, 15 | roomTopic, 16 | msgContact, 17 | msgContent, 18 | }, 19 | }); 20 | }; 21 | return { getMsgResult }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/main/ipcMain/composables/useFriendPlan.ts: -------------------------------------------------------------------------------- 1 | import { http } from "../axios"; 2 | 3 | export default () => { 4 | const getplanInfo = (verifyMsg: string) => { 5 | return http.request({ 6 | url: "/friendPlan/getplanInfo", 7 | method: "GET", 8 | params: { 9 | verifyMsg, 10 | }, 11 | }); 12 | }; 13 | const getverifyMsgResult = (verifyMsg: string, contact: string) => { 14 | return http.request({ 15 | url: "/friendPlan/getverifyMsgResult", 16 | method: "GET", 17 | params: { 18 | verifyMsg, 19 | contact, 20 | }, 21 | }); 22 | }; 23 | return { getplanInfo, getverifyMsgResult }; 24 | }; 25 | 26 | interface FriendModel { 27 | id: number; 28 | planName: string; 29 | verifyMsg: string; 30 | callMsg: string; 31 | status: boolean; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/ipcMain/composables/useGroupMsg.ts: -------------------------------------------------------------------------------- 1 | import { http } from '../axios' 2 | 3 | export default () => { 4 | const getShareById = (id: number) => { 5 | return http.request({ 6 | url: `/groupShare/getShareById/${id}`, 7 | method: 'GET', 8 | }) 9 | } 10 | return { getShareById } 11 | } 12 | interface groupShare { 13 | id: number; 14 | planName: string; 15 | sendRoom: string; 16 | gapNum: number; 17 | msgOne: string; 18 | msgTwo: string; 19 | msgThree: string; 20 | status: boolean; 21 | createdAt: Date; 22 | updatedAt: Date; 23 | userId: number; 24 | } -------------------------------------------------------------------------------- /src/main/ipcMain/composables/useGroupReply.ts: -------------------------------------------------------------------------------- 1 | import { http } from "../axios"; 2 | 3 | export default () => { 4 | const getAllTopicKey = () => { 5 | return http.request({ 6 | url: "/groupReply/getAllTopicKey", 7 | method: "GET", 8 | }); 9 | }; 10 | const getMsgReply = (keyWord: string, topic: string, contact: string) => { 11 | return http.request({ 12 | url: "/groupReply/getMsgReply", 13 | method: "GET", 14 | params: { 15 | keyWord, 16 | topic, 17 | contact, 18 | }, 19 | }); 20 | }; 21 | return { getAllTopicKey, getMsgReply }; 22 | }; 23 | 24 | interface TopicKey { 25 | topicList: Array; 26 | keyWordList: Array; 27 | } 28 | interface MsgReply { 29 | id: number; 30 | planName: string; 31 | listenRoom: string; 32 | triggerWord: string; 33 | msgOne: string; 34 | msgTwo: string; 35 | msgThree: string; 36 | status: boolean; 37 | createdAt: Date; 38 | updatedAt: Date; 39 | userId: number; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/ipcMain/composables/useMsgShare.ts: -------------------------------------------------------------------------------- 1 | import { http } from "../axios"; 2 | 3 | export default () => { 4 | const getAllTopic = () => { 5 | return http.request({ 6 | url: "/msgShare/getAllTopic", 7 | method: "GET", 8 | }); 9 | }; 10 | return { getAllTopic }; 11 | }; 12 | interface listenTopic { 13 | listenTopic: Array; 14 | shareTopic: Array; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/ipcMain/composables/usePeopleReply.ts: -------------------------------------------------------------------------------- 1 | import { http } from "../axios"; 2 | 3 | export default () => { 4 | const getMsgReply = (keyWord: string, contact: string) => { 5 | return http.request({ 6 | url: "/peopleReply/getMsgReply", 7 | method: "GET", 8 | params: { 9 | keyWord, 10 | contact, 11 | }, 12 | }); 13 | }; 14 | const getAllKeyWord = () => { 15 | return http.request>({ 16 | url: "/peopleReply/getAllKeyWord", 17 | method: "GET", 18 | }); 19 | }; 20 | return { getMsgReply, getAllKeyWord }; 21 | }; 22 | 23 | interface peopleReply { 24 | id: number; 25 | planName: string; 26 | triggerWord: string; 27 | msgOne: string; 28 | msgTwo: string; 29 | msgThree: string; 30 | status: boolean; 31 | createdAt: Date; 32 | updatedAt: Date; 33 | userId: number; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/ipcMain/composables/usePermission.ts: -------------------------------------------------------------------------------- 1 | import { http } from "../axios"; 2 | 3 | export default () => { 4 | const permissionStatus = (type: string) => { 5 | return http 6 | .request({ 7 | url: "/permission/getOnePermission", 8 | method: "GET", 9 | params: { 10 | type, 11 | }, 12 | }) 13 | }; 14 | const getTopicType = (topic: string) => { 15 | return http.request({ 16 | url: "/public/getTopicType", 17 | method: "GET", 18 | params: { 19 | topic, 20 | }, 21 | }); 22 | }; 23 | return { permissionStatus, getTopicType }; 24 | }; -------------------------------------------------------------------------------- /src/main/ipcMain/composables/useStorage.ts: -------------------------------------------------------------------------------- 1 | import { Conf } from 'electron-conf/main' 2 | export default () => { 3 | const conf = new Conf() 4 | function set(key: string, data: any): void { 5 | conf.set(key, data) 6 | } 7 | function get(key: string): any { 8 | const cacheStore: any = conf.get(key) 9 | return cacheStore.data 10 | } 11 | function remove(key: string) { 12 | conf.delete(key) 13 | } 14 | return { set, get, remove } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/ipcMain/index.ts: -------------------------------------------------------------------------------- 1 | import './wechatyBot' 2 | -------------------------------------------------------------------------------- /src/main/ipcMain/wechatyBot/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain, IpcMainEvent, IpcMainInvokeEvent } from 'electron' 2 | import { log, ScanStatus, WechatyBuilder } from 'wechaty' 3 | import usePermission from '../composables/usePermission' 4 | import { friendPlan } from './plugins/friendPlan' 5 | import { peopleReply } from './plugins/peopleReply' 6 | import { blackWord } from './plugins/blackWord' 7 | import { groupReply } from './plugins/groupReply' 8 | import { msgShare } from './plugins/msgShare' 9 | import { groupMsg } from './plugins/groupMsg' 10 | const { permissionStatus, getTopicType } = usePermission() 11 | const bot = WechatyBuilder.build({ 12 | name: 'bhBot', 13 | }) 14 | ipcMain.handle('startBot', async (event: IpcMainInvokeEvent) => { 15 | try { 16 | await bot.stop() 17 | } catch (err) { 18 | console.log('又错了') 19 | } 20 | const win = BrowserWindow.fromWebContents(event.sender) 21 | bot 22 | .on('scan', (qrcode, status) => { 23 | console.log(status) 24 | if (status === ScanStatus.Waiting && qrcode) { 25 | win?.webContents.send( 26 | 'wechatyScan', 27 | ['https://api.qrserver.com/v1/create-qr-code/?data=', encodeURIComponent(qrcode)].join(''), 28 | ) 29 | } 30 | }) 31 | .on('login', (user) => { 32 | win?.webContents.send('wechatyLogin', user) 33 | }) 34 | .on('message', async (message) => { 35 | const msgAge = message.age() 36 | const room = message.room() 37 | const talkerMsg = message.text() 38 | const talker = message.talker() 39 | if (!message.self() && msgAge <= 60 && !room && message.type() === bot.Message.Type.Text) { 40 | //私聊文本处理 41 | const peopleReplyStatus = await permissionStatus('peopleReply') 42 | if (peopleReplyStatus) { 43 | await peopleReply(talkerMsg, talker) 44 | } 45 | } else if (!message.self() && msgAge <= 60 && room) { 46 | //群聊处理 47 | const blackWordStatus = await permissionStatus('blackWord') 48 | if (blackWordStatus) { 49 | const checkResult = await blackWord(talkerMsg, room, talker, message) 50 | if (!checkResult) { 51 | const topic = await room.topic() 52 | const roomType = await getTopicType(topic) 53 | if (roomType === 'groupReply') { 54 | //群聊关键词回复 55 | const groupReplyStatus = await permissionStatus('groupReply') 56 | if (groupReplyStatus) { 57 | await groupReply(talkerMsg, topic, talker.payload?.name!, room) 58 | } 59 | } else if (roomType === 'msgShare' && message.type() === bot.Message.Type.Text) { 60 | //消息转发 61 | const msgShareStatus = await permissionStatus('msgShare') 62 | if (msgShareStatus) { 63 | await msgShare(topic, talkerMsg, bot) 64 | } 65 | } 66 | return 67 | } 68 | } 69 | } 70 | }) 71 | .on('friendship', async (friendship) => { 72 | const friendStatus = await permissionStatus('friend') 73 | if (friendStatus) { 74 | await friendPlan(friendship, bot) 75 | } 76 | }) 77 | .on('error', async (error) => { 78 | log.error(`❌程序报错-程序报错-${error.code}❌`) 79 | if (error.code == 2) { 80 | botQuit(win) 81 | } 82 | }) 83 | bot 84 | .start() 85 | .then(() => { 86 | log.info('🚀机器人启动🚀') 87 | }) 88 | .catch((error) => { 89 | log.info(error, 'start的error') 90 | }) 91 | return true 92 | }) 93 | 94 | ipcMain.handle('botStatus', async (event: IpcMainInvokeEvent) => { 95 | const win = BrowserWindow.fromWebContents(event.sender) 96 | checkBotStatus(win) 97 | }) 98 | 99 | ipcMain.on('sendMsg', async (event: IpcMainEvent, id: number) => { 100 | const win = BrowserWindow.fromWebContents(event.sender) 101 | await groupMsg(bot, id, win!) 102 | }) 103 | 104 | const checkBotStatus = (win: BrowserWindow | null) => { 105 | try { 106 | const res = bot.isLoggedIn 107 | if (res) { 108 | win?.webContents.send('userBotOut', 'login') 109 | } else { 110 | botQuit(win) 111 | } 112 | } catch (error: any) { 113 | console.log(error.code) 114 | if (error.code == 2) { 115 | botQuit(win) 116 | } 117 | } 118 | } 119 | 120 | const botQuit = async (win: BrowserWindow | null) => { 121 | try { 122 | await bot.logout() 123 | await bot.stop() 124 | } catch (err) { 125 | console.log('忽略了') 126 | } 127 | win?.webContents.send('userBotOut', 'logOut') 128 | } 129 | -------------------------------------------------------------------------------- /src/main/ipcMain/wechatyBot/plugins/blackWord.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContactInterface, 3 | MessageInterface, 4 | RoomInterface, 5 | } from "wechaty/impls"; 6 | import useBlackWord from "../../composables/useBlackWord"; 7 | const QrCode = require("qrcode-reader"); 8 | const Jimp = require("jimp"); 9 | const { getMsgResult } = useBlackWord(); 10 | const blackWord = async ( 11 | talkerMsg: string, 12 | room: RoomInterface, 13 | talker: ContactInterface, 14 | message: MessageInterface 15 | ) => { 16 | const phoneReg = 17 | /^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[189]))\d{8}$/; 18 | const urlReg = 19 | /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/; 20 | const topic = await room.topic(); 21 | //手机号检测 22 | if (phoneReg.test(talkerMsg)) { 23 | const result = await getMsgResult( 24 | "手机号", 25 | topic, 26 | talker.payload?.name!, 27 | talkerMsg 28 | ); 29 | return result; 30 | } 31 | //链接检测 32 | if (urlReg.test(talkerMsg)) { 33 | const result = await getMsgResult( 34 | "链接", 35 | topic, 36 | talker.payload?.name!, 37 | talkerMsg 38 | ); 39 | return result; 40 | } 41 | //二维码检测 42 | const messageType = message.type(); 43 | if (messageType == 6) { 44 | const image = await message.toFileBox(); 45 | const toBuffer = await image.toBuffer(); 46 | return new Promise((resolve) => { 47 | Jimp.read(toBuffer, (err: any, image: any) => { 48 | if (err) { 49 | resolve(false); 50 | } 51 | const qr = new QrCode(); 52 | qr.callback = async (_err:any, value:any) => { 53 | if (value) { 54 | const result = await getMsgResult( 55 | "二维码", 56 | topic, 57 | talker.payload?.name!, 58 | value.result 59 | ); 60 | resolve(result); 61 | } 62 | }; 63 | qr.decode(image.bitmap); 64 | }); 65 | }); 66 | } 67 | return 68 | }; 69 | export { blackWord }; 70 | -------------------------------------------------------------------------------- /src/main/ipcMain/wechatyBot/plugins/friendPlan.ts: -------------------------------------------------------------------------------- 1 | import { FriendshipInterface, WechatyInterface } from "wechaty/impls"; 2 | import useFriendPlan from "../../composables/useFriendPlan"; 3 | 4 | const { getplanInfo, getverifyMsgResult } = useFriendPlan(); 5 | const friendPlan = async ( 6 | friendship: FriendshipInterface, 7 | bot: WechatyInterface 8 | ) => { 9 | const friendInfo = await getplanInfo(friendship.hello()); 10 | if (friendInfo && friendship.type() === bot.Friendship.Type.Receive) { 11 | await friendship.accept(); 12 | const friendContact = friendship.contact(); 13 | if (friendInfo.callMsg) { 14 | await friendContact.say(friendInfo.callMsg); 15 | } 16 | await getverifyMsgResult(friendship.hello(), friendContact.payload?.name!); 17 | } 18 | }; 19 | export { friendPlan }; 20 | -------------------------------------------------------------------------------- /src/main/ipcMain/wechatyBot/plugins/groupMsg.ts: -------------------------------------------------------------------------------- 1 | import { WechatyInterface } from 'wechaty/impls' 2 | import useGroupMsg from '../../composables/useGroupMsg' 3 | import { BrowserWindow } from 'electron' 4 | const { getShareById } = useGroupMsg() 5 | const groupMsg = async (bot: WechatyInterface, id: number, win: BrowserWindow) => { 6 | const groupSharMsg = await getShareById(+id) 7 | let sendRoomList = groupSharMsg.sendRoom.split('#') 8 | for (let i = 0; i < sendRoomList.length; i++) { 9 | const room = await bot.Room.find({ topic: sendRoomList[i] }) 10 | console.log('找到房间了') 11 | if (room) { 12 | const messages = [groupSharMsg.msgOne, groupSharMsg.msgTwo, groupSharMsg.msgThree].filter(Boolean) 13 | for (const message of messages) { 14 | await room.say(message) 15 | await delay(groupSharMsg.gapNum * 1000) 16 | } 17 | await delay(2000) 18 | } 19 | } 20 | win?.webContents.send('userBotOut', 'success') 21 | } 22 | const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) 23 | export { groupMsg } 24 | -------------------------------------------------------------------------------- /src/main/ipcMain/wechatyBot/plugins/groupReply.ts: -------------------------------------------------------------------------------- 1 | import { RoomInterface } from "wechaty/impls"; 2 | import useGroupReply from "../../composables/useGroupReply"; 3 | 4 | const { getAllTopicKey, getMsgReply } = useGroupReply(); 5 | const groupReply = async ( 6 | keyWord: string, 7 | topic: string, 8 | contact: string, 9 | room: RoomInterface 10 | ) => { 11 | const topicKey = await getAllTopicKey(); 12 | if ( 13 | topicKey.topicList.includes(topic) && 14 | topicKey.keyWordList.includes(keyWord) 15 | ) { 16 | const msgReply = await getMsgReply(keyWord, topic, contact); 17 | if (msgReply) { 18 | const messages = [ 19 | msgReply.msgOne, 20 | msgReply.msgTwo, 21 | msgReply.msgThree, 22 | ].filter(Boolean); 23 | for (const message of messages) { 24 | await room.say(message); 25 | await delay(1000); 26 | } 27 | } 28 | } 29 | return; 30 | }; 31 | const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 32 | export { groupReply }; 33 | -------------------------------------------------------------------------------- /src/main/ipcMain/wechatyBot/plugins/msgShare.ts: -------------------------------------------------------------------------------- 1 | import { WechatyInterface } from "wechaty/impls"; 2 | import useMsgShare from "../../composables/useMsgShare"; 3 | 4 | const { getAllTopic } = useMsgShare(); 5 | const msgShare = async ( 6 | topic: string, 7 | talkerMsg: string, 8 | bot: WechatyInterface 9 | ) => { 10 | const listenRoomList = await getAllTopic(); 11 | const topicInfo = listenRoomList.find((item) => 12 | item.listenTopic.includes(topic) 13 | ); 14 | if (topicInfo) { 15 | for (const roomTopic of topicInfo.shareTopic) { 16 | const currentRoom = await bot.Room.find({ topic: roomTopic }); 17 | if (currentRoom) { 18 | await currentRoom.say(talkerMsg); 19 | await delay(1000); 20 | } else { 21 | return; 22 | } 23 | } 24 | } else { 25 | return; 26 | } 27 | }; 28 | const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 29 | export { msgShare }; 30 | -------------------------------------------------------------------------------- /src/main/ipcMain/wechatyBot/plugins/peopleReply.ts: -------------------------------------------------------------------------------- 1 | import { ContactInterface } from "wechaty/impls"; 2 | import usePeopleReply from "../../composables/usePeopleReply"; 3 | const { getMsgReply, getAllKeyWord } = usePeopleReply(); 4 | const peopleReply = async (talkerMsg: string, talker: ContactInterface) => { 5 | const keyWords = await getAllKeyWord(); 6 | const exitKey = keyWords.filter((word) => talkerMsg.includes(word)); 7 | if (exitKey.length) { 8 | const replyInfo = await getMsgReply(exitKey[0], talker.payload?.name!); 9 | if (replyInfo) { 10 | const messages = [ 11 | replyInfo.msgOne, 12 | replyInfo.msgTwo, 13 | replyInfo.msgThree, 14 | ].filter(Boolean); 15 | for (const message of messages) { 16 | await talker.say(message); 17 | await delay(1000); 18 | } 19 | } 20 | } 21 | }; 22 | const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 23 | export { peopleReply }; 24 | -------------------------------------------------------------------------------- /src/main/tray.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu, Tray } from 'electron' 2 | const path = require('path') 3 | const createTray = (win: BrowserWindow) => { 4 | const tray = new Tray( 5 | path.resolve( 6 | __dirname, 7 | process.platform == 'darwin' 8 | ? '../../resources/macTray@2x.png' 9 | : '../../resources/windowTray.png' 10 | ) 11 | ) 12 | const contextMenu = Menu.buildFromTemplate([ 13 | { 14 | label: '最小化', 15 | click: () => { 16 | win.hide() 17 | } 18 | }, 19 | { 20 | label: '打开应用', 21 | click: () => { 22 | win.show() 23 | } 24 | }, 25 | { 26 | label: '关闭程序', 27 | click: () => { 28 | win.webContents.send('quit') 29 | app.quit() 30 | } 31 | } 32 | ]) 33 | tray.setToolTip('北恒微信助手') 34 | tray.setContextMenu(contextMenu) 35 | } 36 | 37 | export { createTray } 38 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload' 2 | import { ContactSelfInterface } from 'wechaty/impls' 3 | 4 | declare global { 5 | interface Window { 6 | electron: ElectronAPI 7 | api: { 8 | startBot: () => Promise 9 | sendMsg: (id: number) => void 10 | botStatus: () => boolean 11 | wechatyScan: (callBack: Function) => void 12 | wechatyLogin: (callBack: Function) => ContactSelfInterface 13 | userBotOut: (callBack: Function) => void 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | import { electronAPI } from '@electron-toolkit/preload' 3 | 4 | const api = { 5 | startBot: () => { 6 | return ipcRenderer.invoke('startBot') 7 | }, 8 | sendMsg: (id: number) => { 9 | console.log('玩呢') 10 | console.log('sendMsg', id) 11 | ipcRenderer.send('sendMsg', +id) 12 | }, 13 | botStatus: () => { 14 | return ipcRenderer.invoke('botStatus') 15 | }, 16 | wechatyScan: (qrcode: any) => ipcRenderer.on('wechatyScan', qrcode), 17 | wechatyLogin: (userInfo: any) => ipcRenderer.on('wechatyLogin', userInfo), 18 | userBotOut: (callBack) => ipcRenderer.on('userBotOut', callBack), 19 | } 20 | if (process.contextIsolated) { 21 | try { 22 | contextBridge.exposeInMainWorld('electron', electronAPI) 23 | contextBridge.exposeInMainWorld('api', api) 24 | } catch (error) { 25 | console.error(error) 26 | } 27 | } else { 28 | // @ts-ignore (define in dts) 29 | window.electron = electronAPI 30 | // @ts-ignore (define in dts) 31 | window.api = api 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 北恒微信助手 6 | 7 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/renderer/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /src/renderer/src/assets/global.scss: -------------------------------------------------------------------------------- 1 | .drag { 2 | -webkit-app-region: drag; 3 | } 4 | 5 | .nodrag, 6 | input, 7 | button, 8 | textarea { 9 | -webkit-app-region: no-drag; 10 | } 11 | // body { 12 | // @apply w-screen h-screen bg-[#f3f2f7]; 13 | // } 14 | -------------------------------------------------------------------------------- /src/renderer/src/assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abeiheng/bh-wechaty-help/099ff920ddcf885fb8e7f7b5740a6929517ded12/src/renderer/src/assets/logo.jpg -------------------------------------------------------------------------------- /src/renderer/src/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/renderer/src/composables/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { CacheEnum } from '@renderer/enum/CacheEnum' 2 | import useStorage from './useStorage' 3 | import { http } from '@renderer/plugins/axios' 4 | import { reactive } from 'vue' 5 | import useUserStore from '@renderer/store/useUserStore' 6 | 7 | export default () => { 8 | const form = reactive({ 9 | userKey: '', 10 | }) 11 | const storage = useStorage() 12 | //登录检测 13 | function isLogin(): boolean { 14 | return useStorage().get(CacheEnum.TOKEN_NAME) 15 | } 16 | const login = async (userKey: string) => { 17 | const { token } = await http.request<{ token: string }>({ 18 | url: '/user/login', 19 | method: 'POST', 20 | data: { 21 | userKey, 22 | }, 23 | }) 24 | storage.set(CacheEnum.TOKEN_NAME, token) 25 | useUserStore().getCurrentUser() 26 | return true 27 | } 28 | return { isLogin, form, login } 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/composables/useBlackWord.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@renderer/plugins/axios' 2 | import { ElMessage } from 'element-plus' 3 | import { ref } from 'vue' 4 | 5 | export default () => { 6 | const planList = ref() 7 | const planInfo = ref() 8 | const getAllPlanList = async () => { 9 | planList.value = await http.request({ 10 | url: '/blackWord/getAllPlanList', 11 | }) 12 | } 13 | const getPlanById = async (id: number) => { 14 | planInfo.value = await http.request({ 15 | url: `/blackWord/getPlanById/${id}`, 16 | }) 17 | } 18 | const createPlan = async (data: Record) => { 19 | const res = await http.request({ 20 | url: '/blackWord/createPlan', 21 | method: 'POST', 22 | data, 23 | }) 24 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 25 | getAllPlanList() 26 | } 27 | 28 | const deletePlan = async (id: number) => { 29 | const res = await http.request({ 30 | url: `/blackWord/deletePlan/${id}`, 31 | method: 'DELETE', 32 | }) 33 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 34 | getAllPlanList() 35 | } 36 | const updatePlan = async () => { 37 | const res = await http.request({ 38 | url: `/blackWord/updatePlan/${planInfo.value?.id}`, 39 | method: 'PATCH', 40 | data: planInfo.value, 41 | }) 42 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 43 | getAllPlanList() 44 | } 45 | return { getAllPlanList, planList, createPlan, updatePlan, planInfo, deletePlan, getPlanById } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/src/composables/useFriendPlan.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@renderer/plugins/axios' 2 | import { ElMessage } from 'element-plus' 3 | import { ref } from 'vue' 4 | 5 | export default () => { 6 | const planList = ref() 7 | const planInfo = ref() 8 | const getAllPlanList = async () => { 9 | planList.value = await http.request({ 10 | url: '/friendPlan/getAllPlanList', 11 | }) 12 | } 13 | const getPlanById = async (id: number) => { 14 | planInfo.value = await http.request({ 15 | url: `/friendPlan/getPlanById/${id}`, 16 | }) 17 | } 18 | const createPlan = async (data: Record) => { 19 | const res = await http.request({ 20 | url: '/friendPlan/createPlan', 21 | method: 'POST', 22 | data, 23 | }) 24 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 25 | getAllPlanList() 26 | } 27 | 28 | const deletePlan = async (id: number) => { 29 | const res = await http.request({ 30 | url: `/friendPlan/deletePlan/${id}`, 31 | method: 'DELETE', 32 | }) 33 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 34 | getAllPlanList() 35 | } 36 | const updatePlan = async () => { 37 | const res = await http.request({ 38 | url: `/friendPlan/updatePlan/${planInfo.value?.id}`, 39 | method: 'PATCH', 40 | data: planInfo.value, 41 | }) 42 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 43 | getAllPlanList() 44 | } 45 | return { getAllPlanList, planList, createPlan, updatePlan, planInfo, deletePlan, getPlanById } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/src/composables/useGroupReply.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@renderer/plugins/axios' 2 | import { ElMessage } from 'element-plus' 3 | import { ref } from 'vue' 4 | 5 | export default () => { 6 | const replyList = ref() 7 | const replyInfo = ref() 8 | const getAllReplyList = async () => { 9 | replyList.value = await http.request({ 10 | url: '/groupReply/getAllReplyList', 11 | }) 12 | } 13 | const getReplyById = async (id: number) => { 14 | replyInfo.value = await http.request({ 15 | url: `/groupReply/getReplyById/${id}`, 16 | }) 17 | } 18 | const createReply = async (data: Record) => { 19 | const res = await http.request({ 20 | url: '/groupReply/createReply', 21 | method: 'POST', 22 | data, 23 | }) 24 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 25 | getAllReplyList() 26 | } 27 | 28 | const deleteReply = async (id: number) => { 29 | const res = await http.request({ 30 | url: `/groupReply/deleteReply/${id}`, 31 | method: 'DELETE', 32 | }) 33 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 34 | getAllReplyList() 35 | } 36 | const updateReply = async () => { 37 | const res = await http.request({ 38 | url: `/groupReply/updateReply/${replyInfo.value?.id}`, 39 | method: 'PATCH', 40 | data: replyInfo.value, 41 | }) 42 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 43 | getAllReplyList() 44 | } 45 | return { getAllReplyList, replyList, createReply, updateReply, replyInfo, deleteReply, getReplyById } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/src/composables/useGroupShare.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@renderer/plugins/axios' 2 | import { ElMessage } from 'element-plus' 3 | import { ref } from 'vue' 4 | 5 | export default () => { 6 | const shareList = ref() 7 | const shareInfo = ref() 8 | const getAllShareList = async () => { 9 | shareList.value = await http.request({ 10 | url: '/groupShare/getAllShareList', 11 | }) 12 | } 13 | const getShareById = async (id: number) => { 14 | shareInfo.value = await http.request({ 15 | url: `/groupShare/getShareById/${id}`, 16 | }) 17 | } 18 | const createShare = async (data: Record) => { 19 | const res = await http.request({ 20 | url: '/groupShare/createShare', 21 | method: 'POST', 22 | data, 23 | }) 24 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 25 | getAllShareList() 26 | } 27 | 28 | const deleteShare = async (id: number) => { 29 | const res = await http.request({ 30 | url: `/groupShare/deleteShare/${id}`, 31 | method: 'DELETE', 32 | }) 33 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 34 | getAllShareList() 35 | } 36 | const updateShare = async () => { 37 | const res = await http.request({ 38 | url: `/groupShare/updateShare/${shareInfo.value?.id}`, 39 | method: 'PATCH', 40 | data: shareInfo.value, 41 | }) 42 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 43 | getAllShareList() 44 | } 45 | return { getAllShareList, shareList, createShare, updateShare, shareInfo, deleteShare, getShareById } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/src/composables/useMsgShare.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@renderer/plugins/axios' 2 | import { ElMessage } from 'element-plus' 3 | import { ref } from 'vue' 4 | 5 | export default () => { 6 | const shareList = ref() 7 | const shareInfo = ref() 8 | const getAllShareList = async () => { 9 | shareList.value = await http.request({ 10 | url: '/msgShare/getAllShareList', 11 | }) 12 | } 13 | const getShareById = async (id: number) => { 14 | shareInfo.value = await http.request({ 15 | url: `/msgShare/getShareById/${id}`, 16 | }) 17 | } 18 | const createShare = async (data: Record) => { 19 | const res = await http.request({ 20 | url: '/msgShare/createShare', 21 | method: 'POST', 22 | data, 23 | }) 24 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 25 | getAllShareList() 26 | } 27 | 28 | const deleteShare = async (id: number) => { 29 | const res = await http.request({ 30 | url: `/msgShare/deleteShare/${id}`, 31 | method: 'DELETE', 32 | }) 33 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 34 | getAllShareList() 35 | } 36 | const updateShare = async () => { 37 | const res = await http.request({ 38 | url: `/msgShare/updateShare/${shareInfo.value?.id}`, 39 | method: 'PATCH', 40 | data: shareInfo.value, 41 | }) 42 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 43 | getAllShareList() 44 | } 45 | return { getAllShareList, shareList, createShare, updateShare, shareInfo, deleteShare, getShareById } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/src/composables/usePeopleReply.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@renderer/plugins/axios' 2 | import { ElMessage } from 'element-plus' 3 | import { ref } from 'vue' 4 | 5 | export default () => { 6 | const replyList = ref() 7 | const replyInfo = ref() 8 | const getAllReplyList = async () => { 9 | replyList.value = await http.request({ 10 | url: '/peopleReply/getAllReplyList', 11 | }) 12 | } 13 | const getReplyById = async (id: number) => { 14 | replyInfo.value = await http.request({ 15 | url: `/peopleReply/getReplyById/${id}`, 16 | }) 17 | } 18 | const createReply = async (data: Record) => { 19 | const res = await http.request({ 20 | url: '/peopleReply/createReply', 21 | method: 'POST', 22 | data, 23 | }) 24 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 25 | getAllReplyList() 26 | } 27 | 28 | const deleteReply = async (id: number) => { 29 | const res = await http.request({ 30 | url: `/peopleReply/deleteReply/${id}`, 31 | method: 'DELETE', 32 | }) 33 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 34 | getAllReplyList() 35 | } 36 | const updateReply = async () => { 37 | const res = await http.request({ 38 | url: `/peopleReply/updateReply/${replyInfo.value?.id}`, 39 | method: 'PATCH', 40 | data: replyInfo.value, 41 | }) 42 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 43 | getAllReplyList() 44 | } 45 | return { getAllReplyList, replyList, createReply, updateReply, replyInfo, deleteReply, getReplyById } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/src/composables/usePermission.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@renderer/plugins/axios' 2 | import { ElMessage } from 'element-plus' 3 | import { ref } from 'vue' 4 | 5 | export default () => { 6 | const userPower = ref() 7 | const pageResult = ref>() 8 | const getAllPermissions = async () => { 9 | userPower.value = await http.request({ 10 | url: '/permission/getAllPermissions', 11 | }) 12 | } 13 | const updatePermission = async () => { 14 | const res = await http.request({ 15 | url: '/permission/updatePermission', 16 | method: 'PUT', 17 | data: userPower.value, 18 | }) 19 | ElMessage({ message: res.message, type: 'success', duration: 1000 }) 20 | getAllPermissions() 21 | } 22 | const getOnePermission = (type: string) => { 23 | return http.request({ 24 | url: '/permission/getOnePermission', 25 | method: 'GET', 26 | params: { 27 | type, 28 | }, 29 | }) 30 | } 31 | const getAllLogs = async (page = 1) => { 32 | pageResult.value = await http.request>({ 33 | url: '/public/getAllLogs', 34 | method: 'GET', 35 | params: { 36 | page, 37 | }, 38 | }) 39 | } 40 | return { getAllPermissions, userPower, updatePermission, getOnePermission, getAllLogs, pageResult } 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/src/composables/useStorage.ts: -------------------------------------------------------------------------------- 1 | import { Conf } from 'electron-conf/renderer' 2 | export interface IData { 3 | data: any 4 | expire?: number 5 | } 6 | 7 | export default () => { 8 | const conf = new Conf() 9 | async function set(key: string, data: any, expire?: number): Promise { 10 | let cache: IData = { data, expire } 11 | if (expire) { 12 | cache.expire = new Date().getTime() + expire * 1000 13 | } 14 | await conf.set(key, cache) 15 | localStorage.setItem(key, JSON.stringify(cache)) 16 | } 17 | function get(key: string, defaultValue: any = null): any { 18 | const cacheStore = localStorage.getItem(key) 19 | if (cacheStore) { 20 | const cache = JSON.parse(cacheStore) 21 | const expire = cache?.expire 22 | if (expire && expire < new Date().getTime()) { 23 | localStorage.removeItem(key) 24 | return defaultValue 25 | } 26 | return cache.data 27 | } 28 | return defaultValue 29 | } 30 | function remove(key: string) { 31 | localStorage.removeItem(key) 32 | } 33 | return { set, get, remove } 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/src/config.ts: -------------------------------------------------------------------------------- 1 | import appMiddleware from '@renderer/middleware/appMiddleware' 2 | export default { 3 | middleware: [appMiddleware], 4 | router: { 5 | prefix: '', 6 | }, 7 | member: { 8 | menu: [ 9 | { 10 | routeName: 'home', 11 | title: '助手首页', 12 | }, 13 | { 14 | routeName: 'friend', 15 | title: '好友申请', 16 | }, 17 | { 18 | routeName: 'peopleReply', 19 | title: '个人回复', 20 | }, 21 | { 22 | routeName: 'groupReply', 23 | title: '群聊回复', 24 | }, 25 | { 26 | routeName: 'msgShare', 27 | title: '消息转发', 28 | }, 29 | { 30 | routeName: 'blackWord', 31 | title: '屏蔽检测', 32 | }, 33 | { 34 | routeName: 'groupMsg', 35 | title: '群发消息', 36 | }, 37 | { 38 | routeName: 'msgLogs', 39 | title: '日志列表', 40 | }, 41 | ], 42 | msgLogs: [ 43 | { 44 | routeName: 'friend', 45 | title: '好友申请', 46 | }, 47 | { 48 | routeName: 'peopleReply', 49 | title: '个人回复', 50 | }, 51 | { 52 | routeName: 'groupReply', 53 | title: '群聊回复', 54 | }, 55 | { 56 | routeName: 'msgShare', 57 | title: '消息转发', 58 | }, 59 | { 60 | routeName: 'blackWord', 61 | title: '屏蔽检测', 62 | }, 63 | { 64 | routeName: 'groupMsg', 65 | title: '群发消息', 66 | }, 67 | ], 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/src/enum/CacheEnum.ts: -------------------------------------------------------------------------------- 1 | export enum CacheEnum { 2 | TOKEN_NAME = 'login_token', 3 | WECHATY_STATUS = 'wechaty_status', 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/src/enum/HttpCodeEnum.ts: -------------------------------------------------------------------------------- 1 | export enum HttpCodeEnum { 2 | CONTINUE = 100, 3 | SWITCHING_PROTOCOLS = 101, 4 | PROCESSING = 102, 5 | EARLYHINTS = 103, 6 | OK = 200, 7 | CREATED = 201, 8 | ACCEPTED = 202, 9 | NON_AUTHORITATIVE_INFORMATION = 203, 10 | NO_CONTENT = 204, 11 | RESET_CONTENT = 205, 12 | PARTIAL_CONTENT = 206, 13 | AMBIGUOUS = 300, 14 | MOVED_PERMANENTLY = 301, 15 | FOUND = 302, 16 | SEE_OTHER = 303, 17 | NOT_MODIFIED = 304, 18 | TEMPORARY_REDIRECT = 307, 19 | PERMANENT_REDIRECT = 308, 20 | BAD_REQUEST = 400, 21 | UNAUTHORIZED = 401, 22 | PAYMENT_REQUIRED = 402, 23 | FORBIDDEN = 403, 24 | NOT_FOUND = 404, 25 | METHOD_NOT_ALLOWED = 405, 26 | NOT_ACCEPTABLE = 406, 27 | PROXY_AUTHENTICATION_REQUIRED = 407, 28 | REQUEST_TIMEOUT = 408, 29 | CONFLICT = 409, 30 | GONE = 410, 31 | LENGTH_REQUIRED = 411, 32 | PRECONDITION_FAILED = 412, 33 | PAYLOAD_TOO_LARGE = 413, 34 | URI_TOO_LONG = 414, 35 | UNSUPPORTED_MEDIA_TYPE = 415, 36 | REQUESTED_RANGE_NOT_SATISFIABLE = 416, 37 | EXPECTATION_FAILED = 417, 38 | I_AM_A_TEAPOT = 418, 39 | MISDIRECTED = 421, 40 | UNPROCESSABLE_ENTITY = 422, 41 | FAILED_DEPENDENCY = 424, 42 | PRECONDITION_REQUIRED = 428, 43 | TOO_MANY_REQUESTS = 429, 44 | INTERNAL_SERVER_ERROR = 500, 45 | NOT_IMPLEMENTED = 501, 46 | BAD_GATEWAY = 502, 47 | SERVICE_UNAVAILABLE = 503, 48 | GATEWAY_TIMEOUT = 504, 49 | HTTP_VERSION_NOT_SUPPORTED = 505 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/src/layouts/admin/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/renderer/src/layouts/admin/leftMenu.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 40 | -------------------------------------------------------------------------------- /src/renderer/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import '@renderer/assets/global.scss' 4 | import plugin from '@renderer/plugins' 5 | import '@renderer/utils' 6 | function bootstrap() { 7 | const app = createApp(App) 8 | plugin(app) 9 | app.mount('#app') 10 | } 11 | 12 | bootstrap() 13 | -------------------------------------------------------------------------------- /src/renderer/src/middleware/appMiddleware.ts: -------------------------------------------------------------------------------- 1 | import useUserStore from '@renderer/store/useUserStore' 2 | 3 | export default async () => { 4 | await Promise.all([useUserStore().getCurrentUser()]) 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/src/plugins/axios/axios.ts: -------------------------------------------------------------------------------- 1 | import useStorage from '@renderer/composables/useStorage' 2 | import { CacheEnum } from '@renderer/enum/CacheEnum' 3 | import { HttpCodeEnum } from '@renderer/enum/HttpCodeEnum' 4 | import axios, { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios' 5 | import { ElLoading, ElMessage } from 'element-plus' 6 | interface IOptions { 7 | loading?: boolean 8 | message?: boolean 9 | clearValidateError?: boolean 10 | } 11 | export default class Axios { 12 | private instance 13 | private loading: any 14 | private options: IOptions = { loading: true, message: true, clearValidateError: true } 15 | constructor(config: AxiosRequestConfig) { 16 | this.instance = axios.create(config) 17 | this.interceptors() 18 | } 19 | 20 | public async request(config: AxiosRequestConfig, options?: IOptions) { 21 | this.options = Object.assign(this.options, options ?? {}) 22 | return new Promise(async (resolve, reject) => { 23 | try { 24 | const response = await this.instance.request(config) 25 | resolve(response.data) 26 | } catch (error) { 27 | reject(error) 28 | } 29 | }) as Promise 30 | } 31 | 32 | private interceptors() { 33 | this.interceptorsRequest() 34 | this.interceptorsResponse() 35 | } 36 | 37 | private interceptorsRequest() { 38 | this.instance.interceptors.request.use( 39 | (config: InternalAxiosRequestConfig) => { 40 | if (!this.loading && this.options.loading) { 41 | this.loading = ElLoading.service({ 42 | background: 'rgba(255,255,255,0.1)', 43 | fullscreen: true, 44 | }) 45 | } 46 | config.headers.Accept = 'application/json' 47 | config.headers.Authorization = `Bearer ${useStorage().get(CacheEnum.TOKEN_NAME)}` 48 | return config 49 | }, 50 | (error: any) => { 51 | return Promise.reject(error) 52 | }, 53 | ) 54 | } 55 | private interceptorsResponse() { 56 | this.instance.interceptors.response.use( 57 | (response) => { 58 | if (this.loading) { 59 | this.loading.close() 60 | this.loading = undefined 61 | } 62 | const message = response.data?.success ?? response.data?.success 63 | if (message && this.options.message) { 64 | ElMessage({ 65 | type: 'success', 66 | message, 67 | grouping: true, 68 | duration: 2000, 69 | }) 70 | } 71 | 72 | this.options = { loading: true, message: true, clearValidateError: true } 73 | return response 74 | }, 75 | async (error) => { 76 | console.log(error) 77 | if (this.loading) { 78 | this.loading.close() 79 | this.loading = undefined 80 | } 81 | this.options = { loading: true, message: true, clearValidateError: true } 82 | const { 83 | response: { status, data }, 84 | } = error 85 | const message = data.error ?? data.message 86 | switch (status) { 87 | case HttpCodeEnum.FORBIDDEN: 88 | ElMessage({ type: 'error', message: message ?? '没有操作权限' }) 89 | break 90 | case HttpCodeEnum.NOT_FOUND: 91 | ElMessage.error('请求资源不存在') 92 | break 93 | case HttpCodeEnum.TOO_MANY_REQUESTS: 94 | ElMessage({ type: 'error', message: '请求过于频繁,请稍候再试' }) 95 | break 96 | case HttpCodeEnum.UNAUTHORIZED: 97 | ElMessage({ type: 'error', message: 'token过期,重新登录' }) 98 | break 99 | case HttpCodeEnum.UNPROCESSABLE_ENTITY: 100 | ElMessage({ type: 'error', message: '提交的内容不允许为空' }) 101 | break 102 | case HttpCodeEnum.BAD_REQUEST: 103 | ElMessage({ type: 'error', message: data.message }) 104 | break 105 | default: 106 | if (message) { 107 | ElMessage({ type: 'error', message: message ?? '服务器错误' }) 108 | } 109 | } 110 | return Promise.reject(error) 111 | }, 112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/renderer/src/plugins/axios/index.ts: -------------------------------------------------------------------------------- 1 | import Axios from './axios' 2 | 3 | const http = new Axios({ 4 | baseURL: 'https://bot.bhsr922.cn/api', 5 | timeout: 10000, 6 | }) 7 | const setup = () => {} 8 | export { http, setup } -------------------------------------------------------------------------------- /src/renderer/src/plugins/dayjs/index.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import 'dayjs/locale/zh-cn' // 导入本地化语言 3 | import relativeTime from 'dayjs/plugin/relativeTime' 4 | dayjs.locale('zh-cn') // 使用本地化语言 5 | dayjs.extend(relativeTime) 6 | 7 | const setup = () => {} 8 | export { setup } -------------------------------------------------------------------------------- /src/renderer/src/plugins/elementui/index.ts: -------------------------------------------------------------------------------- 1 | import 'element-plus/dist/index.css' 2 | import ElementPlus from 'element-plus' 3 | import { App } from 'vue' 4 | const setup = (app: App) => { 5 | app.use(ElementPlus) 6 | } 7 | export { setup } -------------------------------------------------------------------------------- /src/renderer/src/plugins/iconpark/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | const setup = (_app: App) => {} 3 | 4 | export { setup } -------------------------------------------------------------------------------- /src/renderer/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import { setup as axios } from './axios' 3 | import { setup as dayjs } from './dayjs' 4 | import { setup as elementui } from './elementui' 5 | import { setup as iconpark } from './iconpark' 6 | import { setup as tailwindcss } from './tailwindcss' 7 | import { setup as router } from './router' 8 | import { setup as pinia } from './pinia' 9 | 10 | const modules = [axios, dayjs, elementui, iconpark, tailwindcss, router, pinia] 11 | 12 | export default function register(app: App) { 13 | modules.map((setup) => setup(app)) 14 | } -------------------------------------------------------------------------------- /src/renderer/src/plugins/pinia/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 4 | const setup = (app: App) => { 5 | app.use(createPinia().use(piniaPluginPersistedstate)) 6 | } 7 | 8 | export { setup } 9 | -------------------------------------------------------------------------------- /src/renderer/src/plugins/router/guard.ts: -------------------------------------------------------------------------------- 1 | import FingerprintJS from '@fingerprintjs/fingerprintjs' 2 | import useAuth from '@renderer/composables/useAuth' 3 | import config from '@renderer/config' 4 | import { RouteLocationNormalized, Router } from 'vue-router' 5 | let isInit = false 6 | const { login, isLogin } = useAuth() 7 | export default (router: Router) => { 8 | router.beforeEach(beforeEach) 9 | } 10 | 11 | //路由守卫 12 | async function beforeEach(to: RouteLocationNormalized, _from: RouteLocationNormalized) { 13 | await init() 14 | if (to.meta.auth && !isLogin()) { 15 | const fp = await FingerprintJS.load() 16 | const result = await fp.get() 17 | await login(result.visitorId) 18 | } 19 | } 20 | 21 | //初始应用 22 | async function init() { 23 | if (isInit === true) return 24 | isInit = true 25 | await Promise.all(config.middleware.map((middleware) => middleware())) 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/plugins/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import routes from '@renderer/routes' 3 | import config from '@renderer/config' 4 | import guard from './guard' 5 | import { App } from 'vue' 6 | 7 | //添加路由前缀 8 | routes.forEach((route) => { 9 | if (!route.meta?.noPrefix) { 10 | route.path = config.router.prefix + route.path 11 | } 12 | }) 13 | 14 | const router = createRouter({ 15 | history: createWebHashHistory(), 16 | routes, 17 | scrollBehavior() { 18 | return { top: 0, behavior: 'smooth' } 19 | }, 20 | }) 21 | guard(router) 22 | const setup = (app: App) => { 23 | app.use(router) 24 | } 25 | export { setup } 26 | export default router 27 | -------------------------------------------------------------------------------- /src/renderer/src/plugins/tailwindcss/index.ts: -------------------------------------------------------------------------------- 1 | import './tailwindcss.css' 2 | import { App } from 'vue' 3 | 4 | const setup = (_app: App) => {} 5 | export { setup } -------------------------------------------------------------------------------- /src/renderer/src/plugins/tailwindcss/tailwindcss.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /src/renderer/src/routes/admin.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | export default { 4 | path: '/:any(.*)*', 5 | component: () => import('@renderer/layouts/admin/index.vue'), 6 | meta: { auth: true }, 7 | children: [ 8 | { 9 | path: '', 10 | name: 'home', 11 | component: () => import('@renderer/views/admin/home.vue'), 12 | }, 13 | { 14 | path: '/blackWord', 15 | name: 'blackWord', 16 | component: () => import('@renderer/views/admin/blackWord.vue'), 17 | }, 18 | { 19 | path: '/friend', 20 | name: 'friend', 21 | component: () => import('@renderer/views/admin/friend.vue'), 22 | }, 23 | { 24 | path: '/groupMsg', 25 | name: 'groupMsg', 26 | component: () => import('@renderer/views/admin/groupMsg.vue'), 27 | }, 28 | { 29 | path: '/groupReply', 30 | name: 'groupReply', 31 | component: () => import('@renderer/views/admin/groupReply.vue'), 32 | }, 33 | { 34 | path: '/msgLogs', 35 | name: 'msgLogs', 36 | component: () => import('@renderer/views/admin/msgLogs.vue'), 37 | }, 38 | { 39 | path: '/peopleReply', 40 | name: 'peopleReply', 41 | component: () => import('@renderer/views/admin/peopleReply.vue'), 42 | }, 43 | { 44 | path: '/msgShare', 45 | name: 'msgShare', 46 | component: () => import('@renderer/views/admin/msgShare.vue'), 47 | }, 48 | ], 49 | } as RouteRecordRaw 50 | -------------------------------------------------------------------------------- /src/renderer/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import admin from './admin' 2 | 3 | export default [admin] -------------------------------------------------------------------------------- /src/renderer/src/store/useConfig.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | export const useConfigStore = defineStore( 4 | 'config', 5 | () => { 6 | const config = ref({ 7 | userToken: '', 8 | }) 9 | return { config } 10 | }, 11 | { persist: true }, 12 | ) 13 | -------------------------------------------------------------------------------- /src/renderer/src/store/useUserStore.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@renderer/plugins/axios' 2 | import useAuth from '@renderer/composables/useAuth' 3 | import { defineStore } from 'pinia' 4 | export default defineStore('user', { 5 | state: () => { 6 | return { 7 | user: undefined as UserModel | undefined, 8 | wechatyStatus: false as boolean, 9 | } 10 | }, 11 | actions: { 12 | async getCurrentUser() { 13 | if (useAuth().isLogin()) { 14 | this.user = await http.request({ 15 | url: '/user/info', 16 | }) 17 | } 18 | }, 19 | async editStatus(status: boolean) { 20 | this.wechatyStatus = status 21 | console.log('editStatus', status,this.wechatyStatus) 22 | }, 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /src/renderer/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import './listenMain/user' -------------------------------------------------------------------------------- /src/renderer/src/utils/listenMain/user.ts: -------------------------------------------------------------------------------- 1 | import useUserStore from '@renderer/store/useUserStore' 2 | import { ElMessage } from 'element-plus' 3 | 4 | setTimeout(() => { 5 | const { editStatus } = useUserStore() 6 | window.api.userBotOut(async (_event, status: any) => { 7 | if (status == 'logOut') { 8 | await editStatus(false) 9 | ElMessage({ type: 'error', message: '机器人已退出', duration: 2000 }) 10 | } else if (status == 'login') { 11 | await editStatus(true) 12 | ElMessage({ type: 'success', message: '机器人正常运行中', duration: 2000 }) 13 | } else if (status == 'success') { 14 | ElMessage({ type: 'success', message: '已发送完毕', duration: 2000 }) 15 | } 16 | }) 17 | }, 1000) 18 | -------------------------------------------------------------------------------- /src/renderer/src/views/admin/blackWord.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 148 | 149 | 175 | -------------------------------------------------------------------------------- /src/renderer/src/views/admin/friend.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 132 | 133 | 147 | -------------------------------------------------------------------------------- /src/renderer/src/views/admin/groupMsg.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 231 | 232 | 252 | -------------------------------------------------------------------------------- /src/renderer/src/views/admin/groupReply.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 223 | 224 | 244 | -------------------------------------------------------------------------------- /src/renderer/src/views/admin/home.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 141 | 164 | -------------------------------------------------------------------------------- /src/renderer/src/views/admin/msgLogs.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/renderer/src/views/admin/msgShare.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 151 | 152 | 178 | -------------------------------------------------------------------------------- /src/renderer/src/views/admin/peopleReply.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 213 | 214 | 240 | -------------------------------------------------------------------------------- /src/renderer/types/api.d.ts: -------------------------------------------------------------------------------- 1 | //请求响应结构 2 | interface ApiData { 3 | message?: string 4 | code: number 5 | status?: 'success' | 'error' 6 | data: T 7 | } 8 | 9 | //分页请求响应结构 10 | interface ApiPage { 11 | data: T[] 12 | meta: { 13 | current_page: number 14 | page_row: number 15 | total: number 16 | total_page: number 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/types/model.d.ts: -------------------------------------------------------------------------------- 1 | interface UserModel { 2 | id: number 3 | userKey: String 4 | createdAt: string 5 | updatedAt: string 6 | } 7 | interface UserPowerModel { 8 | id: number 9 | friend: boolean 10 | peopleReply: boolean 11 | groupReply: boolean 12 | msgShare: boolean 13 | blackWord: boolean 14 | groupMsg: boolean 15 | makeApi: boolean 16 | userId: number 17 | User?: UserModel 18 | } 19 | interface FriendModel { 20 | id: number 21 | planName: string 22 | verifyMsg: string 23 | callMsg: string 24 | status: boolean 25 | userId: number 26 | User: UserModel 27 | } 28 | interface PeopleReplyModel { 29 | id: number 30 | planName: string 31 | triggerWord: string 32 | msgOne: string 33 | msgTwo: string 34 | msgThree: string 35 | status: boolean 36 | userId: number 37 | User: UserModel 38 | } 39 | interface GroupReplyModel { 40 | id: number 41 | planName: string 42 | listenRoom: string 43 | triggerWord: string 44 | msgOne: string 45 | msgTwo: string 46 | msgThree: string 47 | status: boolean 48 | userId: number 49 | User: UserModel 50 | } 51 | interface MsgShareModel { 52 | id: number 53 | planName: string 54 | listenRoom: string 55 | shareRoom: string 56 | status: boolean 57 | userId: number 58 | User: UserModel 59 | } 60 | interface GroupShareModel { 61 | id: number 62 | planName: string 63 | sendRoom: string 64 | msgOne: string 65 | msgTwo: string 66 | msgThree: string 67 | gapNum: number 68 | status: boolean 69 | userId: number 70 | User: UserModel 71 | } 72 | interface BlackWord { 73 | id: number 74 | planName: string 75 | listenRoom: string 76 | listenType: Array 77 | status: boolean 78 | userId: number 79 | User: UserModel 80 | } 81 | interface MsgModel { 82 | message: string 83 | } 84 | interface msgLogs { 85 | id: number 86 | logContent: string 87 | logType: string 88 | createdAt: Date 89 | updatedAt: Date 90 | userId: number 91 | } 92 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": ["electron.vite.config.*", "src/main/*", "src/preload/*","src/main/ipcMain/*","src/main/ipcMain/*/*","src/main/ipcMain/wechatyBot/plugins/*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["electron-vite/node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/renderer/src/env.d.ts", 5 | "src/renderer/src/**/*", 6 | "src/renderer/src/*.ts", 7 | "src/renderer/src/**/*.vue", 8 | "src/renderer/src/**/**/*.vue", 9 | "src/preload/*.d.ts", 10 | "src/renderer/types/*.d.ts", 11 | "src/renderer/public/*" 12 | ], 13 | "compilerOptions": { 14 | "composite": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@renderer/*": [ 18 | "src/renderer/src/*" 19 | ] 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------