diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..fc609593 --- /dev/null +++ b/.env.example @@ -0,0 +1,54 @@ +# 维格表配置 +VIKA_SPACE_NAME="" # 维格表空间名称,注意是名称而不是ID +VIKA_TOKEN="" #维格表token + +# Wechaty配置 +WECHATY_PUPPET="wechaty-puppet-wechat" # 可选值:wechaty-puppet-wechat4u、wechaty-puppet-wechat、wechaty-puppet-xp、wechaty-puppet-engine、wechaty-puppet-padlocal、wechaty-puppet-service +WECHATY_TOKEN="" # 使用wechaty-puppet-padlocal、wechaty-puppet-service时需配置此token + +# 基础配置 +ADMINROOM_ADMINROOMID="" # 管理群ID,与管理员群名称任选其一,群ID优先级高于群名称 +ADMINROOM_ADMINROOMTOPIC="瓦力是群主" # 管理群名称,需尽量保持名称复杂,避免重名群干扰 +BASE_WELCOM_EMESSAGE_FOR_JOIN_ROOM="" # 默认进群欢迎语 +BASE_WELCOME_MESSAGE_FOR_ADD_FRIEND="" # 默认添加好友自动回复 + +# 智能问答配置 +AUTOQA_TYPE="wxOpenai" # TBD-可选值:WxOpenai、ChatGPT +WXOPENAI_TOKEN="" # 微信对话开放平台中获取 +WXOPENAI_ENCODINGAESKEY="" # 微信对话开放平台中获取 +CHATGPT_KEY="" # openai key +CHATGPT_ENDPOINT="https://www.openai.com" # openai api请求地址,国内使用官方api可以替换为https://www.openai-proxy.com + +# MQTT配置 +MQTT_USERNAME="" # MQTT连接配置信息,推荐使用百度云的物联网核心套件 +MQTT_PASSWORD="" # MQTT连接配置信息,推荐使用百度云的物联网核心套件 +MQTT_ENDPOINT="" # MQTT连接配置信息,推荐使用百度云的物联网核心套件 +MQTT_PORT=1883 # MQTT连接配置信息,推荐使用百度云的物联网核心套件 + +# 消息推送目的地配置 +WEBHOOK_URL="" +WEBHOOK_TOKEN="" +WEBHOOK_USERNAME="" +WEBHOOK_PASSWORD="" + +# 语雀配置 +YUQUE_TOKEN="" +YUQUE_NAMESPACE="" + +# 功能开关 +AUTOQA_AUTOREPLY=true +AUTOQA_ATREPLY=false +AUTOQA_CUSTOMREPLY=false +AUTOQA_ROOMWHITELIST=true +AUTOQA_CONTACTWHITELIST=false + +VIKA_USEVIKA=false +VIKA_UPLOADMESSAGETOVIKA=false +VIKA_AUTOMATICCLOUD=false + +WEBHOOK_WEBHOOKMESSAGEPUSH=false + +MQTT_MQTTMESSAGEPUSH=false +MQTT_MQTTCONTROL=true + +IM_IMCHAT=false \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b961a14e..8427159d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,8 +1,11 @@ - const rules = { + 'multiline-ternary': 'off', } module.exports = { extends: '@chatie', + parserOptions: { + project: './tsconfig.json', // 更改为正确的路径 + }, rules, } diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 26fc7215..14228e44 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -26,7 +26,7 @@ jobs: # workflow中的job id: meta uses: docker/metadata-action@v3 # 抽取项目信息,主要是镜像的tag with: - images: atorber/wechat-openai-qa-bot + images: atorber/chatflow - name: Build and push Docker image uses: docker/build-push-action@v2 # docker build & push diff --git a/.gitignore b/.gitignore index 15458425..36673a1a 100644 --- a/.gitignore +++ b/.gitignore @@ -118,9 +118,12 @@ package-lock.json .gitpod.yml openai-qa-bot.memory-card.json quick.bat -src/config.js src/config.ts tester.js db/*.db db/*.csv db/*.xlsx +*.memory-card.json +data/db/*.db +data/table/*.xlsx +data/table/*.csv diff --git a/Dockerfile b/Dockerfile index 65e8343a..a9d4e07b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,17 @@ -FROM node:16 +# 使用Alpine Linux作为基础镜像 +FROM node:16-alpine + +# 设置工作目录 WORKDIR /usr/src/app -COPY package.json ./ +# 复制 package.json 和 package-lock.json(如果存在) +COPY package*.json ./ + +# 安装依赖 RUN npm install + +# 复制所有文件到工作目录 COPY . . -CMD [ "npm","run", "init" ] -CMD [ "npm","run", "start" ] + +# 设置默认启动命令 +CMD ["sh", "-c", "npm run init && npm run start"] \ No newline at end of file diff --git a/README.md b/README.md index 9765fd9c..43c0fac7 100644 --- a/README.md +++ b/README.md @@ -34,87 +34,53 @@ 1.下载源码并安装依赖 ```Shell -git clone -cd ./wechat-openai-qa-bot +git clone +cd ./chatflow npm install ``` 2.分别登陆[微信对话开放平台](https://openai.weixin.qq.com/)和[vika维格表](https://spcp52tvpjhxm.com.vika.cn/?inviteCode=55152973)官网注册账号并获取token -3.在电脑上登陆微信,微信版本必须为[WeChatSetup-v3.6.0.18.exe](https://github.com/tom-snow/wechat-windows-versions/releases/download/v3.6.0.18/WeChatSetup-3.6.0.18.exe) +3.重命名.env.example文件为.env并修改配置文件 -4.修改./config.js配置文件 +> 快速开始仅需要修改VIKA_TOKEN、VIKA_SPACE_NAME、ADMINROOM_ADMINROOMTOPIC配置项,其他配置项暂时无需修改,使用微信对话开放平台时配置WXOPENAI_TOKEN、WXOPENAI_ENCODINGAESKEY -快速开始仅需要修改VIKA_TOKEN、VIKA_SPACENAME配置项,其他配置项暂时无需修改 +```.env +# 维格表配置 +VIKA_SPACE_NAME="" # 维格表空间名称,注意是名称而不是ID +VIKA_TOKEN="" #维格表token -```javascript -/* eslint-disable sort-keys */ -// 配置文件,所有配置必须齐全,补充空白配置项,其他配置项可按需要修改 -const configs = { - VIKA_TOKEN: '替换成自己的维格表token', // VIKA维格表token - VIKA_SPACENAME: '替换成你的维格表空间名称', // VIKA维格表空间名称,修改为自己的空间名称 -} +# 基础配置 +ADMINROOM_ADMINROOMTOPIC="瓦力是群主" # 管理群名称,需尽量保持名称复杂,避免重名群干扰 -export default configs +# 智能问答配置 +WXOPENAI_TOKEN="" # 微信对话开放平台中获取 +WXOPENAI_ENCODINGAESKEY="" # 微信对话开放平台中获取 ``` > 只有加入到roomWhiteList里的群才会开启只能问答机器人 -5.初始化系统表,先运行,系统会自动在维格表中创建好初始化表格 +4.启动程序 ```Shell -npm run sys-init -``` - -在维格表查看系统表是否创建成功 - -6.程序默认使用wechaty-puppet-wechat,三大系统均可使用 - -7.启动程序 - -```Shell -npm start +npm run start ``` 出现二维码之后,扫码二维码登陆微信 -8.开启智能问答功能 +5.开启智能问答功能 -8.1 设置微信对话平台token,填写"环境变量"表中的 【对话平台token】、【对话平台EncodingAESKey】并在"功能开关"表中开启智能问答 +5.1 设置微信对话平台token,填写"环境变量"表中的 【对话平台token】、【对话平台EncodingAESKey】并在"功能开关"表中开启智能问答 添加一个简单问题到微信对话开放平台,测试对应群内智能问答内容 -8.2 如果不希望每个群都开启智能问答,需设置群白名单,首先需要将上图中的群白名单开关设置为开启 +5.2 如果不希望每个群都开启智能问答,需设置群白名单,首先需要将上图中的群白名单开关设置为开启 然后将群加入到问答白名单,在“群白名单”表中,加入需要开启的群ID(roomid),群ID在消息中查看(在群里发一条消息,然后控制台查看或在维格表中查找) 详细操作参考 [手把手教程](https://www.yuque.com/atorber/oegota/zm4ulnwnqp9whmd6) -8.4 重启程序,在指定群测试问答 - -## 使用环境变量启动 - -> 也可以不使用配置文件,通过配置环境变量启动 - -Mac、Linux操作系统下运行(仅支持使用wechaty-puppet-wechat和wechaty-puppet-padlocal) - -```Shell -export VIKA_TOKEN="替换成自己的维格表token" -export VIKA_SPACENAME="替换成你的维格表空间名称" -npm run sys-init -npm start -``` - -Windows操作系统下运行(支持使用wechaty-puppet-xp、wechaty-puppet-wechat、wechaty-puppet-padlocal) - -推荐使用 wechaty-puppet-xp - -```Shell -set VIKA_TOKEN="替换成自己的维格表token" -set VIKA_SPACENAME="替换成你的维格表空间名称" -npm run sys-init -npm run start -``` +5.3 重启程序,在指定群测试问答 ## 在Docker中部署运行 @@ -135,23 +101,13 @@ npm run start ### 拉取和运行 -- 稳定版本 - -```Shell -docker run -d ---restart=always ---env VIKA_TOKEN="维格表token" ---env VIKA_SPACENAME="维格表空间名称" -atorber/wechat-openai-qa-bot:v1.8.2 -``` - - 最新版本 ```Shell docker run -d --restart=always --env VIKA_TOKEN="维格表token" ---env VIKA_SPACENAME="维格表空间名称" +--env VIKA_SPACE_NAME="维格表空间名称" atorber/wechat-openai-qa-bot:latest ``` @@ -214,3 +170,7 @@ atorber/wechat-openai-qa-bot:latest - [vue-im](https://github.com/polk6/vue-im) 由@polk6开源的客服web项目,实现客服后台回复咨询消息 + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=choogoo/chatflow&type=Date)](https://star-history.com/#choogoo/chatflow&Date) diff --git a/auto-macos.command b/auto-macos.command deleted file mode 100644 index e69de29b..00000000 diff --git a/auto-windowns.bat b/auto-windowns.bat deleted file mode 100644 index c33e8ec7..00000000 --- a/auto-windowns.bat +++ /dev/null @@ -1,19 +0,0 @@ -@echo off - -echo "killing node.exe ..." - -taskkill /f /im node.exe - -echo "node.exe was killed successfully." - -echo "it will continue to start node.exe in 3 sec ..." - -@ping 127.0.0.1 -n 3 >nul - -cd /d %~dp0 - -npm run start - -echo "node.exe was started successfully." - -exit /b diff --git a/cache/a.md b/cache/a.md deleted file mode 100644 index e69de29b..00000000 diff --git a/data/config.json b/data/config.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/data/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 2c544ddf..32ed89b2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -80,14 +80,14 @@ npm install 4. 修改./config.js配置文件 -快速开始仅需要修改VIKA_TOKEN、VIKA_SPACENAME配置项,其他配置项暂时无需修改 +快速开始仅需要修改VIKA_TOKEN、VIKA_SPACE_NAME配置项,其他配置项暂时无需修改 ``` /* eslint-disable sort-keys */ // 配置文件,所有配置必须齐全,补充空白配置项,其他配置项可按需要修改 const configs = { VIKA_TOKEN: '替换成自己的维格表token', // VIKA维格表token - VIKA_SPACENAME: '替换成你的维格表空间名称', // VIKA维格表空间名称,修改为自己的空间名称 + VIKA_SPACE_NAME: '替换成你的维格表空间名称', // VIKA维格表空间名称,修改为自己的空间名称 } export default configs @@ -155,7 +155,7 @@ Mac、Linux操作系统下运行(仅支持使用wechaty-puppet-wechat和wechaty- ``` export VIKA_TOKEN="替换成自己的维格表token" -export VIKA_SPACENAME="替换成你的维格表空间名称" +export VIKA_SPACE_NAME="替换成你的维格表空间名称" npm run sys-init npm start ``` @@ -166,7 +166,7 @@ Windows操作系统下运行(支持使用wechaty-puppet-xp、wechaty-puppet-wech ``` set VIKA_TOKEN="替换成自己的维格表token" -set VIKA_SPACENAME="替换成你的维格表空间名称" +set VIKA_SPACE_NAME="替换成你的维格表空间名称" npm run sys-init npm run start ``` @@ -280,7 +280,7 @@ npm install // 配置文件,所有配置必须齐全,补充空白配置项,其他配置项可按需要修改 const configs = { VIKA_TOKEN: '替换成自己的维格表token', // VIKA维格表token - VIKA_SPACENAME: '替换成你的维格表空间名称', // VIKA维格表空间名称,修改为自己的空间名称 + VIKA_SPACE_NAME: '替换成你的维格表空间名称', // VIKA维格表空间名称,修改为自己的空间名称 } export default configs diff --git a/download-install-and-run.command b/download-install-and-run-linux.sh old mode 100755 new mode 100644 similarity index 100% rename from download-install-and-run.command rename to download-install-and-run-linux.sh diff --git a/download-install-and-run.sh b/download-install-and-run-mac.command old mode 100644 new mode 100755 similarity index 100% rename from download-install-and-run.sh rename to download-install-and-run-mac.command diff --git a/download-install-and-run.bat b/download-install-and-run-windows.bat similarity index 100% rename from download-install-and-run.bat rename to download-install-and-run-windows.bat diff --git a/example/ding-dong-bot.ts b/example/ding-dong-bot.ts new file mode 100644 index 00000000..e659fa43 --- /dev/null +++ b/example/ding-dong-bot.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import 'dotenv/config.js' +import { + log, + WechatyBuilder, +} from 'wechaty' + +import { ChatFlow, config, getBotOps } from '../src/chatflow.js' + +// 构建机器人 +const ops = getBotOps(config.botConfig.wechaty.puppet, config.botConfig.wechaty.token) +const bot = WechatyBuilder.build(ops) + +bot.use(ChatFlow(config)) +bot.start() + .then(() => log.info('\n================================\n\n机器人启动,如出现二维码,请使用微信扫码登录\n\n================================')) + .catch((e: any) => log.error('机器人运行异常:', JSON.stringify(e))) diff --git a/package.json b/package.json index 86f6578e..65feadf0 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,69 @@ { "name": "chatflow", - "version": "1.11.3", - "description": "openai-qa-bot", + "version": "2.0.2", + "description": "ChatFlow-聊天机器人管理平台", "type": "module", + "exports": { + ".": { + "import": "./dist/esm/src/chatflow.js", + "require": "./dist/cjs/src/chatflow.js" + } + }, + "typings": "./dist/esm/src/chatflow.d.ts", "engines": { "node": ">=16", "npm": ">=7" }, "scripts": { - "start": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/bot.ts", + "start": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./example/ding-dong-bot.ts", + "ding": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./example/ding-dong-bot.ts", + "build": "tsc && tsc -p tsconfig.cjs.json", + "clean": "shx rm -fr dist/*", + "dist": "npm-run-all clean build dist:commonjs", + "dist:commonjs": "jq -n \"{ type: \\\"commonjs\\\" }\" > dist/cjs/package.json", + "test": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/test.ts", "start:index": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/index.ts", "start:notls": "cross-env WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT=true NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/bot.ts", "start:store": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/plugins/store-messages-locally.ts", "start:meet": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/plugins/store-messages-locally.ts", - "sys-init": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/init.ts", - "init": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/init.ts", - "checker": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/puppet-checker.ts", "rm-temp": "rm -r temp; mkdir temp", "rm-cache": "rm -r cache; mkdir cache", "lint": "npm run lint:es && npm run lint:ts && npm run lint:md", "lint:md": "markdownlint README.md", "lint:ts": "tsc --isolatedModules --noEmit", - "lint:es": "eslint \"src/**/*.ts\" \"tests/**/*.spec.ts\" --ignore-pattern tests/fixtures/", - "lint-fix": "eslint --fix \"src/**/*.ts\"" + "lint:es": "eslint \"src/**/*.ts\" --ignore-pattern tests/fixtures/", + "lint-fix": "eslint --fix \"src/**/*.ts\" \"src/**/*.js\"" }, "repository": { "type": "git", - "url": "git+https://github.com/atorber/wechaty-wx-openai-link.git" + "url": "git+https://github.com/choogoo/chatflow.git" }, "keywords": [], "author": "atorber ", "license": "Apache-2.0", "bugs": { - "url": "https://github.com/atorber/wechaty-wx-openai-link/issues" + "url": "https://github.com/choogoo/chatflow/issues" }, - "homepage": "https://github.com/atorber/wechaty-wx-openai-link#readme", + "homepage": "https://github.com/choogoo/chatflow#readme", "dependencies": { "@vikadata/vika": "^1.0.5", + "@yuque/sdk": "^1.1.1", + "api2d": "^0.1.23", "axios": "^1.4.0", - "chatgpt": "^1.1.3", + "chatgpt": "^1.4.0", "dotenv": "^16.0.0", "exceljs": "^4.3.0", "fast-csv": "^4.3.6", "file-box": "^1.4.12", "fs": "^0.0.1-security", "html-to-docx": "^1.8.0", - "install": "^0.13.0", + "language-monitor": "^1.0.3", "moment": "^2.29.1", "mqtt": "^4.3.7", + "nedb": "^1.8.0", "nedb-promises": "^6.2.1", "njwt": "^1.2.0", - "node-schedule": "^2.1.0", + "node-schedule": "^2.1.1", "node-xlsx": "^0.21.0", "npm": "^9.6.4", "openai-sdk": "^1.0.1", @@ -65,6 +78,7 @@ "wechaty-puppet-engine": "^1.0.20", "wechaty-puppet-padlocal": "^1.11.13", "wechaty-puppet-service": "^1.19.8", + "wechaty-puppet-wechat": "^1.13.15", "wechaty-puppet-wechat4u": "^1.13.15", "xlsx": "^0.18.5" }, @@ -73,7 +87,10 @@ "@chatie/git-scripts": "^0.6.2", "@chatie/tsconfig": "^4.6.3", "@types/jest": "^29.2.3", + "@types/mustache": "^4.2.2", + "@types/nedb": "^1.8.13", "@types/node": "^18.11.9", + "@types/node-schedule": "^2.1.0", "@types/qrcode-terminal": "^0.12.0", "@types/request-promise": "^4.1.48", "check-node-version": "^4.2.1", @@ -81,5 +98,13 @@ "is-pr": "^2.0.0", "ts-node": "^10.9.1", "typescript": "^4.9.3" + }, + "files": [ + "bin/", + "dist/", + "src/" + ], + "publishConfig": { + "tag": "next" } } diff --git a/src/api/config.js b/src/api/config.ts similarity index 50% rename from src/api/config.js rename to src/api/config.ts index 69e507f3..ca643faf 100644 --- a/src/api/config.js +++ b/src/api/config.ts @@ -1,77 +1,77 @@ import nedb from '../db/nedb.js' -const cdb = nedb() +const cdb:any = nedb('./config.db') /** * 添加配置文件 * @param {*} config */ -async function addConfig(info) { +async function addConfig (info:any) { try { - let doc = await cdb.insert(info) + const doc = await cdb.insert(info) return doc } catch (error) { - console.log('插入数据错误', error) + // console.log('插入数据错误', error) } } /** * 更新配置文件 * @param {*} config */ -async function updateConfig(config) { +async function updateConfig (config: { id: any }) { try { - let res = await allConfig() + const res = await allConfig() if (res) { - let up = await cdb.update({ id: config.id }, config) + const up = await cdb.update({ id: config.id }, config) return up } else { - let add = await addConfig(config) + const add = await addConfig(config) return add } } catch (error) { - console.log('配置文件更新失败', error) + // console.log('配置文件更新失败', error) } } /** * 获取所有配置 */ -async function allConfig() { +async function allConfig () { try { - let search = await cdb.find() + const search = await cdb.find() return search[0] } catch (error) { - console.log('查询数据错误', error) + // console.log('查询数据错误', error) } } /** * 每日任务 */ -async function dayTaskSchedule() { +async function dayTaskSchedule () { try { - let res = await cdb.find({}) + const res = await cdb.find({}) return res[0].dayTaskSchedule } catch (error) { - console.log('获取每日任务', error) + // console.log('获取每日任务', error) } } /** * 群资讯 */ -async function roomNewsSchedule() { +async function roomNewsSchedule () { try { - let res = await cdb.find.find({}) + const res = await cdb.find.find({}) return res[0].roomNewsSchedule } catch (error) { - console.log('获取每日任务', error) + // console.log('获取每日任务', error) } } /** * 群任务 */ -async function roomTaskSchedule() { +async function roomTaskSchedule () { try { - let res = await cdb.find.find({}) + const res = await cdb.find.find({}) return res[0].roomTaskSchedule } catch (error) { - console.log('获取每日任务', error) + // console.log('获取每日任务', error) } } export { addConfig } @@ -82,9 +82,9 @@ export { roomNewsSchedule } export { roomTaskSchedule } export default { addConfig, - updateConfig, allConfig, dayTaskSchedule, roomNewsSchedule, roomTaskSchedule, + updateConfig, } diff --git a/src/api/contact.js b/src/api/contact.js deleted file mode 100644 index 98757ed5..00000000 --- a/src/api/contact.js +++ /dev/null @@ -1,24 +0,0 @@ -import nedb from '../db/nedb.js' -const db = nedb() -async function addUser(info) { - try { - let doc = await db.insert(info) - return doc - } catch (error) { - console.log('插入数据错误', error) - } -} -async function getUser() { - try { - let search = await db.find({}) - return search[0] - } catch (error) { - console.log('查询数据错误', error) - } -} -export { addUser } -export { getUser } -export default { - addUser, - getUser, -} diff --git a/src/api/contact.ts b/src/api/contact.ts new file mode 100644 index 00000000..9a89e90f --- /dev/null +++ b/src/api/contact.ts @@ -0,0 +1,25 @@ +import nedb from '../db/nedb.js' + +const db:any = nedb('./contact.db') +async function addUser (info:any) { + try { + const doc = await db.insert(info) + return doc + } catch (error) { + // console.log('插入数据错误', error) + } +} +async function getUser () { + try { + const search = await db.find({}) + return search[0] + } catch (error) { + // console.log('查询数据错误', error) + } +} +export { addUser } +export { getUser } +export default { + addUser, + getUser, +} diff --git a/src/api/message.ts b/src/api/message.ts new file mode 100644 index 00000000..1f5f4c68 --- /dev/null +++ b/src/api/message.ts @@ -0,0 +1,36 @@ +import { + Message, + log, +} from 'wechaty' + +import { db } from '../db/tables.js' +const messageData = db.message + +export const addMessage = async (message: Message) => { + const talker = message.talker() + const listener = message.listener() + const room = message.room() + let roomJson:any + if (room) { + roomJson = JSON.parse(JSON.stringify(room)) + delete roomJson.payload.memberIdList + } + + const messageNew = { + _id: message.id, + data: message, + listener:listener ?? undefined, + room:roomJson, + talker, + } + // log.info('addMessage messageNew:', JSON.stringify(messageNew)) + try { + const res:any = await messageData.insert(messageNew) + log.info('消息写入数据库成功:', res._id) + return true + } catch (e) { + log.error('消息写入数据库失败:', e) + return false + } + +} diff --git a/src/api/room.js b/src/api/room.ts similarity index 57% rename from src/api/room.js rename to src/api/room.ts index 94769803..0e8547b1 100644 --- a/src/api/room.js +++ b/src/api/room.ts @@ -1,15 +1,15 @@ import nedb from '../db/nedb.js' -import path from "path"; -import os from "os"; +import path from 'path' +import os from 'os' const baseDir = path.join( os.homedir(), path.sep, - ".wechaty", - "wechaty-panel-cache", + '.wechaty', + 'wechaty-panel-cache', path.sep, -); +) const dbpath = baseDir + 'room.db' -const rdb = nedb(dbpath) +const rdb:any = nedb(dbpath) /** * 记录群聊天记录 记录格式 @@ -17,12 +17,13 @@ const rdb = nedb(dbpath) * @param info * @returns {Promise} */ -export async function addRoomRecord(info) { +export async function addRoomRecord (info:any): Promise { try { - let doc = await rdb.insert(info) - return doc + await rdb.insert(info) + return true } catch (error) { - console.log('插入数据错误', error) + // console.log('插入数据错误', error) + return false } } @@ -31,12 +32,12 @@ export async function addRoomRecord(info) { * @param room * @returns {Promise<*>} */ -export async function getRoomRecord(roomName) { +export async function getRoomRecord (roomName:any): Promise { try { - let search = await rdb.find({roomName}) + const search = await rdb.find({ roomName }) return search } catch (error) { - console.log('查询数据错误', error) + // console.log('查询数据错误', error) } } @@ -45,12 +46,12 @@ export async function getRoomRecord(roomName) { * @param roomName * @returns {Promise} */ -export async function removeRecord(roomName) { +export async function removeRecord (roomName:any): Promise { try { - let search = await rdb.remove({roomName}, {multi: true}) + const search = await rdb.remove({ roomName }, { multi: true }) return search } catch (e) { - console.log("error", e); + // console.log('error', e) } } @@ -60,18 +61,18 @@ export async function removeRecord(roomName) { * @param day 取的天数 * @returns {Promise<*>} */ -export async function getRoomRecordContent(rooName, day) { +export async function getRoomRecordContent (rooName:any, day:any): Promise { try { let list = await getRoomRecord(rooName) - list = list.filter(item=> { + list = list.filter((item:any) => { return item.time >= new Date().getTime() - day * 24 * 60 * 60 * 1000 }) let word = '' - list.forEach((item)=> { + list.forEach((item:any) => { word = word + item.content }) return word } catch (e) { - console.log("error", e); + // console.log('error', e) } } diff --git a/src/bot.ts b/src/bot.ts deleted file mode 100644 index 15259954..00000000 --- a/src/bot.ts +++ /dev/null @@ -1,656 +0,0 @@ -#!/usr/bin/env -S node --no-warnings --loader ts-node/esm -import 'dotenv/config.js' -// import fs from 'fs' -import { - Contact, - Message, - ScanStatus, - log, - // Room, - types, - Wechaty, - WechatyBuilder, -} from 'wechaty' - -import qrcodeTerminal from 'qrcode-terminal' -import { FileBox } from 'file-box' -import { createWriteStream } from 'fs' -import XLSX from 'xlsx' -import csv from 'fast-csv' -import { - VikaBot, - configData, - sendMsg, - sendNotice, - imclient, - wxai, - ChatDevice, - propertyMessage, - eventMessage, - -} from './plugins/index.js' -import { baseConfig, config } from './config.js' -import { - waitForMs as wait, - formatSentMessage, -} from './util/tool.js' -import schedule from 'node-schedule' -import { db } from './db/tables.js' - -log.info('db:', db) -log.info('config:', JSON.stringify(config)) -// log.info('process.env', JSON.stringify(process.env)) - -let bot: Wechaty -let sysConfig: any -let chatdev: any = {} -let job: any -let jobs: any -let vika: any -let socket: any = {} - -baseConfig['VIKA_TOKEN'] = baseConfig['VIKA_TOKEN'] || process.env['VIKA_TOKEN'] || '' -baseConfig['VIKA_SPACENAME'] = baseConfig['VIKA_SPACENAME'] || process.env['VIKA_SPACENAME'] || '' - -// log.info(baseConfig) - -const vikaConfig = { - spaceName: baseConfig['VIKA_SPACENAME'], - token: baseConfig['VIKA_TOKEN'], -} -// log.info(vikaConfig) - -function getBot (sysConfig: any) { - const ops:any = { - name: 'qa-bot', - puppet: sysConfig.puppetName, - puppetOptions: { - token: sysConfig.puppetToken || 'null', - }, - } - - log.info(ops) - - if (sysConfig.puppetName === 'wechaty-puppet-service') { - process.env['WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT'] = 'true' - } - - if (sysConfig.puppetName === 'wechaty-puppet-wechat4u' || sysConfig.puppetName === 'wechaty-puppet-xp' || sysConfig.puppetName === 'wechaty-puppet-engine') { - delete ops.puppetOptions.token - } - - if (sysConfig.puppetName === 'wechaty-puppet-wechat') { - delete ops.puppetOptions.token - ops.puppetOptions.uos = true - } - - log.info('bot ops:', JSON.stringify(getBot)) - - const bot = WechatyBuilder.build(ops) - return bot -} - -function getNow () { - return new Date().toLocaleString() -} - -function checkConfig (configs: { [key: string]: any }) { - const missingConfiguration = [] - - for (const key in configs) { - if (!configs[key] && ![ 'imOpen', 'DIFF_REPLY_ONOFF' ].includes(key)) { - missingConfiguration.push(key) - } - } - - if (missingConfiguration.length > 0) { - log.error('\n======================================\n\n', `错误提示:\n缺少${missingConfiguration.join()}配置参数,请检查环境变量表\n\n======================================`) - log.info('bot configs:', configs) - return true - } - return true -} - -async function relpy (bot:Wechaty, vika:any, replyText:string, message:Message) { - await message.say(replyText) - vika.addRecord(await formatSentMessage(bot.currentUser, replyText, message.room() ? undefined : message.talker(), message.room())) -} - -async function exportContactsAndRoomsToCSV () { - // 获取所有联系人和群聊 - const contacts = await bot.Contact.findAll() - const rooms = await bot.Room.findAll() - - // 准备CSV数据 - const csvData = [] - contacts.forEach((contact:Contact) => { - if (contact.friend()) { - csvData.push({ ID: contact.id, Name:Buffer.from(contact.name(), 'utf-8').toString() || '未知', Type:'Contact' }) - } - }) - - for (const room of rooms) { - csvData.push({ ID:room.id, Name:Buffer.from(await room.topic(), 'utf-8').toString() || '未知', Type:'Room' }) - } - - log.info('通讯录原始数据:', csvData) - - const fileName = './db/contacts_and_rooms.csv' - const writeStream = createWriteStream(fileName) - const csvStream = csv.format({ headers: true }) - csvStream.pipe(writeStream).on('end', () => { - log.info('CSV file written successfully') - }) - - csvData.forEach((item) => { - csvStream.write(item) - }) - - csvStream.end() - - // 返回FileBox对象 - return FileBox.fromFile(fileName) -} - -async function exportContactsAndRoomsToXLSX () { - // 获取所有联系人和群聊 - const contacts = await bot.Contact.findAll() - const rooms = await bot.Room.findAll() - - // 准备联系人和群聊数据 - const contactsData = [ [ 'Name', 'ID' ] ] - const roomsData = [ [ 'Name', 'ID' ] ] - contacts.forEach((contact) => { - if (contact.friend()) { - contactsData.push([ contact.name(), contact.id ]) - } - }) - - for (const room of rooms) { - roomsData.push([ await room.topic(), room.id ]) - } - - // 创建一个新的工作簿 - const workbook = XLSX.utils.book_new() - - // 将数据添加到工作簿的不同sheet中 - const contactsSheet = XLSX.utils.aoa_to_sheet(contactsData) - const roomsSheet = XLSX.utils.aoa_to_sheet(roomsData) - XLSX.utils.book_append_sheet(workbook, contactsSheet, 'Contacts') - XLSX.utils.book_append_sheet(workbook, roomsSheet, 'Rooms') - - // 将工作簿写入文件 - const fileName = './db/contacts_and_rooms.xlsx' - XLSX.writeFile(workbook, fileName) - - // 返回FileBox对象 - return FileBox.fromFile(fileName) -} - -async function updateJobs (bot: Wechaty, vika:any) { - try { - const tasks = await vika.getTimedTask() - schedule.gracefulShutdown() - jobs = {} - // log.info(tasks) - for (let i = 0; i < tasks.length; i++) { - const task: any = tasks[i] - if (task.active) { - const curTimeF = new Date(task.time) - // const curTimeF = new Date(task.time+8*60*60*1000) - let curRule = '* * * * * *' - let dayOfWeek: any = '*' - let month: any = '*' - let dayOfMonth: any = '*' - let hour: any = curTimeF.getHours() - let minute: any = curTimeF.getMinutes() - const second = 0 - const addMonth = [] - switch (task.cycle) { - case '每季度': - month = curTimeF.getMonth() - for (let i = 0; i < 4; i++) { - if (month + 3 <= 11) { - addMonth.push(month) - } else { - addMonth.push(month - 9) - } - month = month + 3 - } - month = addMonth - break - case '每天': - break - case '每周': - dayOfWeek = curTimeF.getDay() - break - case '每月': - month = curTimeF.getMonth() - break - case '每小时': - hour = '*' - break - case '每30分钟': - hour = '*' - minute = [ 0, 30 ] - break - case '每15分钟': - hour = '*' - minute = [ 0, 15, 30, 45 ] - break - case '每10分钟': - hour = '*' - minute = [ 0, 10, 20, 30, 40, 50 ] - break - case '每5分钟': - hour = '*' - minute = [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 ] - break - case '每分钟': - hour = '*' - minute = '*' - break - default: - month = curTimeF.getMonth() - dayOfMonth = curTimeF.getDate() - break - - } - curRule = `${second} ${minute} ${hour} ${dayOfMonth} ${month} ${dayOfWeek}` - log.info(curRule) - - try { - schedule.scheduleJob(task.id, curRule, async () => { - try { - const curDate = new Date() - log.info('定时任务:', curTimeF, curRule, curDate, JSON.stringify(task)) - // await user.say('心跳:' + curDate) - - try { - if (task.contacts.length) { - const contact = await bot.Contact.find({ id: task.contacts[0] }) - if (contact) { - await contact.say(task.msg) - vika.addRecord(await formatSentMessage(bot.currentUser, task.msg, contact, undefined)) - await wait(200) - } - } - } catch (e) { - log.error('发送好友定时任务失败:', e) - } - - try { - if (task.rooms.length) { - const room = await bot.Room.find({ id: task.rooms[0] }) - if (room) { - await room.say(task.msg) - vika.addRecord(await formatSentMessage(bot.currentUser, task.msg, undefined, room)) - await wait(200) - } - } - } catch (e) { - log.error('发送群定时任务失败:', e) - - } - - } catch (err) { - log.error('定时任务执行失败:', err) - } - }) - jobs[task.id] = task - } catch (e) { - log.error('创建定时任务失败:', e) - } - } - } - log.info('通知提醒任务初始化完成,创建任务数量:', Object.keys(jobs).length) - - } catch (err: any) { - log.error('更新通知提醒列表任务失败:', err) - } -} - -async function onScan (qrcode: string, status: ScanStatus) { - // 上传二维码到维格表,可通过扫码维格表中二维码登录 - await vika.onScan(qrcode, status) - - // 控制台显示二维码 - if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) { - const qrcodeUrl = encodeURIComponent(qrcode) - const qrcodeImageUrl = [ - 'https://wechaty.js.org/qrcode/', - qrcodeUrl, - ].join('') - log.info('StarterBot', 'onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl) - qrcodeTerminal.generate(qrcode, { small: true }) // show qrcode on console - - } else { - log.info('StarterBot', 'onScan: %s(%s)', ScanStatus[status], status) - } -} - -async function onLogin (user: Contact) { - log.info('StarterBot', '%s login', user) - log.info('当前登录的账号信息:', JSON.stringify(user)) - - // 启动MQTT通道 - if (sysConfig.mqttPassword && (sysConfig.mqtt_SUB_ONOFF || sysConfig.mqtt_PUB_ONOFF)) { - chatdev = new ChatDevice(sysConfig.mqttUsername, sysConfig.mqttPassword, sysConfig.mqttEndpoint, sysConfig.mqttPort, user.id) - if (sysConfig.mqtt_SUB_ONOFF) { - chatdev.init(bot) - } - } - - const curDate = new Date().toLocaleString() - // await user.say('上线:' + curDate) - - // 更新云端好友和群 - await vika.updateRooms(bot) - await vika.updateContacts(bot) - - // 如果开启了MQTT推送,心跳同步到MQTT,每30s一次 - setInterval(() => { - try { - log.info(curDate) - if (chatdev && sysConfig.mqtt_PUB_ONOFF) { - chatdev.pub_property(propertyMessage('lastActive', curDate)) - } - } catch (err) { - log.error('发送心跳失败:', err) - } - }, 300000) - - // 启动用户定时通知提醒任务 - await updateJobs(bot, vika) - log.info('================================================\n\n登录启动成功,程序准备就绪\n\n================================================\n') -} - -async function onReady () { - const user: Contact = bot.currentUser - log.info('StarterBot', '%s ready', user) - log.info('当前登录的账号信息:', JSON.stringify(user)) - - // 启动MQTT通道 - if (sysConfig.mqttPassword && (sysConfig.mqtt_SUB_ONOFF || sysConfig.mqtt_PUB_ONOFF)) { - chatdev = new ChatDevice(sysConfig.mqttUsername, sysConfig.mqttPassword, sysConfig.mqttEndpoint, sysConfig.mqttPort, user.id) - if (sysConfig.mqtt_SUB_ONOFF) { - chatdev.init(bot) - } - } - - const curDate = new Date().toLocaleString() - // await user.say('上线:' + curDate) - - // 更新云端好友和群 - await vika.updateRooms(bot) - await vika.updateContacts(bot) - - // 如果开启了MQTT推送,心跳同步到MQTT,每30s一次 - setInterval(() => { - try { - log.info(curDate) - if (chatdev && sysConfig.mqtt_PUB_ONOFF) { - chatdev.pub_property(propertyMessage('lastActive', curDate)) - } - } catch (err) { - log.error('发送心跳失败:', err) - } - }, 300000) - - // 启动用户定时通知提醒任务 - await updateJobs(bot, vika) - log.info('================================================\n\n登录启动成功,程序准备就绪\n\n================================================\n') -} - -function onLogout (user: Contact) { - log.info('StarterBot', '%s logout', user) - job.cancel() -} - -async function onMessage (message: Message) { - // log.info('onMessage', JSON.stringify(message)) - await vika.onMessage(message) - const curDate = new Date().toLocaleString() - - // MQTT上报 - if (chatdev && sysConfig.mqtt_PUB_ONOFF) { - /* - 将消息通过mqtt通道上报到云端 - */ - // chatdev.pub_message(message) - chatdev.pub_event(eventMessage('onMessage', { curDate })) - - } - - const talker = message.talker() - const text = message.text() - const room = message.room() - const roomId = room?.id - const topic = await room?.topic() - const keyWord = bot.currentUser.name() - const isSelfMsg = message.self() - log.info('keyWord is:', keyWord) - if (isSelfMsg) { - await sendNotice(bot, message) - } - - let replyText: string = '' - if (isSelfMsg && (text === '#指令列表' || text === '#帮助')) { - replyText = `操作指令说明:\n - #更新配置 更新全部配置 - #更新提醒 更新定时提醒任务 - #更新通讯录 更新维格表通信录 - #下载通讯录 下载通讯录xlsx表 - #下载通知模板 下载通知模板` - - await relpy(bot, vika, replyText, message) - } - - if (isSelfMsg && text === '#更新配置') { - log.info('热更新系统配置~') - try { - sysConfig = await vika.getConfig() - // message.say('配置更新成功:' + JSON.stringify(newConfig)) - log.info('newConfig', sysConfig) - replyText = '配置更新成功~' - } catch (e) { - replyText = '配置更新成功~' - } - - await relpy(bot, vika, getNow() + replyText, message) - } - - if (isSelfMsg && text === '#更新提醒') { - log.info('热更新通知任务~') - try { - await updateJobs(bot, vika) - replyText = '提醒任务更新成功~' - } catch (e) { - replyText = '提醒任务更新失败~' - } - - await relpy(bot, vika, getNow() + replyText, message) - } - - if (isSelfMsg && text === '#更新通讯录') { - log.info('热更新通讯录到维格表~') - try { - await vika.updateContacts(bot) - await vika.updateRooms(bot) - replyText = '通讯录更新成功~' - } catch (e) { - replyText = '通讯录更新失败~' - } - - await relpy(bot, vika, getNow() + replyText, message) - } - - if (isSelfMsg && text === '#下载csv通讯录') { - log.info('下载通讯录到csv表~') - try { - const fileBox = await exportContactsAndRoomsToCSV() - await message.say(fileBox) - } catch (err) { - log.error('exportContactsAndRoomsToCSV', err) - await message.say('下载失败~') - } - } - - if (isSelfMsg && text === '#下载通讯录') { - log.info('下载通讯录到xlsx表~') - try { - const fileBox = await exportContactsAndRoomsToXLSX() - await message.say(fileBox) - } catch (err) { - log.error('exportContactsAndRoomsToXLSX', err) - } - } - - if (isSelfMsg && text === '#下载通知模板') { - log.info('下载通知模板~') - try { - const fileBox = FileBox.fromFile('./src/templates/群发通知模板.xlsx') - await message.say(fileBox) - } catch (err) { - log.error('下载模板失败', err) - await message.say('下载失败,请重试~') - } - } - - try { - - if (room && roomId && !isSelfMsg) { - - // 智能问答开启时执行 - if (sysConfig.WX_OPENAI_ONOFF && ((text.indexOf(keyWord) !== -1 && sysConfig.AT_AHEAD) || !sysConfig.AT_AHEAD)) { - if (sysConfig.roomWhiteListOpen) { - const isInRoomWhiteList = sysConfig.roomWhiteList.includes(roomId) - if (isInRoomWhiteList) { - log.info('当前群在白名单内,请求问答...') - await wxai(sysConfig, bot, talker, room, message) - } else { - log.info('当前群不在白名单内,流程结束') - } - } - - if (!sysConfig.roomWhiteListOpen) { - log.info('系统未开启白名单,请求问答...') - await wxai(sysConfig, bot, talker, room, message) - } - } - - // IM服务开启时执行 - if (sysConfig.imOpen && types.Message.Text === message.type()) { - configData.clientChatEn.clientChatId = talker.id + ' ' + room.id - configData.clientChatEn.clientChatName = talker.name() + '@' + topic - // log.debug(configData) - socket.emit('CLIENT_ON', { - clientChatEn: configData.clientChatEn, - serverChatId: configData.serverChatEn.serverChatId, - }) - const data = { - msg: { - avatarUrl: '/static/image/im_server_avatar.png', - content: text, - contentType: 'text', - role: 'client', - }, - } - log.info(JSON.stringify(data)) - sendMsg(data) - } - - } - - if ((!room || !room.id) && !isSelfMsg) { - // 智能问答开启时执行 - if (sysConfig.WX_OPENAI_ONOFF && ((text.indexOf(keyWord) !== -1 && sysConfig.AT_AHEAD) || !sysConfig.AT_AHEAD)) { - if (sysConfig.contactWhiteListOpen) { - const isInContactWhiteList = sysConfig.contactWhiteList.includes(talker.id) - if (isInContactWhiteList) { - log.info('当前好友在白名单内,请求问答...') - await wxai(sysConfig, bot, talker, undefined, message) - } else { - log.info('当前好友不在白名单内,流程结束') - } - } - - if (!sysConfig.contactWhiteListOpen) { - log.info('系统未开启好友白名单,对所有好友有效,请求问答...') - await wxai(sysConfig, bot, talker, undefined, message) - } - } - } - - } catch (e) { - log.error('发起请求wxai失败', e) - } -} - -async function roomJoin (room: { topic: () => any; id: any; say: (arg0: string, arg1: any) => any }, inviteeList: Contact[], inviter: any) { - const nameList = inviteeList.map(c => c.name()).join(',') - log.info(`Room ${await room.topic()} got new member ${nameList}, invited by ${inviter}`) - - // 进群欢迎语,仅对开启了进群欢迎语白名单的群有效 - if (sysConfig.welcomeList.includes(room.id) && inviteeList.length) { - await room.say(`欢迎加入${await room.topic()},请阅读群公告~`, inviteeList) - } -} - -async function onError (err:any) { - log.error('bot.onError:', JSON.stringify(err)) - try { - job.cancel() - } catch (e) { - log.error('销毁定时任务失败:', JSON.stringify(e)) - } -} - -async function main (vika:any) { - await vika.init() - // 初始化获取配置信息 - const initReady = await vika.checkInit('主程序载入系统配置成功,等待插件初始化...') - if (!initReady) { - return - } - - // 获取系统配置信息 - sysConfig = await vika.getConfig() - config.botConfig.bot = sysConfig - const configReady = checkConfig(sysConfig) - - // 配置齐全,启动机器人 - if (configReady) { - bot = getBot(sysConfig) - bot.on('scan', onScan) - - if (sysConfig.puppetName === 'wechaty-puppet-xp') { - bot.on('login', onLogin) - } - if (sysConfig.puppetName !== 'wechaty-puppet-xp') { - bot.on('ready', onReady) - } - - bot.on('logout', onLogout) - bot.on('message', onMessage) - bot.on('room-join', roomJoin) - bot.on('error', onError) - - bot.start() - .then(() => log.info('Starter Bot Started.')) - .catch((e: any) => log.error('bot运行异常:', JSON.stringify(e))) - - if (sysConfig.imOpen) { - socket = imclient(bot, vika, configData) - } - } -} - -// 检查维格表配置并启动 -if (vikaConfig.spaceName && vikaConfig.token) { - vika = new VikaBot(vikaConfig) - void main(vika) -} else { - log.error('\n================================================\n\nvikaConfig配置缺少token或spaceName,请检查config.json文件\n\n================================================\n') - -} diff --git a/src/chatflow.ts b/src/chatflow.ts new file mode 100644 index 00000000..ea78c54a --- /dev/null +++ b/src/chatflow.ts @@ -0,0 +1,614 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/* eslint-disable sort-keys */ +import 'dotenv/config.js' +// import fs from 'fs' +import { + Contact, + Message, + ScanStatus, + WechatyPlugin, + log, + // types, + Sayable, + Wechaty, + Room, +} from 'wechaty' + +import qrcodeTerminal from 'qrcode-terminal' +import { FileBox } from 'file-box' +import fs from 'fs' +import { + VikaBot, + sendNotice, + // getFormattedRideInfo, + wxai, + ChatDevice, + propertyMessage, + eventMessage, + exportContactsAndRoomsToCSV, + exportContactsAndRoomsToXLSX, + getContact, + getRoom, + TaskConfig, +} from './plugins/mod.js' +import type { configTypes } from './types/mod.js' + +import { config } from './services/config.js' + +import { + waitForMs as wait, + getNow, + getRule, +} from './utils/utils.js' +import schedule from 'node-schedule' + +import { addMessage } from './api/message.js' + +log.info('配置文件信息:', JSON.stringify(config)) + +// log.info('process.env', JSON.stringify(process.env)) + +enum KeyWords { + HelpText = '帮助', + SetAdminText = '设置为管理群', + UpdateConfig = '更新配置', + UpdateNotice = '更新定时提醒', + UpdateContactList = '更新通讯录' + // ExportDoc = '导出文档' +} + +let chatdev: any = {} +let jobs: any +let vika: any +let isVikaOk: boolean = false + +// 消息发布器 +export const sendMsg = async (publisher:Message|Room|Contact, sayable: Sayable, inviteeList?: Contact[]) => { + try { + let replyMessage:Message| void + if (inviteeList?.length) { + const text = sayable as string + replyMessage = await (publisher as Room).say(text, ...inviteeList) + } else { + replyMessage = await publisher.say(sayable) + } + if (isVikaOk && replyMessage) { + await vika.onMessage(replyMessage) + } + } catch (e) { + log.error('消息发送失败:', publisher, sayable, e) + } +} + +// 从维格表下载配置 +export async function getCloudConfig () { + const newConfig = await vika.downConfigFromVika() + config.botConfig = newConfig.botConfig + config.functionOnStatus = newConfig.functionOnStatus + config.contactWhiteList = newConfig.contactWhiteList + config.roomWhiteList = newConfig.roomWhiteList + config.welcomeList = newConfig.welcomeList +} + +// 保存配置文件到data/config.json +export function updateConfig (config:any) { + fs.writeFileSync('data/config.json', JSON.stringify(config, null, '\t')) +} + +// 启动vika客户端 +export async function createVika () { + try { + vika = new VikaBot({ + spaceName: config.botConfig.vika.spaceName || '', + token: config.botConfig.vika.token || '', + }) + // 初始化系统表 + await vika.init() + + // 初始化获取配置信息 + const initReady = await vika.checkInit('主程序载入系统配置成功,等待插件初始化...') + if (!initReady) { + return + } + + const configReady = checkConfig(config) + // 配置齐全,启动机器人 + if (configReady) { + return vika + } + return false + } catch { + return false + } +} + +// 启动时检查配置信息 +export function checkConfig (config: configTypes.Config) { + const missingConfiguration = [] + + if (!config.botConfig.vika.token) { + missingConfiguration.push('VIKA_TOKEN') + } + + if (!config.botConfig.vika.spaceName) { + missingConfiguration.push('VIKA_SPACE_NAME') + } + + if (missingConfiguration.length > 0) { + // log.error('\n======================================\n\n', `错误提示:\n缺少${missingConfiguration.join()}配置参数,请在.env文件中设置或设置环境变量\n\n======================================`) + log.info('机器人配置信息:', config) + return false + } + return true +} + +// 更新任务 +export async function updateJobs (bot: Wechaty, vika: any) { + try { + // 结束所有任务 + const res = await schedule.gracefulShutdown() + log.info('结束所有任务成功:', res) + } catch (e) { + log.error('结束所有任务失败:', e) + } + try { + const tasks = await vika.getTimedTask() + log.info('格式化的定时提醒任务tasks:', JSON.stringify(tasks)) + jobs = {} + for (let i = 0; i < tasks.length; i++) { + const task: TaskConfig = tasks[i] + if (task.active) { + // 格式化任务 + const curRule = getRule(task) + log.info('当前任务原始信息', curRule, JSON.stringify(task)) + + try { + await schedule.scheduleJob(task.id, curRule, async () => { + try { + if (task.targetType === 'contact') { + try { + const contact = await getContact(bot, { name:task.targetName, id:task.targetId }) + if (contact) { + await sendMsg(contact, task.msg) + await wait(200) + } else { + log.info('当前好友不存在:', task.targetName) + } + } catch (e) { + log.error('发送好友定时任务失败:', e) + } + } + + if (task.targetType === 'room') { + try { + const room = await getRoom(bot, { topic: task.targetName, id: task.targetId }) + if (room) { + await sendMsg(room, task.msg) + await wait(200) + } else { + log.info('当前群不存在:', task.targetName) + } + } catch (e) { + log.error('发送群定时任务失败:', e) + } + } + } catch (err) { + log.error('定时任务执行失败:', err) + } + }) + jobs[task.id] = task + } catch (e) { + log.error('创建定时任务失败:', e) + } + } + } + log.info('定时提醒任务初始化完成,创建任务数量:', Object.keys(jobs).length) + + } catch (err: any) { + log.error('更新定时提醒列表任务失败:', err) + } +} + +export const onReadyOrLogin = async (bot:Wechaty) => { + const user: Contact = bot.currentUser + // 检查维格表配置并启动 + if (config.botConfig.vika.spaceName && config.botConfig.vika.token) { + try { + const res = await createVika() + isVikaOk = true + log.info('初始化vika成功:', res) + } catch (err) { + log.info('初始化vika失败:', err) + } + } else { + log.error('\n================================================\n\n维格表配置不全,.env文件或环境变量中中设置的token和spaceName之后重启\n\n================================================\n') + } + if (isVikaOk) { + const curDate = new Date().toLocaleString() + + // 启动MQTT通道 + if (config.botConfig.mqtt.password && config.botConfig.mqtt.username && config.botConfig.mqtt.endpoint && (config.functionOnStatus.mqtt.mqttControl || config.functionOnStatus.mqtt.mqttMessagePush)) { + chatdev = new ChatDevice(config.botConfig.mqtt.username, config.botConfig.mqtt.password, config.botConfig.mqtt.endpoint, config.botConfig.mqtt.port, user.id) + if (config.functionOnStatus.mqtt.mqttControl) { + chatdev.init(bot) + } + } + + const sysConfig = await vika.getConfig() + log.info('维格表中的配置信息:', JSON.stringify(sysConfig)) + + config.contactWhiteList = sysConfig.contactWhiteList + config.roomWhiteList = sysConfig.roomWhiteList + + // 更新云端好友和群 + await vika.updateRooms(bot) + await vika.updateContacts(bot) + + // 如果开启了MQTT推送,心跳同步到MQTT,每30s一次 + setInterval(() => { + try { + log.info('当前时间:', curDate) + if (chatdev && config.functionOnStatus.mqtt.mqttMessagePush) { + chatdev.pub_property(propertyMessage('lastActive', curDate)) + } + } catch (err) { + log.error('发送心跳失败:', err) + } + }, 300000) + + // 启动用户定时定时提醒任务 + await updateJobs(bot, vika) + log.info('================================================\n\n登录启动成功,程序准备就绪\n\n================================================\n') + } else { + log.info('================================================\n\n登录启动成功,但没有配置维格表\n\n================================================\n') + } + + if (config.botConfig.adminRoom.adminRoomId || config.botConfig.adminRoom.adminRoomTopic) { + const adminRoom = await getRoom(bot, { topic:config.botConfig.adminRoom.adminRoomTopic, id:config.botConfig.adminRoom.adminRoomId }) + await adminRoom?.say('chatflow启动成功,可输入【帮助】获取操作指令') + } +} + +export async function onScan (qrcode: string, status: ScanStatus) { + // 上传二维码到维格表,可通过扫码维格表中二维码登录 + if (isVikaOk) await vika.onScan(qrcode, status) + + // 控制台显示二维码 + if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) { + const qrcodeUrl = encodeURIComponent(qrcode) + const qrcodeImageUrl = [ + 'https://wechaty.js.org/qrcode/', + qrcodeUrl, + ].join('') + log.info('机器人启动,使用手机微信扫描二维码登录', 'onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl) + qrcodeTerminal.generate(qrcode, { small: true }) // show qrcode on console + } else { + log.error('机器人启动,获取登录二维码失败', 'onScan: %s(%s)', ScanStatus[status], status) + } +} + +export function ChatFlow (config: configTypes.Config): WechatyPlugin { + log.verbose('ChatFlow', 'ChatFlow is used.') + + return function ChatFlowPlugin (bot: Wechaty) :void { + log.verbose('StoreByVika', 'installing on %s ...', bot) + + bot.on('scan', async (qrcode: string, status: ScanStatus) => { + // 上传二维码到维格表,可通过扫码维格表中二维码登录 + if (isVikaOk) await vika.onScan(qrcode, status) + + // 控制台显示二维码 + if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) { + const qrcodeUrl = encodeURIComponent(qrcode) + const qrcodeImageUrl = [ + 'https://wechaty.js.org/qrcode/', + qrcodeUrl, + ].join('') + log.info('机器人启动,使用手机微信扫描二维码登录', 'onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl) + qrcodeTerminal.generate(qrcode, { small: true }) // show qrcode on console + } else { + log.error('机器人启动,获取登录二维码失败', 'onScan: %s(%s)', ScanStatus[status], status) + } + }) + bot.on('login', async (user: Contact) => { + log.info('onLogin,当前登录的账号信息:', JSON.stringify(user)) + + // 更新机器人基本信息 + config.botInfo = user.payload as configTypes.BotInfo + await updateConfig(config) + + // 登录成功向机器人自己发送上线通知(不是所有的puppet都支持想自己发消息) + // await sendMsg(user, '上线:' + curDate) + if ([ 'wechaty-puppet-xp' ].includes(config.botConfig.wechaty.puppet)) await onReadyOrLogin(bot) + }) + bot.on('ready', async () => { + const user: Contact = bot.currentUser + log.info('onReady,当前登录的账号信息:', JSON.stringify(user)) + config.botInfo = user.payload as configTypes.BotInfo + await updateConfig(config) + + // 登录成功向机器人自己发送上线通知(不是所有的puppet都支持想自己发消息) + // await sendMsg(user, '上线:' + curDate) + + if (![ 'wechaty-puppet-xp' ].includes(config.botConfig.wechaty.puppet)) await onReadyOrLogin(bot) + }) + bot.on('logout', (user: Contact) => { + log.info('logout,退出登录:', '%s logout', user) + // job.cancel() + }) + bot.on('message', async message => { + log.info('onMessage,接收到消息:', JSON.stringify(message.payload)) + + // 存储消息到db + const addRes = await addMessage(message) + if (addRes) { + const curDate = new Date().toLocaleString() + const talker = message.talker() + const name = talker.name() + const alias = await talker.alias() + const text = message.text() + const room = message.room() + const roomId = room?.id + const topic = await room?.topic() + const keyWord = bot.currentUser.name() + const isSelf = message.self() + let isAdminRoom: boolean = false + + if (room && room.id) { + + // 机器人发送 设置为管理群 到群,设置管理员群 + if (message.self() && text === KeyWords.SetAdminText) { + config.botConfig['adminRoom'] = { + adminRoomId: room.id, + adminRoomTopic:await room.topic(), + } + await updateConfig(config) + await sendMsg(message, '设置管理群成功') + } + } + + isAdminRoom = (roomId && (roomId === config.botConfig.adminRoom.adminRoomId || topic === config.botConfig.adminRoom.adminRoomTopic)) || isSelf + + // 管理员群接收到管理指令时执行相关操作 + if (isAdminRoom) { + await sendNotice(bot, message) + let replyText: string = '' + if (text === '指令列表' || text === '帮助') { + replyText = '操作指令说明:\n【更新配置】 更新全部配置\n【更新定时提醒】 更新定时提醒任务\n【更新通讯录】 更新维格表通信录\n【下载通讯录】 下载通讯录xlsx表\n【下载通知模板】 下载通知模板' + await sendMsg(message, replyText) + } + + if (isVikaOk && [ '更新配置', '更新定时提醒', '更新通讯录', '上传配置', '下载配置' ].includes(text)) { + switch (text) { + case KeyWords.UpdateConfig: + log.info('热更新系统配置~') + try { + await getCloudConfig() + replyText = '配置更新成功~' + } catch (e) { + replyText = '配置更新成功~' + } + break + case KeyWords.UpdateNotice: + log.info('热更新通知任务~') + try { + await updateJobs(bot, vika) + replyText = '提醒任务更新成功~' + } catch (e) { + replyText = '提醒任务更新失败~' + } + break + case KeyWords.UpdateContactList: + log.info('热更新通讯录到维格表~') + try { + await vika.updateContacts(bot) + await vika.updateRooms(bot) + replyText = '通讯录更新成功~' + } catch (e) { + replyText = '通讯录更新失败~' + } + break + case '上传配置': + log.info('上传配置信息到维格表~') + try { + await vika.updateConfigToVika(config) + replyText = '上传配置信息成功~' + } catch (e) { + replyText = '上传配置信息失败~' + } + break + case '下载配置': + log.info('上传配置信息到维格表~') + try { + await getCloudConfig() + replyText = '下载配置信息成功~' + } catch (e) { + replyText = '下载配置信息失败~' + } + break + default: + // 当text不匹配任何case时执行的操作 + break + } + + await sendMsg(message, getNow() + replyText) + } + + if (!isVikaOk && [ '更新配置', '更新定时提醒', '更新通讯录' ].includes(text)) { + await sendMsg(message, '未配置维格表,指令无效') + } + + switch (text) { + case '下载csv通讯录': + log.info('下载通讯录到csv表~') + try { + const fileBox = await exportContactsAndRoomsToCSV(bot) + await sendMsg(message, fileBox) + } catch (err) { + log.error('exportContactsAndRoomsToCSV', err) + await sendMsg(message, '下载失败~') + } + break + case '下载通讯录': + log.info('下载通讯录到xlsx表~') + try { + const fileBox = await exportContactsAndRoomsToXLSX(bot) + await sendMsg(message, fileBox) + } catch (err) { + log.error('exportContactsAndRoomsToXLSX', err) + await sendMsg(message, '下载失败~') + } + break + case '下载通知模板': + log.info('下载通知模板~') + try { + const fileBox = FileBox.fromFile('./src/templates/群发通知模板.xlsx') + await sendMsg(message, fileBox) + } catch (err) { + log.error('下载模板失败', err) + await sendMsg(message, '下载失败,请重试~') + } + break + case '初始化': + log.info('初始化系统~') + try { + await vika.init() + await sendMsg(message, '初始化系统表完成~') + } catch (err) { + log.error('初始化系统失败', err) + await sendMsg(message, '初始化系统失败,请重试~') + } + break + default: + break + } + } + + // 微信对话开放平台智能问答 + try { + if (room && roomId && !isSelf) { + // 检测顺风车信息并格式化 + // const KEYWORD_LIST = [ '人找车', '车找人' ] + // try { + // // 判断消息中是否包含关键字 + // if (KEYWORD_LIST.some(keyword => message.text().includes(keyword))) { + // const replyMsg = await getFormattedRideInfo(message) + // if (replyMsg) { + // const replyText = replyMsg.choices[0].message.content.replace(/\r/g, '') + // log.info('回复内容:', replyText) + // await sendMsg(room, replyText) + // } + // } + // } catch (err) { + + // } + + // 智能问答开启时执行 + if (config.functionOnStatus.autoQa.autoReply && ((text.indexOf(keyWord) !== -1 && config.functionOnStatus.autoQa.atReply) || !config.functionOnStatus.autoQa.atReply)) { + if (config.functionOnStatus.autoQa.roomWhitelist) { + const isInRoomWhiteList = config.roomWhiteList.includes(roomId) || (topic && config.roomWhiteList.includes(topic)) + if (isInRoomWhiteList) { + log.info('当前群在白名单内,请求问答...') + await wxai(config, bot, talker, room, message) + } else { + log.info('当前群不在白名单内,流程结束') + } + } else { + log.info('系统未开启白名单,请求问答...') + await wxai(config, bot, talker, room, message) + } + } + } + + if ((!room || !room.id) && !isSelf) { + // 智能问答开启时执行 + if (config.functionOnStatus.autoQa.autoReply && ((text.indexOf(keyWord) !== -1 && config.functionOnStatus.autoQa.atReply) || !config.functionOnStatus.autoQa.atReply)) { + if (config.functionOnStatus.autoQa.contactWhitelist) { + const isInContactWhiteList = config.contactWhiteList.includes(talker.id) || (alias && config.contactWhiteList.includes(alias)) || config.contactWhiteList.includes(name) + if (isInContactWhiteList) { + log.info('当前好友在白名单内,请求问答...') + await wxai(config, bot, talker, undefined, message) + } else { + log.info('当前好友不在白名单内,流程结束') + } + } else { + log.info('系统未开启好友白名单,对所有好友有效,请求问答...') + await wxai(config, bot, talker, undefined, message) + } + } + } + + } catch (e) { + log.error('发起请求wxai失败', e) + } + + // 消息存储到维格表 + if (isVikaOk) { + await vika.onMessage(message) + } + + // 消息通过MQTT上报 + if (chatdev && config.functionOnStatus.mqtt.mqttMessagePush) { + /* + 将消息通过mqtt通道上报到云端 + */ + // chatdev.pub_message(message) + chatdev.pub_event(eventMessage('onMessage', { curDate })) + } + } else { + log.info('重复消费消息:', message.id) + } + }) + + bot.on('room-join', async (room: Room, inviteeList: Contact[], inviter: any) => { + const nameList = inviteeList.map(c => c.name()).join(',') + log.info(`roomJoin,接收到消息:群 ${await room.topic()} 有新成员 ${nameList}, 邀请人 ${inviter}`) + + // 进群欢迎语,仅对开启了进群欢迎语白名单的群有效 + if (isVikaOk && config.welcomeList?.includes(room.id) && inviteeList.length) { + await sendMsg(room, `欢迎加入${await room.topic()},请阅读群公告~`, inviteeList) + } + }) + bot.on('error', async (err: any) => { + log.error('onError,bot运行错误:', JSON.stringify(err)) + // try { + // // job.cancel() + // } catch (e) { + // log.error('销毁定时任务失败:', JSON.stringify(e)) + // } + }, + ) + } + +} + +// 配置机器人 +export function getBotOps (puppet:string, token:string) { + const ops:any = { + name: 'chatflow', + puppet, + puppetOptions: { + token, + }, + } + + if (puppet === 'wechaty-puppet-service') { + process.env['WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT'] = 'true' + } + + if ([ 'wechaty-puppet-wechat4u', 'wechaty-puppet-xp', 'wechaty-puppet-engine' ].includes(puppet)) { + delete ops.puppetOptions.token + } + + if (puppet === 'wechaty-puppet-wechat') { + delete ops.puppetOptions.token + ops.puppetOptions.uos = true + } + + log.info('Wchaty配置信息:', JSON.stringify(ops)) + return ops +} + +export { + config, +} diff --git a/src/config.json b/src/config.json deleted file mode 100644 index c540442b..00000000 --- a/src/config.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "baseConfig": { - "VIKA_TOKEN": "", - "VIKA_SPACENAME": "", - "puppetName": "wechaty-puppet-wechat4u", - "puppetToken": "" - }, - "botConfig": { - "adminRoomId": "213411825721@chatroom", - "adminRoomTopic": "TODO", - "apps": { - "qa": { - "config": { - "key1": 123, - "key2": "xxx" - }, - "isOpen": true - }, - "riding": { - "config": {}, - "isOpen": true - } - }, - "bot": { - "puppet": "", - "token": "", - "VIKA_TOKEN": "", - "VIKA_SPACENAME": "" - }, - "command": { - "bot": { - "reboot": "#重启机器人", - "selfInfo": "#机器人信息" - }, - "contact": { - "findall": "#联系人列表" - }, - "room": { - "findall": "#群列表" - } - } - }, - "contactConfig": { - "tyutluyc": { - "app": "waiting", - "apps": { - "qa": { - "config": {}, - "isOpen": true - }, - "riding": { - "config": {}, - "isOpen": true - } - } - }, - "tyutluyc2": { - "app": "waiting", - "apps": { - "qa": { - "config": {}, - "isOpen": true - }, - "riding": { - "config": {}, - "isOpen": true - } - } - } - }, - "roomConfig": { - "213411825721@chatroom": { - "app": "waiting", - "apps": { - "qa": { - "config": {}, - "isOpen": true - }, - "riding": { - "config": {}, - "isOpen": true - } - } - }, - "21341182572@chatroom": { - "app": "waiting", - "apps": { - "qa": { - "config": {}, - "isOpen": true - }, - "riding": { - "config": {}, - "isOpen": true - }, - "riding2": { - "config": {}, - "isOpen": true - } - } - } - } -} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 82dde42c..00000000 --- a/src/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* -修改config.json配置文件,参考一下说明修改 -{ - "baseConfig": { - "VIKA_TOKEN": "修改为自己的维格表token", - "VIKA_SPACENAME": "修改为自己的维格表空间名称", - "puppetName": "wechaty-puppet-wechat4u", //可选值:wechaty-puppet-wechat4u、wechaty-puppet-wechat、wechaty-puppet-xp、wechaty-puppet-engine、wechaty-puppet-padlocal、wechaty-puppet-service - "puppetToken": "" - }, -} -*/ -import fs from 'fs' -let config:any = fs.readFileSync('src/config.json', 'utf8') -config = JSON.parse(config) -type Configs = { - [key: string]: any; -} -// 配置文件,所有配置必须齐全,补充空白配置项,其他配置项可按需要修改 -const baseConfig:Configs = config.baseConfig - -export { baseConfig, config } diff --git a/src/db/nedb.ts b/src/db/nedb.ts index 89ae9ab6..7c20d83a 100644 --- a/src/db/nedb.ts +++ b/src/db/nedb.ts @@ -1,88 +1,114 @@ import Datastore from 'nedb' -function DB (this: any, database: any) { - const options = { - autoload: true, - filename: database, - } - this.db = new Datastore(options) -} -DB.prototype.limit = function (offset: number, limit: number) { - this.offset = offset || 0 - this.limit = limit || 15 - return this -} -DB.prototype.sort = function (orderby: any) { - this.orderby = orderby - return this -} -DB.prototype.find = function (query: any, select: any) { - return new Promise((resolve, reject) => { - const stmt = this.db.find(query || {}) - if (this.orderby !== undefined) { - stmt.sort(this.orderby) - } - if (this.offset !== undefined) { - stmt.skip(this.offset).limit(this.limit) - } - if (select !== undefined) { - stmt.projection(select || {}) +class DB { + + private db: any + private offsetValue: number + private limitValue: number + private orderby: any + + constructor (database: any) { + const options = { + autoload: true, + filename: database, } - stmt.exec((err: any, docs: unknown) => { - if (err) { - return reject(err) + this.db = new Datastore(options) + this.offsetValue = 0 + this.limitValue = 15 + } + + public limit (offset: number, limit: number): this { + this.offsetValue = offset || 0 + this.limitValue = limit || 15 + return this + } + + public sort (orderby: any): this { + this.orderby = orderby + return this + } + + public find (query: any, select?: any): Promise { + return new Promise((resolve, reject) => { + const stmt = this.db.find(query || {}) + if (this.orderby !== undefined) { + stmt.sort(this.orderby) } - resolve(docs) - }) - }) -} -DB.prototype.findOne = function (query: any, select: any) { - return new Promise((resolve, reject) => { - const stmt = this.db.findOne(query || {}) - if (this.sort !== undefined) { - stmt.sort(this.sort) - } - if (select !== undefined) { - stmt.projection(select || {}) - } - stmt.exec((err: any, doc: unknown) => { - if (err) { - return reject(err) + if (this.offsetValue !== 0) { + stmt.skip(this.offsetValue).limit(this.limitValue) } - resolve(doc) - }) - }) -} -DB.prototype.insert = function (values: any) { - return new Promise((resolve, reject) => { - this.db.insert(values, (err: any, newDoc: unknown) => { - if (err) { - return reject(err) + if (select !== undefined) { + stmt.projection(select || {}) } - resolve(newDoc) + stmt.exec((err: any, docs: unknown) => { + if (err) { + return reject(err) + } + resolve(docs) + }) }) - }) -} -DB.prototype.update = function (query: any, values: any, options: any) { - return new Promise((resolve, reject) => { - this.db.update(query || {}, values || {}, options || {}, (err: any, numAffected: unknown) => { - if (err) { - return reject(err) + } + + public findOne (query: any, select?: any): Promise { + return new Promise((resolve, reject) => { + const stmt = this.db.findOne(query || {}) + if (this.orderby !== undefined) { + stmt.sort(this.orderby) } - resolve(numAffected) - }) - }) -} -DB.prototype.remove = function (query: any, options: any) { - return new Promise((resolve, reject) => { - this.db.remove(query || {}, options || {}, (err: any, numAffected: unknown) => { - if (err) { - return reject(err) + if (select !== undefined) { + stmt.projection(select || {}) } - resolve(numAffected) + stmt.exec((err: any, doc: unknown) => { + if (err) { + return reject(err) + } + resolve(doc) + }) + }) + } + + public insert (values: any): Promise { + return new Promise((resolve, reject) => { + this.db.insert(values, (err: any, newDoc: unknown) => { + if (err) { + return reject(err) + } + resolve(newDoc) + }) }) - }) + } + + // options: { + // multi: true, + // upsert: true, + // returnUpdatedDocs: true, + // returnOriginal: false, + // returnUpdatedExisting: false + // } + public update (query: any, values: any, options?: any): Promise { + return new Promise((resolve, reject) => { + this.db.update(query || {}, values || {}, options || {}, (err: any, numAffected: unknown) => { + if (err) { + return reject(err) + } + resolve(numAffected) + }) + }) + } + + public remove (query: any, options?: any): Promise { + return new Promise((resolve, reject) => { + this.db.remove(query || {}, options || {}, (err: any, numAffected: unknown) => { + if (err) { + return reject(err) + } + resolve(numAffected) + }) + }) + } + } + export default (database: any) => { - return DB(database) + return new DB(database) } diff --git a/src/db/tables.ts b/src/db/tables.ts index 191d683d..f1e841d7 100644 --- a/src/db/tables.ts +++ b/src/db/tables.ts @@ -1,27 +1,15 @@ -import Datastore from 'nedb-promises' +import DB from './nedb.js' -const db:any = {} - -db.message = Datastore.create({ - autoload: true, - filename: './db/messages.db', -}) - -db.bot = Datastore.create({ - autoload: true, - filename: './db/bot.db', -}) - -db.room = Datastore.create({ - autoload: true, - filename: './db/room.db', -}) - -db.contact = Datastore.create({ - autoload: true, - filename: './db/contact.db', -}) +export interface Database { + message: any; + bot: any; + room: any; + contact: any; +} -export { - db, +export const db: Database = { + bot: DB('data/db/bots.db'), + contact: DB('data/db/contacts.db'), + message: DB('data/db/messages.db'), + room: DB('data/db/rooms.db'), } diff --git a/src/handlers/on-message.js b/src/handlers/on-message.js deleted file mode 100644 index dc3ff5b3..00000000 --- a/src/handlers/on-message.js +++ /dev/null @@ -1,168 +0,0 @@ -import fs from 'fs' -import console from 'console' - -import * as PUPPET from 'wechaty-puppet' -import { log } from 'wechaty-puppet' - -const msgList = [] - -const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) - -async function onMessage (message, vika) { - // console.debug(message) - try { - - let uploadedAttachments = '' - const msgType = PUPPET.types.Message[message.type()] - let file = '' - let filePath = '' - let text = '' - - let urlLink - let miniProgram - - switch (message.type()) { - // 文本消息 - case PUPPET.types.Message.Text: - text = message.text() - break - - // 图片消息 - - case PUPPET.types.Message.Image: - - try { - // await wait(2500) - // const img = await message.toImage() - // file = await img.thumbnail() - file = await message.toFileBox() - - } catch (e) { - console.error('Image解析失败:', e) - file = '' - } - - break - - // 链接卡片消息 - case PUPPET.types.Message.Url: - urlLink = await message.toUrlLink() - text = JSON.stringify(JSON.parse(JSON.stringify(urlLink)).payload) - // file = await message.toFileBox(); - break - - // 小程序卡片消息 - case PUPPET.types.Message.MiniProgram: - - miniProgram = await message.toMiniProgram() - - text = JSON.stringify(JSON.parse(JSON.stringify(miniProgram)).payload) - - // console.debug(miniProgram) - /* - miniProgram: 小程序卡片数据 - { - appid: "wx363a...", - description: "贝壳找房 - 真房源", - title: "美国白宫,10室8厅9卫,99999刀/月", - iconUrl: "http://mmbiz.qpic.cn/mmbiz_png/.../640?wx_fmt=png&wxfrom=200", - pagePath: "pages/home/home.html...", - shareId: "0_wx363afd5a1384b770_..._1615104758_0", - thumbKey: "84db921169862291...", - thumbUrl: "3051020100044a304802010002046296f57502033d14...", - username: "gh_8a51...@app" - } - */ - break - - // 语音消息 - case PUPPET.types.Message.Audio: - - try { - file = await message.toFileBox() - - } catch (e) { - console.error('Audio解析失败:', e) - file = '' - } - - break - - // 视频消息 - case PUPPET.types.Message.Video: - - try { - file = await message.toFileBox() - - } catch (e) { - console.error('Video解析失败:', e) - file = '' - } - break - - // 动图表情消息 - case PUPPET.types.Message.Emoticon: - - try { - file = await message.toFileBox() - - } catch (e) { - console.error('Emoticon解析失败:', e) - file = '' - } - - break - - // 文件消息 - case PUPPET.types.Message.Attachment: - - try { - file = await message.toFileBox() - - } catch (e) { - console.error('Attachment解析失败:', e) - file = '' - } - - break - // 文件消息 - case PUPPET.types.Message.Location: - - // const location = await message.toLocation() - // text = JSON.stringify(JSON.parse(JSON.stringify(location)).payload) - break - // 其他消息 - default: - break - } - - if (file) { - filePath = './' + file.name - try { - const writeStream = fs.createWriteStream(filePath) - await file.pipe(writeStream) - await wait(500) - const readerStream = fs.createReadStream(filePath) - uploadedAttachments = await vika.upload(readerStream) - fs.unlink(filePath, (err) => { - console.debug('上传vika完成删除文件:', filePath, err) - }) - } catch { - console.debug('上传失败:', filePath) - fs.unlink(filePath, (err) => { - console.debug('上传vika失败删除文件', filePath, err) - }) - } - - } - - vika.addChatRecord(message, uploadedAttachments, msgType, text) - - } catch (e) { - console.log('vika 写入失败:', e) - } -} - -export { onMessage } - -export default onMessage diff --git a/src/handlers/on-scan.js b/src/handlers/on-scan.js deleted file mode 100644 index 750c6533..00000000 --- a/src/handlers/on-scan.js +++ /dev/null @@ -1,79 +0,0 @@ -import fs from 'fs' -import console from 'console' - -import * as PUPPET from 'wechaty-puppet' -import { log } from 'wechaty-puppet' -import { FileBox } from 'file-box' -// import { -// Contact, -// Room, -// Message, -// ScanStatus, -// WechatyBuilder, -// types, -// } from 'wechaty' - -const msgList = [] - -const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) - -async function onScan (qrcode, status, vika) { - console.debug(qrcode, status) - - if (status === PUPPET.ScanStatus.Waiting || status === PUPPET.ScanStatus.Timeout) { - const qrcodeImageUrl = [ - 'https://wechaty.js.org/qrcode/', - encodeURIComponent(qrcode), - ].join('') - log.info('qrcodeImageUrl: %s', qrcodeImageUrl) - - try { - let uploadedAttachments = '' - let file = '' - let filePath = '' - const text = qrcodeImageUrl - - try { - file = FileBox.fromUrl( - qrcodeImageUrl, - 'logo.jpg', - ) - file.toFile('/tmp/file-box-logo.jpg') - - // await wait(1000) - // console.debug('file=======================',file) - } catch (e) { - console.error('Image解析失败:', e) - } - - if (file) { - filePath = './' + file.name - try { - const writeStream = fs.createWriteStream(filePath) - await file.pipe(writeStream) - await wait(200) - const readerStream = fs.createReadStream(filePath) - uploadedAttachments = await vika.upload(readerStream) - vika.addScanRecord(uploadedAttachments, text) - fs.unlink(filePath, (err) => { - console.debug('上传vika完成删除文件:', filePath, err) - }) - } catch { - console.debug('上传失败:', filePath) - fs.unlink(filePath, (err) => { - console.debug('上传vika失败删除文件', filePath, err) - }) - } - - } - - } catch (e) { - console.log('vika 写入失败:', e) - } - - } -} - -export { onScan } - -export default onScan diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 02a52354..00000000 --- a/src/index.ts +++ /dev/null @@ -1,761 +0,0 @@ -#!/usr/bin/env -S node --no-warnings --loader ts-node/esm -import 'dotenv/config.js' -// import fs from 'fs' -import { - Contact, - Message, - ScanStatus, - log, - // Room, - types, - Wechaty, - WechatyBuilder, -} from 'wechaty' - -import qrcodeTerminal from 'qrcode-terminal' -import { FileBox } from 'file-box' -import fs, { createWriteStream } from 'fs' -import XLSX from 'xlsx' -import csv from 'fast-csv' -import { - VikaBot, - configData, - sendMsg, - sendNotice, - getFormattedRideInfo, - imclient, - wxai, - ChatDevice, - propertyMessage, - eventMessage, - -} from './plugins/index.js' -import type { types as configTypes } from './mods/mod.js' -import { baseConfig, config } from './config.js' -import { - waitForMs as wait, - formatSentMessage, -} from './util/tool.js' -import schedule from 'node-schedule' -import { db } from './db/tables.js' - -log.info('db:', db) -log.info('config:', JSON.stringify(config)) -// log.info('process.env', JSON.stringify(process.env)) - -enum Prompts { - a = '输入的信息错误或格式不符合要求,请输入如下格式"维格表token+空间名称",中间用加号分开,例如:\nuskxRhxxxxxxxx3UK959A8093+wechatbot', - b = '启动成功,请输入"维格表token+空间名称",中间用加号分开,例如:\nuskxRhxxxxxxxx3UK959A8093+wechatbot' -} - -let bot: Wechaty -let puppet = baseConfig['puppetName'] || process.env['WECHATY_PUPPET'] -let token = baseConfig['puppetToken'] || process.env['WECHATY_TOKEN'] -const vikaConfig = { - spaceName: baseConfig['VIKA_SPACENAME'] || process.env['VIKA_SPACENAME'], - token: baseConfig['VIKA_TOKEN'] || process.env['VIKA_TOKEN'], -} -// log.info(vikaConfig) -let sysConfig: configTypes.SysConfig -let chatdev: any = {} -// let job: any -let jobs: any -let vika: any -let isVikaOk: boolean = false -let socket: any = {} - -// log.info(baseConfig) - -function updateBaseConfig (config: configTypes.SysConfig) { - puppet = config['puppetName'] || puppet - token = config['puppetToken'] || token -} - -function updateConfig (config:any) { - fs.writeFileSync('src/config.json', JSON.stringify(config)) -} - -async function createVika () { - try { - vika = new VikaBot(vikaConfig) - await vika.init() - - // 初始化获取配置信息 - const initReady = await vika.checkInit('主程序载入系统配置成功,等待插件初始化...') - if (!initReady) { - return - } - - // 获取系统配置信息 - sysConfig = await vika.getConfig() - log.info('config:', JSON.stringify(config)) - const configReady = checkConfig(config) - updateBaseConfig(config) - // 配置齐全,启动机器人 - if (configReady) { - return vika - } - } catch { - return false - } -} - -function getBot () { - const ops: any = { - name: 'qa-bot', - puppet, - puppetOptions: { - token, - }, - } - - log.info(ops) - - if (puppet === 'wechaty-puppet-service') { - process.env['WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT'] = 'true' - } - - if ([ 'wechaty-puppet-wechat4u', 'wechaty-puppet-xp', 'wechaty-puppet-engine' ].includes(puppet)) { - delete ops.puppetOptions.token - } - - if (puppet === 'wechaty-puppet-wechat') { - delete ops.puppetOptions.token - ops.puppetOptions.uos = true - } - - log.info('bot ops:', JSON.stringify(ops)) - - const bot = WechatyBuilder.build(ops) - return bot -} - -function getNow () { - return new Date().toLocaleString() -} - -function checkConfig (config: { [key: string]: any }) { - const missingConfiguration = [] - - for (const key in config) { - if (!config[key] && ![ 'imOpen', 'DIFF_REPLY_ONOFF' ].includes(key)) { - missingConfiguration.push(key) - } - } - - if (missingConfiguration.length > 0) { - // log.error('\n======================================\n\n', `错误提示:\n缺少${missingConfiguration.join()}配置参数,请检查config.js文件\n\n======================================`) - log.info('bot config:', config) - return false - } - return true -} - -async function relpy (bot: Wechaty, vika: any, replyText: string, message: Message) { - await message.say(replyText) - vika.addRecord(await formatSentMessage(bot.currentUser, replyText, message.room() ? undefined : message.talker(), message.room())) -} - -async function exportContactsAndRoomsToCSV () { - // 获取所有联系人和群聊 - const contacts = await bot.Contact.findAll() - const rooms = await bot.Room.findAll() - - // 准备CSV数据 - const csvData = [] - contacts.forEach((contact: Contact) => { - if (contact.friend()) { - csvData.push({ ID: contact.id, Name: Buffer.from(contact.name(), 'utf-8').toString() || '未知', Type: 'Contact' }) - } - }) - - for (const room of rooms) { - csvData.push({ ID: room.id, Name: Buffer.from(await room.topic(), 'utf-8').toString() || '未知', Type: 'Room' }) - } - - log.info('通讯录原始数据:', csvData) - - const fileName = './db/contacts_and_rooms.csv' - const writeStream = createWriteStream(fileName) - const csvStream = csv.format({ headers: true }) - csvStream.pipe(writeStream).on('end', () => { - log.info('CSV file written successfully') - }) - - csvData.forEach((item) => { - csvStream.write(item) - }) - - csvStream.end() - - // 返回FileBox对象 - return FileBox.fromFile(fileName) -} - -async function exportContactsAndRoomsToXLSX () { - // 获取所有联系人和群聊 - const contacts = await bot.Contact.findAll() - const rooms = await bot.Room.findAll() - - // 准备联系人和群聊数据 - const contactsData = [ [ 'Name', 'ID' ] ] - const roomsData = [ [ 'Name', 'ID' ] ] - contacts.forEach((contact) => { - if (contact.friend()) { - contactsData.push([ contact.name(), contact.id ]) - } - }) - - for (const room of rooms) { - roomsData.push([ await room.topic(), room.id ]) - } - - // 创建一个新的工作簿 - const workbook = XLSX.utils.book_new() - - // 将数据添加到工作簿的不同sheet中 - const contactsSheet = XLSX.utils.aoa_to_sheet(contactsData) - const roomsSheet = XLSX.utils.aoa_to_sheet(roomsData) - XLSX.utils.book_append_sheet(workbook, contactsSheet, 'Contacts') - XLSX.utils.book_append_sheet(workbook, roomsSheet, 'Rooms') - - // 将工作簿写入文件 - const fileName = './db/contacts_and_rooms.xlsx' - XLSX.writeFile(workbook, fileName) - - // 返回FileBox对象 - return FileBox.fromFile(fileName) -} - -async function updateJobs (bot: Wechaty, vika: any) { - try { - const tasks = await vika.getTimedTask() - schedule.gracefulShutdown() - jobs = {} - // log.info(tasks) - for (let i = 0; i < tasks.length; i++) { - const task: any = tasks[i] - if (task.active) { - const curTimeF = new Date(task.time) - // const curTimeF = new Date(task.time+8*60*60*1000) - let curRule = '* * * * * *' - let dayOfWeek: any = '*' - let month: any = '*' - let dayOfMonth: any = '*' - let hour: any = curTimeF.getHours() - let minute: any = curTimeF.getMinutes() - const second = 0 - const addMonth = [] - switch (task.cycle) { - case '每季度': - month = curTimeF.getMonth() - for (let i = 0; i < 4; i++) { - if (month + 3 <= 11) { - addMonth.push(month) - } else { - addMonth.push(month - 9) - } - month = month + 3 - } - month = addMonth - break - case '每天': - break - case '每周': - dayOfWeek = curTimeF.getDay() - break - case '每月': - month = curTimeF.getMonth() - break - case '每小时': - hour = '*' - break - case '每30分钟': - hour = '*' - minute = [ 0, 30 ] - break - case '每15分钟': - hour = '*' - minute = [ 0, 15, 30, 45 ] - break - case '每10分钟': - hour = '*' - minute = [ 0, 10, 20, 30, 40, 50 ] - break - case '每5分钟': - hour = '*' - minute = [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 ] - break - case '每分钟': - hour = '*' - minute = '*' - break - default: - month = curTimeF.getMonth() - dayOfMonth = curTimeF.getDate() - break - - } - curRule = `${second} ${minute} ${hour} ${dayOfMonth} ${month} ${dayOfWeek}` - log.info(curRule) - - try { - schedule.scheduleJob(task.id, curRule, async () => { - try { - const curDate = new Date() - log.info('定时任务:', curTimeF, curRule, curDate, JSON.stringify(task)) - // await user.say('心跳:' + curDate) - - try { - if (task.contacts.length) { - const contact = await bot.Contact.find({ id: task.contacts[0] }) - if (contact) { - await contact.say(task.msg) - vika.addRecord(await formatSentMessage(bot.currentUser, task.msg, contact, undefined)) - await wait(200) - } - } - } catch (e) { - log.error('发送好友定时任务失败:', e) - } - - try { - if (task.rooms.length) { - const room = await bot.Room.find({ id: task.rooms[0] }) - if (room) { - await room.say(task.msg) - vika.addRecord(await formatSentMessage(bot.currentUser, task.msg, undefined, room)) - await wait(200) - } - } - } catch (e) { - log.error('发送群定时任务失败:', e) - - } - - } catch (err) { - log.error('定时任务执行失败:', err) - } - }) - jobs[task.id] = task - } catch (e) { - log.error('创建定时任务失败:', e) - } - } - } - log.info('通知提醒任务初始化完成,创建任务数量:', Object.keys(jobs).length) - - } catch (err: any) { - log.error('更新通知提醒列表任务失败:', err) - } -} - -async function onScan (qrcode: string, status: ScanStatus) { - // 上传二维码到维格表,可通过扫码维格表中二维码登录 - if (isVikaOk) await vika.onScan(qrcode, status) - - // 控制台显示二维码 - if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) { - const qrcodeUrl = encodeURIComponent(qrcode) - const qrcodeImageUrl = [ - 'https://wechaty.js.org/qrcode/', - qrcodeUrl, - ].join('') - log.info('StarterBot', 'onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl) - qrcodeTerminal.generate(qrcode, { small: true }) // show qrcode on console - - } else { - log.info('StarterBot', 'onScan: %s(%s)', ScanStatus[status], status) - } -} - -async function onLogin (user: Contact) { - log.info('StarterBot', '%s login', user) - log.info('当前登录的账号信息:', JSON.stringify(user)) - - if (isVikaOk) { - const curDate = new Date().toLocaleString() - await user.say('上线:' + curDate) - // 启动MQTT通道 - if (sysConfig.mqttPassword && (sysConfig.mqtt_SUB_ONOFF || sysConfig.mqtt_PUB_ONOFF)) { - chatdev = new ChatDevice(sysConfig.mqttUsername, sysConfig.mqttPassword, sysConfig.mqttEndpoint, sysConfig.mqttPort, user.id) - if (sysConfig.mqtt_SUB_ONOFF) { - chatdev.init(bot) - } - } - - // 更新云端好友和群 - await vika.updateRooms(bot) - await vika.updateContacts(bot) - - // 如果开启了MQTT推送,心跳同步到MQTT,每30s一次 - setInterval(() => { - try { - log.info(curDate) - if (chatdev && sysConfig.mqtt_PUB_ONOFF) { - chatdev.pub_property(propertyMessage('lastActive', curDate)) - } - } catch (err) { - log.error('发送心跳失败:', err) - } - }, 300000) - - // 启动用户定时通知提醒任务 - await updateJobs(bot, vika) - log.info('================================================\n\n登录启动成功,程序准备就绪\n\n================================================\n') - } else { - log.info('================================================\n\n登录启动成功,但没有配置维格表\n\n================================================\n') - await user.say(Prompts.b) - } -} - -async function onReady () { - const user: Contact = bot.currentUser - log.info('StarterBot', '%s ready', user) - log.info('当前登录的账号信息:', JSON.stringify(user)) - - if (isVikaOk) { - const curDate = new Date().toLocaleString() - await user.say('上线:' + curDate) - // 启动MQTT通道 - if (sysConfig.mqttPassword && (sysConfig.mqtt_SUB_ONOFF || sysConfig.mqtt_PUB_ONOFF)) { - chatdev = new ChatDevice(sysConfig.mqttUsername, sysConfig.mqttPassword, sysConfig.mqttEndpoint, sysConfig.mqttPort, user.id) - if (sysConfig.mqtt_SUB_ONOFF) { - chatdev.init(bot) - } - } - - // 更新云端好友和群 - await vika.updateRooms(bot) - await vika.updateContacts(bot) - - // 如果开启了MQTT推送,心跳同步到MQTT,每30s一次 - setInterval(() => { - try { - log.info(curDate) - if (chatdev && sysConfig.mqtt_PUB_ONOFF) { - chatdev.pub_property(propertyMessage('lastActive', curDate)) - } - } catch (err) { - log.error('发送心跳失败:', err) - } - }, 300000) - - // 启动用户定时通知提醒任务 - await updateJobs(bot, vika) - log.info('================================================\n\n登录启动成功,程序准备就绪\n\n================================================\n') - - } else { - log.info('================================================\n\n登录启动成功,但没有配置维格表\n\n================================================\n') - await user.say(Prompts.b) - } -} - -function onLogout (user: Contact) { - log.info('StarterBot', '%s logout', user) - // job.cancel() -} - -async function onMessage (message: Message) { - log.info('onMessage', JSON.stringify(message)) - const curDate = new Date().toLocaleString() - const talker = message.talker() - // const listener = message.listener() - const text = message.text() - const room = message.room() - const roomId = room?.id - const topic = await room?.topic() - const keyWord = bot.currentUser.name() - const isSelfMsg = message.self() - let isAdminRoom: boolean = false - log.info('keyWord is:', keyWord) - - if (isVikaOk) { - await vika.onMessage(message) - // MQTT上报 - if (chatdev && sysConfig.mqtt_PUB_ONOFF) { - /* - 将消息通过mqtt通道上报到云端 - */ - // chatdev.pub_message(message) - chatdev.pub_event(eventMessage('onMessage', { curDate })) - } - if (room || isSelfMsg) { - isAdminRoom = (topic !== undefined && topic === sysConfig.adminRoomTopic) || isSelfMsg - - if (isAdminRoom) { - await sendNotice(bot, message) - } - - let replyText: string = '' - if (isAdminRoom && (text === '#指令列表' || text === '#帮助')) { - replyText = `操作指令说明: - #更新配置 更新全部配置 - #更新提醒 更新定时提醒任务 - #更新通讯录 更新维格表通信录 - #下载通讯录 下载通讯录xlsx表 - #下载通知模板 下载通知模板` - - await relpy(bot, vika, replyText, message) - } - - if (isAdminRoom && text === '#更新配置') { - log.info('热更新系统配置~') - try { - sysConfig = await vika.getConfig() - // message.say('配置更新成功:' + JSON.stringify(newConfig)) - log.info('newConfig', sysConfig) - replyText = '配置更新成功~' - } catch (e) { - replyText = '配置更新成功~' - } - - await relpy(bot, vika, getNow() + replyText, message) - } - - if (isAdminRoom && text === '#更新提醒') { - log.info('热更新通知任务~') - try { - await updateJobs(bot, vika) - replyText = '提醒任务更新成功~' - } catch (e) { - replyText = '提醒任务更新失败~' - } - - await relpy(bot, vika, getNow() + replyText, message) - } - - if (isAdminRoom && text === '#更新通讯录') { - log.info('热更新通讯录到维格表~') - try { - await vika.updateContacts(bot) - await vika.updateRooms(bot) - replyText = '通讯录更新成功~' - } catch (e) { - replyText = '通讯录更新失败~' - } - - await relpy(bot, vika, getNow() + replyText, message) - } - - if (isAdminRoom && text === '#下载csv通讯录') { - log.info('下载通讯录到csv表~') - try { - const fileBox = await exportContactsAndRoomsToCSV() - await message.say(fileBox) - } catch (err) { - log.error('exportContactsAndRoomsToCSV', err) - await message.say('下载失败~') - } - } - - if (isAdminRoom && text === '#下载通讯录') { - log.info('下载通讯录到xlsx表~') - try { - const fileBox = await exportContactsAndRoomsToXLSX() - await message.say(fileBox) - } catch (err) { - log.error('exportContactsAndRoomsToXLSX', err) - } - } - - if (isAdminRoom && text === '#下载通知模板') { - log.info('下载通知模板~') - try { - const fileBox = FileBox.fromFile('./src/templates/群发通知模板.xlsx') - await message.say(fileBox) - } catch (err) { - log.error('下载模板失败', err) - await message.say('下载失败,请重试~') - } - } - - if (isAdminRoom && text === '#初始化') { - log.info('初始化系统~') - try { - await vika.init() - await message.say('初始化系统表完成~') - } catch (err) { - log.error('初始化系统失败', err) - await message.say('初始化系统失败,请重试~') - } - } - - } - - try { - - if (room && roomId && !isSelfMsg) { - - // 检测顺风车信息并格式化 - // const KEYWORD_LIST = [ '人找车', '车找人' ] - // try { - // // 判断消息中是否包含关键字 - // if (KEYWORD_LIST.some(keyword => message.text().includes(keyword))) { - // const replyMsg = await getFormattedRideInfo(message) - // if (replyMsg) { - // const replyText = replyMsg.choices[0].message.content.replace(/\r/g, '') - // log.info('回复内容:', replyText) - // await room.say(replyText) - // } - // } - // } catch (err) { - - // } - - // 智能问答开启时执行 - if (sysConfig.WX_OPENAI_ONOFF && ((text.indexOf(keyWord) !== -1 && sysConfig.AT_AHEAD) || !sysConfig.AT_AHEAD)) { - if (sysConfig.roomWhiteListOpen) { - const isInRoomWhiteList = sysConfig.roomWhiteList.includes(roomId) - if (isInRoomWhiteList) { - log.info('当前群在白名单内,请求问答...') - await wxai(sysConfig, bot, talker, room, message) - } else { - log.info('当前群不在白名单内,流程结束') - } - } - - if (!sysConfig.roomWhiteListOpen) { - log.info('系统未开启白名单,请求问答...') - await wxai(sysConfig, bot, talker, room, message) - } - } - - // IM服务开启时执行 - if (sysConfig.imOpen && types.Message.Text === message.type()) { - configData.clientChatEn.clientChatId = talker.id + ' ' + room.id - configData.clientChatEn.clientChatName = talker.name() + '@' + topic - // log.debug(configData) - socket.emit('CLIENT_ON', { - clientChatEn: configData.clientChatEn, - serverChatId: configData.serverChatEn.serverChatId, - }) - const data = { - msg: { - avatarUrl: '/static/image/im_server_avatar.png', - content: text, - contentType: 'text', - role: 'client', - }, - } - log.info(JSON.stringify(data)) - sendMsg(data) - } - - } - - if ((!room || !room.id) && !isSelfMsg) { - // 智能问答开启时执行 - if (sysConfig.WX_OPENAI_ONOFF && ((text.indexOf(keyWord) !== -1 && sysConfig.AT_AHEAD) || !sysConfig.AT_AHEAD)) { - if (sysConfig.contactWhiteListOpen) { - const isInContactWhiteList = sysConfig.contactWhiteList.includes(talker.id) - if (isInContactWhiteList) { - log.info('当前好友在白名单内,请求问答...') - await wxai(sysConfig, bot, talker, undefined, message) - } else { - log.info('当前好友不在白名单内,流程结束') - } - } - - if (!sysConfig.contactWhiteListOpen) { - log.info('系统未开启好友白名单,对所有好友有效,请求问答...') - await wxai(sysConfig, bot, talker, undefined, message) - } - } - } - - } catch (e) { - log.error('发起请求wxai失败', e) - } - } else { - if (message.self() && message.type() === types.Message.Text && text !== Prompts.a && text !== Prompts.b && text.includes('+')) { - try { - const textArr = text.split('+') - log.info(JSON.stringify(textArr)) - if (text.length > 23 && text.length < 33 && textArr.length === 2) { - vikaConfig.spaceName = textArr[1] - vikaConfig.token = textArr[0] - await createVika() - isVikaOk = true - config.baseConfig.VIKA_TOKEN = vikaConfig.token - config.baseConfig.VIKA_SPACENAME = vikaConfig.spaceName - await updateConfig(config) - await talker.say('配置成功,初始化中,请稍后...') - log.info('初始化系统~') - try { - await vika.init() - await talker.say('初始化系统表完成~') - } catch (err) { - log.error('初始化系统失败', err) - await talker.say('初始化系统失败,请发送 #初始化 重试~') - } - } else { - await talker.say(Prompts.a) - } - } catch (err) { - log.error('解析失败:', err) - await talker.say(Prompts.a) - } - } - } -} - -async function roomJoin (room: { topic: () => any; id: any; say: (arg0: string, arg1: any) => any }, inviteeList: Contact[], inviter: any) { - const nameList = inviteeList.map(c => c.name()).join(',') - log.info(`Room ${await room.topic()} got new member ${nameList}, invited by ${inviter}`) - - // 进群欢迎语,仅对开启了进群欢迎语白名单的群有效 - if (isVikaOk && sysConfig.welcomeList.includes(room.id) && inviteeList.length) { - await room.say(`欢迎加入${await room.topic()},请阅读群公告~`, inviteeList) - } -} - -async function onError (err: any) { - log.error('bot.onError:', JSON.stringify(err)) - // try { - // // job.cancel() - // } catch (e) { - // log.error('销毁定时任务失败:', JSON.stringify(e)) - // } -} - -async function main (vika: any) { - - // 检查维格表配置并启动 - if (vikaConfig.spaceName && vikaConfig.token) { - try { - await createVika() - isVikaOk = true - } catch (err) { - log.info('初始化vika失败:', err) - } - - } else { - log.error('\n================================================\n\nvikaConfig配置不全,请重新配置config.json文件中的token和spaceName之后重启,或者根据提示进行配置\n\n================================================\n') - } - - bot = getBot() - bot.on('scan', onScan) - - if (puppet === 'wechaty-puppet-xp') { - bot.on('login', onLogin) - } - if (puppet !== 'wechaty-puppet-xp') { - bot.on('ready', onReady) - } - - bot.on('logout', onLogout) - bot.on('message', onMessage) - bot.on('room-join', roomJoin) - bot.on('error', onError) - - bot.start() - .then(() => log.info('Starter Bot Started.')) - .catch((e: any) => log.error('bot运行异常:', JSON.stringify(e))) - - if (isVikaOk && sysConfig.imOpen) { - socket = imclient(bot, vika, configData) - } -} - -void main(vika) diff --git a/src/init.ts b/src/init.ts deleted file mode 100644 index c65cfa80..00000000 --- a/src/init.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable sort-keys */ -/* eslint-disable no-console */ - -import { VikaBot } from './plugins/vika.js' -import { baseConfig } from './config.js' - -const vikaConfig = { - spaceName: baseConfig['VIKA_SPACENAME'] || process.env['VIKA_SPACENAME'], - token: baseConfig['VIKA_TOKEN'] || process.env['VIKA_TOKEN'], -} -const vika = new VikaBot(vikaConfig) - -async function init (): Promise { - await vika.init() -} - -// async function getFields (datasheetId: string): Promise { -// await vika.getSheetFields(datasheetId) -// } - -// void getFields('dstKiDu2sEAXJGvsJR') - -void init() diff --git a/src/lib/tool.ts b/src/lib/tool.ts deleted file mode 100644 index 3ee4bfa3..00000000 --- a/src/lib/tool.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable sort-keys */ -import moment from 'moment' - -import type { - Contact, - Room, - // ScanStatus, - // WechatyBuilder, -} from 'wechaty' - -async function formatSentMessage (userSelf: Contact, text: string, talker?: Contact, room?: Room) { - // console.debug('发送的消息:', text) - const curTime = new Date().getTime() - const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss') - const record = { - fields: { - timeHms, - name: userSelf.name(), - topic: room ? (await room.topic() || '--') : (talker?.name() || '--'), - messagePayload: text, - wxid: room && talker ? (talker.id !== 'null' ? talker.id : '--') : userSelf.id, - roomid: room ? (room.id || '--') : (talker?.id || '--'), - messageType: 'selfSent', - }, - } - return record -} - -// 定义一个延时方法 -const waitForMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) - -export { - waitForMs, - formatSentMessage, -} - -export default waitForMs diff --git a/src/mods/mod.ts b/src/mods/mod.ts deleted file mode 100644 index eff420db..00000000 --- a/src/mods/mod.ts +++ /dev/null @@ -1 +0,0 @@ -export * as types from './types.js' diff --git a/src/mods/types.ts b/src/mods/types.ts deleted file mode 100644 index 9ec87177..00000000 --- a/src/mods/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { - AppConfig, - AppConfigs, - BotConfig, - ContactConfig, - RoomConfig, - Config, - SysConfig, -} from '../schemas/mod.js' - -export { - type AppConfig, - type AppConfigs, - type BotConfig, - type ContactConfig, - type RoomConfig, - type Config, - type SysConfig, -} diff --git a/src/plugins/basic-data-storage-for-vika.ts b/src/plugins/basic-data-storage-for-vika.ts new file mode 100644 index 00000000..c003576a --- /dev/null +++ b/src/plugins/basic-data-storage-for-vika.ts @@ -0,0 +1,77 @@ +/** + * Author: Huan LI https://github.com/huan + * Date: Apr 2020 + */ +/* eslint-disable sort-keys */ +import { + Wechaty, + WechatyPlugin, + Message, + types, + log, +} from 'wechaty' + +export interface DingDongConfigObject { + /** + * Whether response to the self message + */ + self: boolean, + /** + * Whether response the Room Message with mention self. + * Default: true + */ + mention: boolean, +} + +export type DingDongConfig = Partial + +const DEFAULT_CONFIG: DingDongConfigObject = { + mention : false, + self : true, +} + +const isMatchConfig = (config: DingDongConfigObject) => { + log.verbose('DingDong', ' isMatchConfig(%s)', JSON.stringify(config)) + + return async function isMatch (message: Message) { + log.verbose('DingDong', 'isMatchConfig() isMatch(%s)', message.toString()) + + return true + } +} + +function DingDong (config?: DingDongConfig): WechatyPlugin { + log.verbose('DingDong', 'DingDong(%s)', + typeof config === 'undefined' ? '' + : typeof config === 'function' ? 'function' + : JSON.stringify(config), + ) + + const normalizedConfig: DingDongConfigObject = { + ...DEFAULT_CONFIG, + ...config, + } + + const isMatch = isMatchConfig(normalizedConfig) + + return function DingDongPlugin (wechaty: Wechaty) { + log.verbose('DingDong', 'installing on %s ...', wechaty) + + wechaty.on('message', async message => { + if (message.type() !== types.Message.Text) { + return + } + + if (!await isMatch(message)) { + return + } + + await message.say('dong') + }) + } + +} + +export { + DingDong, +} diff --git a/src/plugins/bilingual-translation-assistant-for-room-chat.ts b/src/plugins/bilingual-translation-assistant-for-room-chat.ts new file mode 100644 index 00000000..67fd071e --- /dev/null +++ b/src/plugins/bilingual-translation-assistant-for-room-chat.ts @@ -0,0 +1,155 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty - Conversational RPA SDK for Chatbot Makers. + * - https://github.com/wechaty/wechaty + */ +// https://stackoverflow.com/a/42817956/1123955 +// https://github.com/motdotla/dotenv/issues/89#issuecomment-587753552 +import 'dotenv/config.js' + +import { + Contact, + Message, + ScanStatus, + types, + WechatyBuilder, + log, +} from 'wechaty' + +import qrcodeTerminal from 'qrcode-terminal' +import Api2d from 'api2d' +import fs from 'fs' + +// 设置白名单 +let whitelist = new Set() +const whitelistPath = 'src/plugins/whitelist.json' + +// 从文件中读取白名单 +if (fs.existsSync(whitelistPath)) { + const data = fs.readFileSync(whitelistPath, { encoding: 'utf-8' }) + whitelist = new Set(JSON.parse(data || '[]')) +} + +// 定时保存白名单到文件 +setInterval(() => { + const data = JSON.stringify([ ...whitelist ]) + fs.writeFileSync(whitelistPath, data, { encoding: 'utf-8' }) +}, 5000) + +/** + * @param {string} messages + * @returns {string} + */ +async function getChatGPTReply (message: string) { + const systemPrompt = '你是一个翻译器,用户将输入{"text":"你要翻译的内容..."},你只需要翻译text对应的value内容。如果输入的是中文你翻译成英文,如果输入的是英文你翻译成中文。例如输入是:{"text":"你是谁"},你的回到是:who are you(只返回翻译结果,而不是返回{"text":"who are you"}格式)' + const apiKey = process.env['OPENAI_API_KEY'] || 'your key' + // const apiEndpoint = 'https://api.openai.com' + const apiEndpoint = 'https://api.openai-proxy.com' + try { + const api = new Api2d(apiKey, apiEndpoint, 60 * 1000) + const body = { + max_tokens:2048, + messages:[ + { content:systemPrompt, role:'system' }, + { content:`{"text":"${message}"}`, role:'user' }, + ], + model: 'gpt-3.5-turbo', + n: 1, + stream: false, + temperature: 1, + } + log.info('body:', JSON.stringify(body)) + const completion: any = await api.completion(body) + const responseMessage = completion + return responseMessage.choices[0].message.content + } catch (err) { + console.error(err) + return '发生了一些错误,请稍后再试~' + } +} + +function onScan (qrcode: string, status: ScanStatus) { + if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) { + const qrcodeImageUrl = [ + 'https://wechaty.js.org/qrcode/', + encodeURIComponent(qrcode), + ].join('') + log.info('StarterBot', 'onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl) + + qrcodeTerminal.generate(qrcode, { small: true }) // show qrcode on console + + } else { + log.info('StarterBot', 'onScan: %s(%s)', ScanStatus[status], status) + } +} + +function onLogin (user: Contact) { + log.info('StarterBot', '%s login', user) +} + +function onLogout (user: Contact) { + log.info('StarterBot', '%s logout', user) +} + +async function onMessage (message: Message) { + log.info('onMessage', JSON.stringify(message)) + const type = message.type() + const text = message.text() + const room = message.room() + + if (!room) { + return + } + + const roomId = room.id + + const mentionSelf = await message.mentionSelf() + + if (type === types.Message.Text && mentionSelf) { + const mentionText = text.replace(`@${bot.currentUser.name()}`, '').trim() + + if (mentionText === '#开启互译') { + if (whitelist.has(roomId)) { + log.info(`群"${await room.topic()}"已在白名单中,无需重复添加`) + await message.say('已在白名单中,无需重复添加') + } else { + whitelist.add(roomId) + log.info(`已将群"${await room.topic()}"加入到白名单`, JSON.stringify(whitelist)) + await message.say('已开启双语互译') + } + } else if (mentionText === '#关闭互译') { + if (whitelist.has(roomId)) { + whitelist.delete(roomId) + log.info(`已将群"${await room.topic()}"从白名单中移除`) + await message.say('已关闭双语互译') + } else { + log.info(`群"${await room.topic()}"不在白名单中,无需移除`) + await message.say('未开启双语互译,无需关闭') + } + } + } + + if (type === types.Message.Text && whitelist.has(roomId)) { + log.info(`收到来自群"${await room.topic()}"的文本消息:"${text}"`) + try { + const reply = await getChatGPTReply(text) + await message.say(reply) + } catch (err) { + log.error('err', err) + } + } +} + +const bot = WechatyBuilder.build({ + name: 'ding-dong-bot', + puppet: 'wechaty-puppet-xp', +}) + +bot.on('scan', onScan) +bot.on('login', onLogin) +bot.on('logout', onLogout) +bot.on('message', onMessage) + +bot.start() + .then(() => log.info('StarterBot', 'Starter Bot Started.')) + .catch(e => log.error('StarterBot', e)) diff --git a/src/plugins/chat-device.ts b/src/plugins/chat-device.ts index 71dfaf05..f28be501 100644 --- a/src/plugins/chat-device.ts +++ b/src/plugins/chat-device.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-empty */ -/* eslint-disable sort-keys */ import mqtt from 'mqtt' import { v4 } from 'uuid' import { FileBox } from 'file-box' @@ -18,7 +16,7 @@ import { wechaty2chatdev, propertyMessage, eventMessage } from './msg-format.js' import { // waitForMs as wait, formatSentMessage, -} from '../util/tool.js' +} from '../utils/utils.js' class ChatDevice { @@ -33,9 +31,9 @@ class ChatDevice { constructor (username:string, password:string, endpoint:string, port:string|number, botId:string) { this.mqttclient = mqtt.connect(`mqtt://${endpoint}:${port || 1883}`, { - username, - password, clientId: v4(), + password, + username, }) this.isConnected = false this.propertyApi = `thing/chatbot/${botId}/property/post` @@ -53,7 +51,7 @@ class ChatDevice { log.info('================================================\n\nMQTT连接成功~\n\n================================================\n') }) this.mqttclient.on('reconnect', function (e:any) { - log.info('subscriber on reconnect') + log.info('subscriber on reconnect', e) }) this.mqttclient.on('disconnect', function (e:any) { log.info('disconnect--------', e) @@ -104,21 +102,25 @@ class ChatDevice { const params = message.params if (name === 'start') { - + log.info('cmd name:', name) } if (name === 'stop') { - + log.info('cmd name:', name) } if (name === 'logout') { + log.info('cmd name:', name) } if (name === 'logonoff') { + log.info('cmd name:', name) } if (name === 'userSelf') { + log.info('cmd name:', name) } if (name === 'say') { + log.info('cmd name:', name) } if (name === 'send') { @@ -129,33 +131,42 @@ class ChatDevice { } if (name === 'aliasGet') { + log.info('cmd name:', name) } if (name === 'aliasSet') { + log.info('cmd name:', name) } if (name === 'roomCreate') { await createRoom(params, this.chatbot) } if (name === 'roomAdd') { + log.info('cmd name:', name) } if (name === 'roomDel') { + log.info('cmd name:', name) } if (name === 'roomAnnounceGet') { + log.info('cmd name:', name) } if (name === 'roomAnnounceSet') { + log.info('cmd name:', name) } if (name === 'roomQuit') { + log.info('cmd name:', name) } if (name === 'roomTopicGet') { + log.info('cmd name:', name) } if (name === 'roomTopicSet') { + log.info('cmd name:', name) } if (name === 'roomQrcodeGet') { @@ -163,27 +174,33 @@ class ChatDevice { } if (name === 'memberAllGet') { + log.info('cmd name:', name) } if (name === 'contactAdd') { + log.info('cmd name:', name) } if (name === 'contactAliasSet') { + log.info('cmd name:', name) } if (name === 'contactFindAll') { await getAllContact(this.chatdevice, this.chatbot) } if (name === 'contactFind') { + log.info('cmd name:', name) } if (name === 'roomFindAll') { await getAllRoom(this.chatdevice, this.chatbot) } if (name === 'roomFind') { + log.info('cmd name:', name) } if (name === 'config') { + log.info('cmd name:', name) } @@ -203,11 +220,11 @@ async function getAllContact (chatdevice:any, bot:Wechaty) { } const contactInfo = { - id: contact?.id, - gender: contact?.gender() || '', - name: contact?.name() || '', alias: await contact?.alias() || '', avatar, + gender: contact?.gender() || '', + id: contact?.id, + name: contact?.name() || '', } friends.push(contactInfo) @@ -244,7 +261,7 @@ async function getAllRoom (chatdevice:any, bot:Wechaty) { chatdevice.pub_property(msg) } -async function send (params:any, bot:Wechaty) { +async function send (params:any, bot:Wechaty): Promise { log.info('params:', params) let msg:any = '' diff --git a/auto-linux.sh b/src/plugins/config.json similarity index 100% rename from auto-linux.sh rename to src/plugins/config.json diff --git a/src/plugins/connectors/chatgpt.ts b/src/plugins/connectors/chatgpt.ts index bfaede40..eb9d759d 100644 --- a/src/plugins/connectors/chatgpt.ts +++ b/src/plugins/connectors/chatgpt.ts @@ -7,7 +7,7 @@ import { // nlp, // QueryData, genToken, -} from '../sdk/openai/index.js' +} from '../../sdk/openai/index.js' import { FileBox } from 'file-box' // import excel2order from '../excel.js' @@ -29,7 +29,7 @@ import path from 'path' import { waitForMs as wait, formatSentMessage, -} from '../util/tool.js' +} from '../../utils/utils.js' import { ChatGPTAPI } from 'chatgpt' @@ -91,12 +91,12 @@ async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | answer = answer.text + '\n' // console.debug(answer) await room.say(answer, ...[ talker ]) - formatSentMessage(bot.currentUser, answer, undefined, room) + await formatSentMessage(bot.currentUser, answer, undefined, room) } else { answer = answer.text + '\n' await message.say(answer) - formatSentMessage(bot.currentUser, answer, message.talker(), undefined) + await formatSentMessage(bot.currentUser, answer, message.talker(), undefined) } break @@ -106,10 +106,10 @@ async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | if (room) { await room.say(fileBox) - formatSentMessage(bot.currentUser, fileBox.toString(), undefined, room) + await formatSentMessage(bot.currentUser, fileBox.toString(), undefined, room) } else { await message.say(fileBox) - formatSentMessage(bot.currentUser, fileBox.toString(), message.talker(), undefined) + await formatSentMessage(bot.currentUser, fileBox.toString(), message.talker(), undefined) } break @@ -127,11 +127,11 @@ async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | if (room) { await room.say(miniProgram) - formatSentMessage(bot.currentUser, miniProgram.toString(), undefined, message.room()) + await formatSentMessage(bot.currentUser, miniProgram.toString(), undefined, message.room()) } else { await message.say(miniProgram) - formatSentMessage(bot.currentUser, miniProgram.toString(), message.talker(), undefined) + await formatSentMessage(bot.currentUser, miniProgram.toString(), message.talker(), undefined) } break @@ -152,7 +152,7 @@ async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | // answer = await aibot(talker, room, text) if (fileName.split('.')[1] === 'xlsx') { // log.info('file=============', file) - const filePath = __dirname + `\\cache\\${new Date().getTime() + fileName}` + const filePath = __dirname + `\\data\\media\\image\\${new Date().getTime() + fileName}` // let filePath = `C:\\Users\\wechaty\\Documents\\WeChat Files\\wxid_0o1t51l3f57221\\FileStorage\\File\\2022-05\\${file.name}` await file.toFile(filePath) await wait(1000) @@ -194,8 +194,8 @@ async function aibot (sysConfig: any, talker: any, room: any, query: any) { case 'WxOpenai': // log.info('开始请求微信对话平台...') init({ - EncodingAESKey: sysConfig.EncodingAESKey, - TOKEN: sysConfig.WX_TOKEN, + EncodingAESKey: sysConfig.WXOPENAI_ENCODINGAESKEY, + TOKEN: sysConfig.WXOPENAI_TOKEN, }) try { diff --git a/src/plugins/connectors/wx-openai.ts b/src/plugins/connectors/wx-openai.ts index 83d53ed5..8134e98b 100644 --- a/src/plugins/connectors/wx-openai.ts +++ b/src/plugins/connectors/wx-openai.ts @@ -7,7 +7,7 @@ import { // nlp, // QueryData, genToken, -} from '../sdk/openai/index.js' +} from '../../sdk/openai/index.js' import { FileBox } from 'file-box' // import excel2order from '../excel.js' @@ -29,7 +29,7 @@ import path from 'path' import { waitForMs as wait, formatSentMessage, -} from '../util/tool.js' +} from '../../utils/utils.js' import { ChatGPTAPI } from 'chatgpt' @@ -91,12 +91,12 @@ async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | answer = answer.text + '\n' // console.debug(answer) await room.say(answer, ...[ talker ]) - formatSentMessage(bot.currentUser, answer, undefined, room) + await formatSentMessage(bot.currentUser, answer, undefined, room) } else { answer = answer.text + '\n' await message.say(answer) - formatSentMessage(bot.currentUser, answer, message.talker(), undefined) + await formatSentMessage(bot.currentUser, answer, message.talker(), undefined) } break @@ -106,10 +106,10 @@ async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | if (room) { await room.say(fileBox) - formatSentMessage(bot.currentUser, fileBox.toString(), undefined, room) + await formatSentMessage(bot.currentUser, fileBox.toString(), undefined, room) } else { await message.say(fileBox) - formatSentMessage(bot.currentUser, fileBox.toString(), message.talker(), undefined) + await formatSentMessage(bot.currentUser, fileBox.toString(), message.talker(), undefined) } break @@ -127,11 +127,11 @@ async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | if (room) { await room.say(miniProgram) - formatSentMessage(bot.currentUser, miniProgram.toString(), undefined, message.room()) + await formatSentMessage(bot.currentUser, miniProgram.toString(), undefined, message.room()) } else { await message.say(miniProgram) - formatSentMessage(bot.currentUser, miniProgram.toString(), message.talker(), undefined) + await formatSentMessage(bot.currentUser, miniProgram.toString(), message.talker(), undefined) } break @@ -152,7 +152,7 @@ async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | // answer = await aibot(talker, room, text) if (fileName.split('.')[1] === 'xlsx') { // log.info('file=============', file) - const filePath = __dirname + `\\cache\\${new Date().getTime() + fileName}` + const filePath = __dirname + `\\data\\media\\image\\${new Date().getTime() + fileName}` // let filePath = `C:\\Users\\wechaty\\Documents\\WeChat Files\\wxid_0o1t51l3f57221\\FileStorage\\File\\2022-05\\${file.name}` await file.toFile(filePath) await wait(1000) @@ -193,8 +193,8 @@ async function aibot (sysConfig: any, talker: any, room: any, query: any) { case 'WxOpenai': // log.info('开始请求微信对话平台...') init({ - EncodingAESKey: sysConfig.EncodingAESKey, - TOKEN: sysConfig.WX_TOKEN, + EncodingAESKey: sysConfig.WXOPENAI_ENCODINGAESKEY, + TOKEN: sysConfig.WXOPENAI_TOKEN, }) try { diff --git a/src/plugins/contact-room.ts b/src/plugins/contact-room.ts new file mode 100644 index 00000000..469ae8a5 --- /dev/null +++ b/src/plugins/contact-room.ts @@ -0,0 +1,83 @@ +import XLSX from 'xlsx' +import csv from 'fast-csv' +import { + Contact, + log, + Wechaty, +} from 'wechaty' + +import { FileBox } from 'file-box' +import { createWriteStream } from 'fs' + +// 导出联系人和群列表到csv文件 +export async function exportContactsAndRoomsToCSV (bot:Wechaty) { + // 获取所有联系人和群聊 + const contacts = await bot.Contact.findAll() + const rooms = await bot.Room.findAll() + + // 准备CSV数据 + const csvData = [] + contacts.forEach((contact: Contact) => { + if (contact.friend()) { + csvData.push({ ID: contact.id, Name: Buffer.from(contact.name(), 'utf-8').toString() || '未知', Type: 'Contact' }) + } + }) + + for (const room of rooms) { + csvData.push({ ID: room.id, Name: Buffer.from(await room.topic(), 'utf-8').toString() || '未知', Type: 'Room' }) + } + + log.info('通讯录原始数据:', csvData) + + const fileName = './db/contacts_and_rooms.csv' + const writeStream = createWriteStream(fileName) + const csvStream = csv.format({ headers: true }) + csvStream.pipe(writeStream).on('end', () => { + log.info('CSV file written successfully') + }) + + csvData.forEach((item) => { + csvStream.write(item) + }) + + csvStream.end() + + // 返回FileBox对象 + return FileBox.fromFile(fileName) +} + +// 导出联系人和群列表到xlsx文件 +export async function exportContactsAndRoomsToXLSX (bot:Wechaty) { + // 获取所有联系人和群聊 + const contacts = await bot.Contact.findAll() + const rooms = await bot.Room.findAll() + + // 准备联系人和群聊数据 + const contactsData = [ [ 'Name', 'ID' ] ] + const roomsData = [ [ 'Name', 'ID' ] ] + contacts.forEach((contact) => { + if (contact.friend()) { + contactsData.push([ contact.name(), contact.id ]) + } + }) + + for (const room of rooms) { + roomsData.push([ await room.topic(), room.id ]) + } + + // 创建一个新的工作簿 + const workbook = XLSX.utils.book_new() + + // 将数据添加到工作簿的不同sheet中 + const contactsSheet = XLSX.utils.aoa_to_sheet(contactsData) + const roomsSheet = XLSX.utils.aoa_to_sheet(roomsData) + XLSX.utils.book_append_sheet(workbook, contactsSheet, 'Contacts') + XLSX.utils.book_append_sheet(workbook, roomsSheet, 'Rooms') + + // 将工作簿写入文件 + const fileName = './db/contacts_and_rooms.xlsx' + XLSX.writeFile(workbook, fileName) + + // 返回FileBox对象 + return FileBox.fromFile(fileName) +} diff --git a/src/plugins/excel.d.ts b/src/plugins/excel.d.ts deleted file mode 100644 index 93e34f4a..00000000 --- a/src/plugins/excel.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module '*' -declare module './excel.js' diff --git a/src/plugins/excel.js b/src/plugins/excel.js deleted file mode 100644 index ca4aa337..00000000 --- a/src/plugins/excel.js +++ /dev/null @@ -1,283 +0,0 @@ -/* eslint-disable no-undef */ -/* eslint-disable sort-keys */ -/* eslint-disable @typescript-eslint/no-misused-promises */ -/* eslint-disable eqeqeq */ -/* eslint-disable no-console */ -import nodeXlsx from 'node-xlsx' -import ExcelJS from 'exceljs' -import fs from 'fs' -import { FileBox } from 'file-box' -import path from 'path' -import { VikaBot } from './vika.js' -const __dirname = path.resolve() - -async function excel2order (filepath, message) { - // console.debug('文件路径:', filepath) - const s = { - fill: { - fgColor: { rgb: 'FFCC33' }, // 16进制,注意要去掉# - }, - } - - const sheets = nodeXlsx.parse(filepath) - // console.debug(sheets) - // 解析所有sheet - if (sheets.length === 6) { - sheets.forEach(async sheet => { - // sheet.data是所有行数据 - const rows = sheet.data - const name = sheet.name - console.debug(name) - if (name == '顾客购买表(商品列排)') { - console.log(rows.length) - const keys = rows[0] - const keysLength = keys.length - const orders = {} - const rowLength = keys.length - rows.shift() - rows.pop() - rows.sort(function (a, b) { - return String(a[keysLength - 1]) - String(b[keysLength - 1]) - }) - const num = { - - } - for (let i = 0; i < rows.length; i++) { - - // console.log(`第${i + 1}行数据:${rows[i]}`) - const row = rows[i] - row[rowLength - 1] = String(row[rowLength - 1]) - const order = {} - for (const y in row) { - order[y] = row[y] - } - // order.index = i - // console.debug(order)louOrders - // console.debug(order[0]) - - if (Object.keys(orders).includes(order[rowLength - 1])) { - const qiOrders = orders[order[rowLength - 1]] - - if (Object.keys(qiOrders).includes(order[rowLength - 2])) { - const louOrders = qiOrders[order[rowLength - 2]] - louOrders.push(order) - louOrders.sort(function (a, b) { - return a[rowLength - 3] - b[rowLength - 3] - }) - qiOrders[order[rowLength - 2]] = louOrders - orders[order[rowLength - 1]] = qiOrders - - } else { - const louOrders = [order] - qiOrders[order[rowLength - 2]] = louOrders - orders[order[rowLength - 1]] = qiOrders - - } - - } else { - const qiOrders = {} - qiOrders[order[rowLength - 2]] = [order] - orders[order[rowLength - 1]] = qiOrders - // console.debug(JSON.stringify(orders)) - - } - - } - // console.debug(JSON.stringify(orders)) - const newList = [] - const excelData = [] - - // 添加数据 - for (const i in orders) { - // newList.push(i + '期') - const addInfo = {} - // 名称 - addInfo.name = i + '期(弄)' - - // 固定表头 - addInfo.data = [ - [keys[keysLength - 2], keys[keysLength - 3], keys[keysLength - 10]], - ] - - const qicount = { - - } - - // 表头及计数初始化 - for (let g = 4; g < keys.length - 19; g++) { - addInfo.data[0].push(keys[g]) - qicount[g] = 0 - } - - const qi = orders[i] - for (const j in qi) { - // newList.push(j + '号楼') - const loucount = { - - } - - for (let g = 4; g < keys.length - 19; g++) { - loucount[g] = 0 - } - - const lou = qi[j] - - for (const x in lou) { - const shi = lou[x] - for (let g = 4; g < keys.length - 19; g++) { - loucount[g] = loucount[g] + shi[g] - } - newList.push(shi) - const shiorder = [shi[rowLength - 2] + '号楼', shi[rowLength - 3], shi[rowLength - 10]] - - for (let g = 4; g < keys.length - 19; g++) { - shiorder.push(shi[g] || 0) - } - addInfo.data.push(shiorder) - } - - const count = [`${j}号楼小计:`, '', ''] - const blankRow = ['', '', ''] - const titleRow = [keys[keysLength - 2], keys[keysLength - 3], keys[keysLength - 10]] - - for (let g = 4; g < keys.length - 19; g++) { - count.push(loucount[g] || 0) - blankRow.push('') - titleRow.push(keys[g]) - qicount[g] = qicount[g] + loucount[g] - } - console.debug(JSON.stringify(count)) - addInfo.data.push(count) - addInfo.data.push(blankRow) - addInfo.data.push(titleRow) - - } - // console.debug(JSON.stringify(qicount)) - - const count = ['合计', '', ''] - for (let g = 4; g < keys.length - 19; g++) { - count.push(qicount[g] || 0) - } - // console.debug('合计-------------------', count) - - addInfo.data.push(count) - excelData.push(JSON.parse(JSON.stringify(addInfo))) - } - // console.debug(excelData) - console.debug(newList.length) - // 写入Excel数据 - try { - // 写xlsx - const buffer = nodeXlsx.build(excelData) - let newpath = __dirname + `\\cache\\汇总单_${path.basename(filepath)}` - // const newpath = 'C:\\Users\\wechaty\\Documents\\GitHub\\wechat-openai-qa-bot\\data1652169999200.xls' - // console.info('newpath==================================', newpath) - // 写入数据 - fs.writeFile(newpath, buffer, async function (err) { - if (err) { - throw err - } - // 输出日志 - console.log('Write to xls has finished') - await xlsxrw(newpath) - - const fileBox = FileBox.fromFile(newpath) - console.log(fileBox) - if (message) { - await message.say('转换成功,请下载查看~') - await message.say(fileBox) - newpath = '' - message = '' - } - }) - - } catch (e) { - await message.say('格式转换失败:\n1.请检查原始表格是否正确\n2.仅支持从快团团默认导出的全量字段表格\n3.表格中必须须包含 顾客购买表(商品列排) sheet\n4.文件名中不能包含括号等特殊字符,建议使用导出的原始文件名') - // 输出日志 - console.log('excel写入异常,error=%s', e.stack) - return e - } - } - - }) - } -} - -// let demopath = 'tools/订单20_47_20.xlsx' -// excel2order(demopath) - -async function xlsxrw (filename) { - const workbook = new ExcelJS.Workbook() - await workbook.xlsx.readFile(filename) - workbook.eachSheet(function (worksheet, sheetId) { - - // 遍历工作表中的所有行(包括空行) - worksheet.eachRow({ includeEmpty: true }, function (row, rowNumber) { - // console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values)) - row.height = 30 - - row.eachCell({ includeEmpty: true }, function (cell, colNumber) { - // console.log('Cell ' + colNumber + ' = ' + cell.value) - if (row.getCell(1).value) { - // 在A1周围设置单个细边框 - cell.border = { - top: { style:'thin' }, - left: { style:'thin' }, - bottom: { style:'thin' }, - right: { style:'thin' }, - } - cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true } - } - - if (rowNumber === 1) { - row.height = 20 - // console.log('Cell ' + colNumber + ' = ' + cell.value) - if (colNumber < 3) { - worksheet.getColumn(colNumber).width = 10 - } else { - worksheet.getColumn(colNumber).width = 20 - } - - cell.alignment = { wrapText: true } - // cell.font = { - // bold: true, - // } - - } - if (row.getCell(1).value && row.getCell(1).value.includes('小计')) { - // 遍历一行中的所有单元格(包括空单元格) - console.log('row ', JSON.stringify(row.values)) - // console.log('Cell ' + colNumber + ' = ' + cell.value) - if (colNumber < 3) { - cell.font = { - bold: true, - } - cell.fill = { - type: 'pattern', - pattern: 'darkTrellis', - fgColor: { argb: 'FFFFFF00' }, - bgColor: { argb: 'FF0000FF' }, - } - } else { - cell.font = { - bold: true, - } - cell.fill = { - type: 'pattern', - pattern: 'darkTrellis', - fgColor: { argb: 'FFFFFF00' }, - bgColor: { argb: 'FF0000FF' }, - } - } - - } - - }) - }) - - }) - - await workbook.xlsx.writeFile(filename) -} - -export default excel2order diff --git a/src/plugins/finder.ts b/src/plugins/finder.ts new file mode 100644 index 00000000..6528f882 --- /dev/null +++ b/src/plugins/finder.ts @@ -0,0 +1,122 @@ +import { Wechaty, Room, Contact, log } from 'wechaty' + +export type BusinessUser = { + alias?: string; + id?: string; + name: string; + }; + +// 优先级:好友ID、备注名称、昵称 +export const getContact = async (bot:Wechaty, businessUser:BusinessUser) => { + + // 查找联系人 + let contact:Contact|undefined + let contactList:Contact[] + + if (businessUser.id) { + contact = await bot.Contact.find({ id:businessUser.id }) + } else if (businessUser.alias) { + contact = await bot.Contact.find({ alias:businessUser.alias }) + } else { + contactList = await bot.Contact.findAll({ name:businessUser.name }) + if (contactList.length > 0) { + contact = contactList[0] + } else { + log.info('getContact:', '昵称没有匹配到好友') + } + } + + return contact +} + +export const isThisContact = async (businessUser:BusinessUser, contact:Contact) => { + // 判断联系人 + const alias = await contact.alias() + if (businessUser.id && contact.id === businessUser.id) { + return true + } else if (businessUser.alias && alias === businessUser.alias) { + return true + } else { + if (businessUser.name && contact.name() === businessUser.name) { + return true + } else { + return false + } + } +} + +export type BusinessRoom = { + id?:string; + luckyDog?:string; + memberAlias?: string; + topic: string; + }; + +// 优先级:群ID、机器人昵称、指定好友、群名称 +export const getRoom = async (bot:Wechaty, businessRoom:BusinessRoom) => { + // 查找联系人 + let room:Room|undefined + let roomList:Room[] + + if (businessRoom.id) { + room = await bot.Room.find({ id:businessRoom.id }) + } else if (businessRoom.memberAlias) { + roomList = await bot.Room.findAll({ topic:businessRoom.topic }) + if (roomList.length > 0) { + for (const i in roomList) { + const item = roomList[i] + const memberAlias = await item?.alias(bot.currentUser) + if (memberAlias === businessRoom.memberAlias) { + room = item + break + } + } + } else { + log.info('getContact:', '昵称没有匹配到好友') + } + } else if (businessRoom.luckyDog) { + roomList = await bot.Room.findAll({ topic:businessRoom.topic }) + if (roomList.length > 0) { + for (const i in roomList) { + const item = roomList[i] + const member = await item?.member(businessRoom.luckyDog) + if (member) { + room = item + break + } + } + } else { + log.info('getContact:', '昵称没有匹配到好友') + } + } else { + room = await bot.Room.find({ topic:businessRoom.topic }) + } + return room +} + +export const isThisRoom = async (bot:Wechaty, businessRoom:BusinessRoom, room:Room) => { + const topic = await room.topic() + if (businessRoom.id && room.id === businessRoom.id) { + return true + } else if (businessRoom.memberAlias) { + const memberAlias = await room.alias(bot.currentUser) + if (memberAlias === businessRoom.memberAlias) { + return true + } else { + return false + } + } else if (businessRoom.luckyDog) { + const member = await room.member(businessRoom.luckyDog) + if (member) { + return true + } else { + return false + } + } else { + if (topic === businessRoom.topic) { + return true + } else { + return false + } + } +} diff --git a/src/plugins/group-notice.ts b/src/plugins/group-notice.ts index 8ae1e3cb..0ac9ca9d 100644 --- a/src/plugins/group-notice.ts +++ b/src/plugins/group-notice.ts @@ -18,7 +18,7 @@ async function sendTextMessage (contact: Contact, text: string): Promise 1000 * 60 * 5) { - // msgList.push({ - // role: 'sys', - // contentType: 'text', - // content: '2022-5-30 20:00:00', - // }) - // configData.chatInfoEn.lastMsgShowTime = msg.createTime - // } - - // 2)插入消息 - // msgList.push(msg) - - // 3.设置chat对象相关属性 - // configData.chatInfoEn.msgList = msgList - - // 4.回调 - successCallback() -} - -/** - * 发送消息 - * @param {Object} rs 回调对象 - */ -function sendMsg (rs: any) { - const msg = rs.msg - msg.role = 'client' - msg.avatarUrl = configData.clientChatEn.avatarUrl - if (configData.chatInfoEn.chatState === 'robot') { - // 机器人发送接口 - } else if (configData.chatInfoEn.chatState === 'agent') { - // 客服接口 - configData.socket.emit('CLIENT_SEND_MSG', { - clientChatEn: configData.clientChatEn, - msg, - serverChatId: configData.serverChatEn.serverChatId, - }) - - // log.debug(configData.serverChatEn.serverChatId) - } - // 2.添加到消息集合 - // addChatMsg(msg) -} - -function imclient (bot:Wechaty, vika:any, configData:any) { - let socket: any = {} - try { - socket = io.connect('http://localhost:3001') - configData.socket = socket - socket.on('connect', () => { - // 客户端上线 - socket.emit('CLIENT_ON', { - clientChatEn: configData.clientChatEn, - serverChatId: configData.serverChatEn.serverChatId, - }) - - // 服务端链接 - socket.on('SERVER_CONNECTED', (data: { serverChatEn: { serverChatId: string; serverChatName: string; avatarUrl: string } }) => { - // 1)获取客服消息 - configData.serverChatEn = data.serverChatEn - - // 2)添加消息 - addChatMsg({ - content: '客服 ' + configData.serverChatEn.serverChatName + ' 为你服务', - contentType: 'text', - role: 'sys', - }, () => { }) - }) - - // 接受服务端信息 - socket.on('SERVER_SEND_MSG', async (data: any) => { - log.info(data) - // if (data.msg && data.msg.role === 'server') { - // data.msg.role = 'client' - // sendMsg(data) - // } - try { - const roomId = data.msg.clientChatId.split(' ')[1] - const contactId = data.msg.clientChatId.split(' ')[0] - const room:Room|undefined = await bot.Room.find({ id: roomId }) - const contact = await bot.Contact.find({ id: contactId }) - if (room) { - await room.say(data.msg.content, ...[ contact ]) - vika.addRecord(await formatSentMessage(bot.currentUser, data.msg.content, undefined, room)) - } - - // configData.msg.avatarUrl = data.serverChatEn.avatarUrl; - } catch (e) { - log.error('发送消息失败:', JSON.stringify(e)) - } - - }) - }) - } catch (err) { - log.error('连接失败:', err) - } - return socket -} - -export { configData, addChatMsg, sendMsg, imclient } diff --git a/src/plugins/index.js b/src/plugins/index.js deleted file mode 100644 index ce498424..00000000 --- a/src/plugins/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import onMessage from '../handlers/on-message.js' -import onScan from '../handlers/on-scan.js' - -import { VikaBot } from './vika.js' -import { - configData, - addChatMsg, - imclient, - sendMsg, -} from './im.js' -import { wxai } from './wxai.js' -import { sendNotice } from './group-notice.js' - -import { ChatDevice } from './chat-device.js' -import { propertyMessage, eventMessage } from './msg-format.js' -import { getFormattedRideInfo } from './riding.js' - -function WechatyVikaPlugin (vika) { - return function (bot) { - bot.on('onScan', async (qrcode, status) => { - await onScan(qrcode, status, vika) - }) - bot.on('login', async () => { - // await vika.checkInit('vika插件载入系统配置完成,系统启动成功~') - }) - bot.on('message', async (msg) => { - await onMessage(msg, vika) - }) - } -} - -export { - WechatyVikaPlugin, - VikaBot, - configData, - imclient, - addChatMsg, - getFormattedRideInfo, - sendMsg, - sendNotice, - wxai, - ChatDevice, - propertyMessage, - eventMessage, -} - -export default WechatyVikaPlugin diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 00000000..978dd359 --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,17 @@ +import { VikaBot } from './vika.js' +import { wxai } from './wxai.js' +import { sendNotice } from './group-notice.js' + +import { ChatDevice } from './chat-device.js' +import { propertyMessage, eventMessage } from './msg-format.js' +import { getFormattedRideInfo } from './riding.js' + +export { + VikaBot, + getFormattedRideInfo, + sendNotice, + wxai, + ChatDevice, + propertyMessage, + eventMessage, +} diff --git a/src/plugins/lib/dataModel.ts b/src/plugins/lib/dataModel.ts index e8160941..01cc4a8d 100644 --- a/src/plugins/lib/dataModel.ts +++ b/src/plugins/lib/dataModel.ts @@ -54,13 +54,13 @@ const commandSheet: Sheet = { name: '说明', type: FieldType.Text, }, - { - name: '管理员微信号', - type: FieldType.SingleText, - property: { + // { + // name: '管理员微信号', + // type: FieldType.SingleText, + // property: { - }, - }, + // }, + // }, { name: '类型', type: FieldType.SingleSelect, diff --git a/src/plugins/lib/vikaModel/CommandList.ts b/src/plugins/lib/vikaModel/CommandList.ts index 1e8d9b15..f44e14be 100644 --- a/src/plugins/lib/vikaModel/CommandList.ts +++ b/src/plugins/lib/vikaModel/CommandList.ts @@ -11,6 +11,26 @@ const recordRes = { data: { total: 7, records: [ + { + recordId: 'receuIOdNUz8T', + createdAt: 1671682513000, + updatedAt: 1671682529000, + fields: { + 指令名称: '帮助', + 类型: '系统指令', + 说明: '获得操作指令集', + }, + }, + { + recordId: 'receuIOdNUz8T', + createdAt: 1671682513000, + updatedAt: 1671682529000, + fields: { + 指令名称: '更新定时提醒', + 类型: '系统指令', + 说明: '更新定时提醒任务', + }, + }, { recordId: 'recUqpQPkMrQO', createdAt: 1670218148000, @@ -71,16 +91,6 @@ const recordRes = { 说明: 'TBD当前群关闭智能问答', }, }, - { - recordId: 'receuIOdNUz8T', - createdAt: 1671682513000, - updatedAt: 1671682529000, - fields: { - 指令名称: '更新提醒', - 类型: '系统指令', - 说明: '更新通知提醒任务', - }, - }, ], pageNum: 1, pageSize: 7, @@ -111,13 +121,6 @@ const vikaRes = { type: 'Text', editable: true, }, - { - id: 'fldE1R4eb6E8S', - name: '管理员微信号', - type: 'SingleText', - property: {}, - editable: true, - }, { id: 'fldopkUTne42Y', name: '类型', diff --git a/src/plugins/lib/vikaModel/Contact.ts b/src/plugins/lib/vikaModel/Contact.ts index cfefa2c6..9e9e8715 100644 --- a/src/plugins/lib/vikaModel/Contact.ts +++ b/src/plugins/lib/vikaModel/Contact.ts @@ -45,6 +45,15 @@ const vikaRes = { }, editable: true, }, + { + id: 'fldCbZKiBXklM0', + name: 'updated', + type: 'SingleText', + property: { + defaultValue: '', + }, + editable: true, + }, { id: 'fldK2fgxvTDcs', name: 'friend', diff --git a/src/plugins/lib/vikaModel/EnvConfig.ts b/src/plugins/lib/vikaModel/EnvConfig.ts index 2a281aeb..7e79120d 100644 --- a/src/plugins/lib/vikaModel/EnvConfig.ts +++ b/src/plugins/lib/vikaModel/EnvConfig.ts @@ -9,172 +9,204 @@ const recordRes = { code: 200, success: true, data: { - total: 13, + total: 19, records: [ { - recordId: 'recm3YUoiY3lX', + recordId: 'recPBl9ASIJR7', + createdAt: 1683302503000, + updatedAt: 1683362189000, fields: { - 配置组: '基本配置', - 配置组标识: 'base', - 配置项: '管理群', - 标识: 'adminRoomTopic', - '值(只修改此列)': '大师是群主', - 说明: '管理群名称,需尽量保持名称复杂,避免重名群干扰', + 配置项: '管理群-管理群ID', + 标识: 'ADMINROOM_ADMINROOMID', + 说明: '管理群的ID,只有在此群内发布管理指令才会生效', + }, + }, + { + recordId: 'rec5A51Aa2xhX', + createdAt: 1683302744000, + updatedAt: 1683362199000, + fields: { + 配置项: '管理群-管理群名称', + 标识: 'ADMINROOM_ADMINROOMTOPIC', + 说明: '管理群名称,只有在此群内发布管理指令才会生效', }, }, { recordId: 'recrEIHXFV14w', createdAt: 1671304478000, - updatedAt: 1671308763000, + updatedAt: 1683354854000, fields: { - 配置项: 'WechatyPuppet', - 标识: 'puppetName', - 配置组: 'Wechaty', - 配置组标识: 'wechaty', - 说明: '可选值:\nwechaty-puppet-wechat4u\nwechaty-puppet-wechat\nwechaty-puppet-xp\nwechaty-puppet-padlocal\nwechaty-puppet-service', - '值(只修改此列)': 'wechaty-puppet-wechat4u', + 配置项: 'Wechaty-WechatyPuppet', + 标识: 'WECHATY_PUPPET', + 说明: '可选值:\nwechaty-puppet-wechat4u\nwechaty-puppet-wechat\nwechaty-puppet-xp\nwechaty-puppet-engine\u0000\nwechaty-puppet-padlocal\nwechaty-puppet-service', + 值: 'wechaty-puppet-wechat4u', }, }, { recordId: 'rec99fo7LJIXP', createdAt: 1671304478000, - updatedAt: 1671308940000, + updatedAt: 1683354845000, fields: { - 配置项: 'WechatyToken', - 标识: 'puppetToken', - 配置组: 'Wechaty', - 配置组标识: 'wechaty', + 配置项: 'Wechaty-WechatyToken', + 标识: 'WECHATY_TOKEN', 说明: '使用wechaty-puppet-padlocal、wechaty-puppet-service时需配置此token', }, }, { recordId: 'recinVcKkDT4g', createdAt: 1671304478000, - updatedAt: 1671306617000, + updatedAt: 1683362233000, fields: { - 配置项: 'AI对话平台Type', - 标识: 'aiType', - 配置组: '自动问答', - 配置组标识: 'auto-qa', - 说明: 'TODO-可选值:\nWxOpenai\nChatGPT', - '值(只修改此列)': 'WxOpenai', + 配置项: '智能问答类型-平台类型', + 标识: 'AUTOQA_TYPE', + 说明: '可选值:\nwxOpenAi\nchatGpt', + 值: 'wxOpenAi', }, }, { recordId: 'reca02j4zeJJO', createdAt: 1671304478000, - updatedAt: 1671304478000, + updatedAt: 1683302805000, fields: { - 配置项: '微信对话开放平台Token', - 标识: 'WX_TOKEN', - 配置组: '微信开放对话平台', - 配置组标识: 'wx-open-ai', + 配置项: '微信对话开放平台-Token', + 标识: 'WXOPENAI_TOKEN', 说明: '微信对话开放平台中获取', }, }, { recordId: 'recDs5CswG6Y2', createdAt: 1671304478000, - updatedAt: 1671304478000, + updatedAt: 1683302809000, fields: { - 配置项: '微信对话开放平台EncodingAESKey', - 标识: 'EncodingAESKey', - 配置组: '智能问答', + 配置项: '微信对话开放平台-EncodingAESKey', + 标识: 'WXOPENAI_ENCODINGAESKEY', 说明: '微信对话开放平台中获取', }, }, { - recordId: 'rec5Mjc4E6GjK', - createdAt: 1671304478000, - updatedAt: 1671304478000, + recordId: 'recC1fHb229aw', + createdAt: 1683351452000, + updatedAt: 1683362266000, fields: { - 配置项: 'ChatGPTAEmail', - 标识: 'ChatGPTAEmail', - 配置组: '智能问答', - 说明: 'TODO', + 配置项: 'ChatGPT-Key', + 标识: 'CHATGPT_KEY', + 说明: 'openai平台获取或使用api2d', }, }, { - recordId: 'recN4gbSUoWIa', - createdAt: 1671304478000, - updatedAt: 1671304478000, - fields: { - 配置项: 'ChatGPTAPassword', - 标识: 'ChatGPTAPassword', - 配置组: '智能问答', - 说明: 'TODO', - }, - }, - { - recordId: 'rechhkGPqXzo6', - createdAt: 1671304478000, - updatedAt: 1671304478000, + recordId: 'recInnEvRdBtB', + createdAt: 1683351450000, + updatedAt: 1683362267000, fields: { - 配置项: 'ChatGPTASessionToken', - 标识: 'ChatGPTASessionToken', - 配置组: '智能问答', - 说明: 'TODO', + 配置项: 'ChatGPT-Endpoint', + 标识: 'CHATGPT_ENDPOINT', + 说明: 'openai平台获取或使用api2d', }, }, { recordId: 'recos1u8VvHuQ', createdAt: 1671304478000, - updatedAt: 1671304478000, + updatedAt: 1683302909000, fields: { - 配置项: 'MQTT用户名', - 标识: 'mqttUsername', - 配置组: 'MQTT连接', + 配置项: 'MQTT连接-用户名', + 标识: 'MQTT_USERNAME', 说明: 'MQTT连接配置信息,推荐使用百度云的物联网核心套件', }, }, { recordId: 'rechxZI6WS5Uq', createdAt: 1671304478000, - updatedAt: 1671304478000, + updatedAt: 1683302912000, fields: { - 配置项: 'MQTT密码', - 标识: 'mqttPassword', - 配置组: 'MQTT连接', + 配置项: 'MQTT连接-密码', + 标识: 'MQTT_PASSWORD', 说明: 'MQTT连接配置信息,推荐使用百度云的物联网核心套件', }, }, { recordId: 'recB2MNTLz9zM', createdAt: 1671304480000, - updatedAt: 1671304480000, + updatedAt: 1683302918000, fields: { - 配置项: 'MQTT接入地址', - 标识: 'mqttEndpoint', - 配置组: 'MQTT连接', + 配置项: 'MQTT连接-接入地址', + 标识: 'MQTT_ENDPOINT', 说明: 'MQTT连接配置信息,推荐使用百度云的物联网核心套件', }, }, { recordId: 'recqXfHERfj3b', createdAt: 1671304480000, - updatedAt: 1671304480000, + updatedAt: 1683362365000, fields: { - 配置项: 'MQTT端口号', - 标识: 'mqttPort', - 配置组: 'MQTT连接', + 配置项: 'MQTT连接-端口号', + 标识: 'MQTT_PORT', 说明: 'MQTT连接配置信息,推荐使用百度云的物联网核心套件', - '值(只修改此列)': '1883', + 值: '1883', }, }, { recordId: 'rec8prGUMpMiw', createdAt: 1671304480000, - updatedAt: 1671304480000, + updatedAt: 1683362373000, + fields: { + 配置项: 'HTTP消息推送-地址', + 标识: 'WEBHOOK_URL', + 说明: '格式 http://baidu.com/abc,多个地址使用英文逗号隔开,使用post请求推送', + }, + }, + { + recordId: 'rec5qrdq7KQDD', + createdAt: 1683362377000, + updatedAt: 1683362490000, + fields: { + 配置项: 'HTTP消息推送-Token', + 标识: 'WEBHOOK_TOKEN', + 说明: '当填写token时优先使用token,其次用户名+密码,再次无鉴权请求', + }, + }, + { + recordId: 'recQrTGCt5HoY', + createdAt: 1683362393000, + updatedAt: 1683362494000, + fields: { + 配置项: 'HTTP消息推送-用户名', + 标识: 'WEBHOOK_USERNAME', + 说明: '当填写token时优先使用token,其次用户名+密码,再次无鉴权请求', + }, + }, + { + recordId: 'recJZm5Ip0ycw', + createdAt: 1683362395000, + updatedAt: 1683362496000, + fields: { + 配置项: 'HTTP消息推送-密码', + 标识: 'WEBHOOK_PASSWORD', + 说明: '当填写token时优先使用token,其次用户名+密码,再次无鉴权请求', + }, + }, + { + recordId: 'recXerzWBcbcX', + createdAt: 1683302493000, + updatedAt: 1683362296000, fields: { - 配置项: 'WebHook地址', - 标识: 'WEB_HOOK', - 配置组: '消息推送', - 说明: 'TODO-格式 http://baidu.com/abc,多个地址使用英文逗号隔开,使用post请求推送', + 配置项: '语雀-token', + 标识: 'YUQUE_TOKEN', + 说明: '语雀知识库token', + }, + }, + { + recordId: 'recqyaulVdpTr', + createdAt: 1683302493000, + updatedAt: 1683362308000, + fields: { + 配置项: '语雀-空间名称', + 标识: 'YUQUE_NAMESPACE', + 说明: '语雀知识库空间名称', }, }, ], pageNum: 1, - pageSize: 13, + pageSize: 19, }, message: 'SUCCESS', } @@ -187,43 +219,28 @@ const vikaRes = { data: { fields: [ { - id: 'fldswxMTbHJwr', - name: '配置组', - type: 'SingleText', - property: {}, - editable: true, - isPrimary: true, - }, - { - id: 'fldswxMTbHJwr', - name: '配置组标识', - type: 'SingleText', - property: {}, - editable: true, - isPrimary: true, - }, - { - id: 'fldlCUQ2Aju1Y', + id: 'fldHweXlPkEre', name: '配置项', type: 'SingleText', property: {}, editable: true, + isPrimary: true, }, { - id: 'fldDrMTuWCuCM', + id: 'fldRvvud37nrh', name: '标识', type: 'SingleText', property: {}, editable: true, }, { - id: 'fld6GYkhQCQ7m', - name: '值(只修改此列)', + id: 'fldzgaXqumNkW', + name: '值', type: 'Text', editable: true, }, { - id: 'fldpD6BA5xeZf', + id: 'fldx2StDC2LGw', name: '说明', type: 'Text', editable: true, diff --git a/src/plugins/lib/vikaModel/Message.ts b/src/plugins/lib/vikaModel/Message.ts index 3d89c81f..3e86ce4f 100644 --- a/src/plugins/lib/vikaModel/Message.ts +++ b/src/plugins/lib/vikaModel/Message.ts @@ -29,6 +29,15 @@ const vikaRes = { }, editable: true, }, + { + id: 'fldIDa0zPtgYo0', + name: 'alias', + type: 'SingleText', + property: { + defaultValue: '', + }, + editable: true, + }, { id: 'fldCbOzc2qfVn', name: 'topic', @@ -44,6 +53,21 @@ const vikaRes = { type: 'Text', editable: true, }, + { + id: 'fldh1g0q0rx9M', + name: 'file', + type: 'Attachment', + editable: true, + }, + { + id: 'fldiRwFyYEIYX', + name: 'messageType', + type: 'SingleText', + property: { + defaultValue: '', + }, + editable: true, + }, { id: 'fldJ9S09Ib9ZT', name: 'wxid', @@ -64,19 +88,13 @@ const vikaRes = { }, { id: 'fldiRwFyYEIYX', - name: 'messageType', + name: 'messageId', type: 'SingleText', property: { defaultValue: '', }, editable: true, }, - { - id: 'fldh1g0q0rx9M', - name: 'file', - type: 'Attachment', - editable: true, - }, ], }, message: 'SUCCESS', diff --git a/src/plugins/lib/vikaModel/Notice.ts b/src/plugins/lib/vikaModel/Notice.ts index 009672ed..1bc97eea 100644 --- a/src/plugins/lib/vikaModel/Notice.ts +++ b/src/plugins/lib/vikaModel/Notice.ts @@ -18,37 +18,51 @@ const vikaRes = { isPrimary: true, }, { - id: 'fldfqyrfkWBBy', - name: '接收好友', - type: 'MagicLink', + id: 'fld3qG3X0ZlFs', + name: '通知目标类型', + type: 'SingleSelect', + property: { + options: [ + { + id: 'optWMNg5xJJZS', + name: '好友', + color: { + name: 'deepPurple_0', + value: '#E5E1FC', + }, + }, + { + id: 'optNtIR7ecTja', + name: '群', + color: { + name: 'indigo_0', + value: '#DDE7FF', + }, + }, + ], + defaultValue: '好友', + }, + editable: true, + }, + { + id: 'fldiRwFyYEIYX', + name: '好友备注/昵称或群名称', + type: 'SingleText', + desc: '类型为好友时优先匹配好友ID、备注、昵称,类型为群时优先匹配群ID、群名称', property: { - foreignDatasheetId: 'dstbutP3T8WorWLlbq', - brotherFieldId: 'fld0reGOMyTGV', + defaultValue: '', }, editable: true, - desc: '好友列表', }, - // { - // "id": "fldXq00SDGvS9", - // "name": "接收分组", - // "type": "MagicLink", - // "property": { - // "foreignDatasheetId": "dsttzJxMEqxZ0m5UHZ", - // "brotherFieldId": "fldNqvOAzEkxC" - // }, - // "editable": true, - // "desc": "好友分组" - // }, { - id: 'fldJ1TvTV1T8c', - name: '接收群', - type: 'MagicLink', + id: 'fldiRwFyYEIYX0', + name: '好友ID/群ID', + type: 'SingleText', + desc:'选填,使用wehcaty-puppet-wechat时因id会变化,不要填写', property: { - foreignDatasheetId: 'dstRVUymHGd1e4mrWU', - brotherFieldId: 'flduYxMKg3ERW', + defaultValue: '', }, editable: true, - desc: '群列表', }, { id: 'fldoCm0thVXmq', @@ -194,7 +208,7 @@ const fields: Field[] = vikaRes.data.fields const noticeSheet: Sheet = { fields, - name: '通知提醒', + name: '定时提醒', defaultRecords, } diff --git a/src/plugins/lib/vikaModel/Room.ts b/src/plugins/lib/vikaModel/Room.ts index dc35ee87..8d84856e 100644 --- a/src/plugins/lib/vikaModel/Room.ts +++ b/src/plugins/lib/vikaModel/Room.ts @@ -36,6 +36,15 @@ const vikaRes = { }, editable: true, }, + { + id: 'fldCbZKiBXklM0', + name: 'updated', + type: 'SingleText', + property: { + defaultValue: '', + }, + editable: true, + }, { id: 'fld96Wo2Jn0tW', name: 'avatar', diff --git a/src/plugins/lib/vikaModel/Switch.ts b/src/plugins/lib/vikaModel/Switch.ts index 20afcdcf..42492b16 100644 --- a/src/plugins/lib/vikaModel/Switch.ts +++ b/src/plugins/lib/vikaModel/Switch.ts @@ -9,131 +9,141 @@ const recordRes = { code: 200, success: true, data: { - total: 10, + total: 12, records: [ { - recordId: 'recf6KOk48YKn', - createdAt: 1671302940000, - updatedAt: 1671303805000, + recordId: 'recdZM91Tu8Vq', + createdAt: 1671304486000, + updatedAt: 1683341245000, fields: { + 启用状态: '关闭', + 功能项: '智能问答-启用自动问答', + 标识: 'AUTOQA_AUTOREPLY', 说明: '开启后可以使用微信对话平台只能问答', - 功能项: '智能问答', - 标识: 'WX_OPENAI_ONOFF', - '启用状态(只修改此列)': '关闭', - 配置组: '智能问答', }, }, { - recordId: 'recYniqLy8b8D', - createdAt: 1671302940000, - updatedAt: 1671303814000, + recordId: 'recILZpkkckpi', + createdAt: 1671304486000, + updatedAt: 1683303494000, fields: { + 启用状态: '开启', + 功能项: '智能问答-@回复', + 标识: 'AUTOQA_ATREPLY', 说明: '开启后只有@机器人时才会回复问答', - 功能项: 'AT回复', - 标识: 'AT_AHEAD', - '启用状态(只修改此列)': '开启', - 配置组: '智能问答', }, }, { - recordId: 'recn53pOPa3Fu', - createdAt: 1671302940000, - updatedAt: 1671303822000, + recordId: 'recKRx4x1ajE8', + createdAt: 1671304486000, + updatedAt: 1683303494000, fields: { + 启用状态: '关闭', + 功能项: '智能问答-不同群个性回复', + 标识: 'AUTOQA_CUSTOMREPLY', 说明: '开启后不同群相同问题可以设置不同的回答', - 功能项: '不同群个性回复', - 标识: 'DIFF_REPLY_ONOFF', - '启用状态(只修改此列)': '开启', - 配置组: '智能问答', }, }, { - recordId: 'recPdt5BOLiXq', - createdAt: 1671302940000, - updatedAt: 1671303837000, + recordId: 'recClukJEugD8', + createdAt: 1671304486000, + updatedAt: 1683303494000, fields: { + 启用状态: '开启', + 功能项: '群智能问答-群白名单白名单', + 标识: 'AUTOQA_ROOMWHITELIST', 说明: '开启后只对白名单内的群消息进行自动问答', - 功能项: '群白名单', - 标识: 'roomWhiteListOpen', - '启用状态(只修改此列)': '开启', - 配置组: '智能问答', }, }, { - recordId: 'recHv8B2IaofP', - createdAt: 1671302940000, - updatedAt: 1671303853000, + recordId: 'recFni0OMgcXR', + createdAt: 1671304486000, + updatedAt: 1683303494000, fields: { + 启用状态: '开启', + 功能项: '智能问答-好友白名单', + 标识: 'AUTOQA_CONTACTWHITELIST', 说明: '开启后只对白名单内的好友消息进行自动问答', - 功能项: '好友白名单', - 标识: 'contactWhiteListOpen', - '启用状态(只修改此列)': '开启', - 配置组: '智能问答', }, }, { - recordId: 'recXEwHZSAATR', - createdAt: 1671302940000, - updatedAt: 1671303860000, + recordId: 'reclZvMhMbrLA', + createdAt: 1683341186000, + updatedAt: 1683341252000, fields: { + 启用状态: '开启', + 功能项: '维格表-启用维格表', + 标识: 'VIKA_USEVIKA', + }, + }, + { + recordId: 'reccta6czN536', + createdAt: 1671304486000, + updatedAt: 1683303809000, + fields: { + 启用状态: '开启', + 功能项: '维格表-消息上传到维格表', + 标识: 'VIKA_UPLOADMESSAGETOVIKA', 说明: '开启后消息记录会自动上传到维格表的【消息记录】表', - 功能项: '消息上传到维格表', - 标识: 'VIKA_ONOFF', - '启用状态(只修改此列)': '开启', - 配置组: '消息推送', }, }, { - recordId: 'rec1WdnWXsyPo', - createdAt: 1671302940000, - updatedAt: 1671303946000, + recordId: 'rec7QjKnBKpIY', + createdAt: 1683303823000, + updatedAt: 1683303850000, + fields: { + 启用状态: '关闭', + 功能项: '维格表-配置云同步', + 标识: 'VIKA_AUTOMATICCLOUD', + }, + }, + { + recordId: 'rec0hgqiA6Cc7', + createdAt: 1671304486000, + updatedAt: 1683303514000, fields: { + 启用状态: '关闭', + 功能项: 'HTTP消息推送-WebHook推送', + 标识: 'WEBHOOK_WEBHOOKMESSAGEPUSH', 说明: 'TODO-开启后系统将机器人事件消息推送到指定的地址', - 功能项: 'WebHook推送', - 标识: 'WEB_HOOK_ONOFF', - '启用状态(只修改此列)': '关闭', - 配置组: '消息推送', }, }, { - recordId: 'recjMAPK1OZbT', - createdAt: 1671302940000, - updatedAt: 1671303880000, + recordId: 'recchnZdbQK2S', + createdAt: 1671304486000, + updatedAt: 1683303518000, fields: { + 启用状态: '关闭', + 功能项: 'MQTT连接-MQTT推送', + 标识: 'MQTT_MQTTMESSAGEPUSH', 说明: '开启后消息会发送到MQTT队列,需要先配置MQTT配置项', - 功能项: 'MQTT推送', - 标识: 'mqtt_PUB_ONOFF', - '启用状态(只修改此列)': '关闭', - 配置组: '消息推送', }, }, { - recordId: 'recESlHvyEPcj', - createdAt: 1671302940000, - updatedAt: 1671303873000, + recordId: 'reciwvE8H9CPf', + createdAt: 1671304486000, + updatedAt: 1683303522000, fields: { + 启用状态: '关闭', + 功能项: 'MQTT连接-MQTT控制', + 标识: 'MQTT_MQTTCONTROL', 说明: '开启可以通过MQTT控制微信,需要先配置MQTT配置项', - 功能项: 'MQTT控制', - 标识: 'mqtt_SUB_ONOFF', - '启用状态(只修改此列)': '关闭', - 配置组: '远程控制', }, }, { - recordId: 'recumi1YTrUAq', - createdAt: 1671302940000, - updatedAt: 1671303906000, + recordId: 'recaYaXkyTVJA', + createdAt: 1671304486000, + updatedAt: 1683303530000, fields: { + 启用状态: '关闭', + 功能项: '客服系统-IM对话', + 标识: 'IM_IMCHAT', 说明: '开启后可以使用客服对话系统,需先手动启用IM服务', - 功能项: 'IM对话', - 标识: 'imOpen', - '启用状态(只修改此列)': '关闭', - 配置组: '客服系统', }, }, ], pageNum: 1, - pageSize: 10, + pageSize: 12, }, message: 'SUCCESS', } @@ -146,37 +156,28 @@ const vikaRes = { data: { fields: [ { - id: 'fldq84eKS9Cyq', - name: '配置组', - type: 'SingleText', - property: { - defaultValue: '', - }, - editable: true, - isPrimary: true, - }, - { - id: 'fldPp0bwSk84x', + id: 'fldC7hfa7e1Xw', name: '功能项', type: 'SingleText', property: {}, editable: true, + isPrimary: true, }, { - id: 'fldZ0kmh0WQTh', + id: 'fldSosuw4xlXB', name: '标识', type: 'SingleText', property: {}, editable: true, }, { - id: 'fldmndbxeLd37', - name: '启用状态(只修改此列)', + id: 'fld3qG3X0ZlFs', + name: '启用状态', type: 'SingleSelect', property: { options: [ { - id: 'opt4DXQURFJQf', + id: 'optWMNg5xJJZS', name: '开启', color: { name: 'deepPurple_0', @@ -184,7 +185,7 @@ const vikaRes = { }, }, { - id: 'optvpgML9rza6', + id: 'optNtIR7ecTja', name: '关闭', color: { name: 'indigo_0', @@ -196,7 +197,7 @@ const vikaRes = { editable: true, }, { - id: 'fldAKw21lBTlb', + id: 'fldogMQE99uDi', name: '说明', type: 'Text', editable: true, diff --git a/src/plugins/mappers/message-mapper.ts b/src/plugins/mappers/message-mapper.ts new file mode 100644 index 00000000..4e066e35 --- /dev/null +++ b/src/plugins/mappers/message-mapper.ts @@ -0,0 +1,72 @@ +import { + Message, + log, +} from 'wechaty' + +import type { + TalkerMessage, +} from '../types/mod.js' + +type MessageMapperFunction = (message: Message) => TalkerMessage + | TalkerMessage[] + | Promise< + never + | TalkerMessage + | TalkerMessage[] + > +type MessageMapperOption = TalkerMessage | MessageMapperFunction +export type MessageMapperOptions = MessageMapperOption | MessageMapperOption[] + +function messageMapper ( + mapperOptions: MessageMapperOptions, +) { + log.verbose('WechatyPluginContrib', 'messageMapper(%s)', + typeof mapperOptions === 'function' + ? 'function' + : JSON.stringify(mapperOptions), + ) + + return async function mapMessage (message: Message): Promise { + log.verbose('WechatyPluginContrib', 'mapMessage(%s)', message) + + return normalizeMappedMessageList(mapperOptions, message) + } +} + +async function normalizeMappedMessageList ( + options: MessageMapperOptions, + message: Message, +): Promise { + log.verbose('WechatyPluginContrib', 'normalizeMappedMessageList(%s, %s)', + JSON.stringify(options), + message, + ) + + const msgList = [] as TalkerMessage[] + + let optionList + if (Array.isArray(options)) { + optionList = options + } else { + optionList = [ options ] + } + + for (const option of optionList) { + if (!option) { continue } + + if (typeof option === 'function') { + const ret = await option(message) + if (ret) { + msgList.push(...await normalizeMappedMessageList(ret, message)) + } + } else { + msgList.push(option) + } + } + + return msgList +} + +export { + messageMapper, +} diff --git a/src/plugins/mappers/mod.ts b/src/plugins/mappers/mod.ts new file mode 100644 index 00000000..a8934751 --- /dev/null +++ b/src/plugins/mappers/mod.ts @@ -0,0 +1,11 @@ +import { + MessageMapperOptions, + messageMapper, +} from './message-mapper.js' + +export type { + MessageMapperOptions, +} +export { + messageMapper, +} diff --git a/src/plugins/matchers/contact-matcher.spec.ts b/src/plugins/matchers/contact-matcher.spec.ts new file mode 100755 index 00000000..235d8017 --- /dev/null +++ b/src/plugins/matchers/contact-matcher.spec.ts @@ -0,0 +1,85 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' +import type { Contact } from 'wechaty' + +import { contactMatcher } from './contact-matcher.js' + +test('contactMatcher() smoke testing', async t => { + const matcher = contactMatcher(/test/i) + t.equal(typeof matcher, 'function', 'should return a match function') +}) + +test('contactMatcher() with string option', async t => { + const TEXT_OK = 'hello' + const TEXT_NOT_OK = 'world' + + const nameOk = () => TEXT_OK + const nameNotOk = () => TEXT_NOT_OK + + const contactIdOk = { + id: TEXT_OK, + name: nameNotOk, + } as any as Contact + + const contactNameOk = { + id: TEXT_NOT_OK, + name: nameOk, + } as any as Contact + + const contactNotOk = { + id: TEXT_NOT_OK, + name: nameNotOk, + } as any as Contact + + const falseMatcher = contactMatcher() + t.notOk(await falseMatcher(contactIdOk), 'should not match any contact without options') + t.notOk(await falseMatcher(contactNameOk), 'should not match any contact without options') + + const idMatcher = contactMatcher(TEXT_OK) + + t.notOk(await idMatcher(contactNotOk), 'should not match unexpected contact by id') + + t.ok(await idMatcher(contactIdOk), 'should match expected contact by id') + t.notOk(await idMatcher(contactNameOk), 'should not match contact by name') + + const idListMatcher = contactMatcher([ TEXT_OK ]) + + t.notOk(await idListMatcher(contactNotOk), 'should not match unexpected contact by id list') + + t.ok(await idListMatcher(contactIdOk), 'should match expected contact by id list') + t.notOk(await idListMatcher(contactNameOk), 'should not match contact by name list') + + const regexpMatcher = contactMatcher(new RegExp(TEXT_OK)) + + t.notOk(await regexpMatcher(contactNotOk), 'should not match unexpected contact by regexp') + + t.notOk(await regexpMatcher(contactIdOk), 'should match contact id by regexp') + t.ok(await regexpMatcher(contactNameOk), 'should match expected contact name by regexp') + + const regexpListMatcher = contactMatcher([ new RegExp(TEXT_OK) ]) + + t.notOk(await regexpListMatcher(contactNotOk), 'should not match unexpected contact by regexp list') + + t.notOk(await regexpListMatcher(contactIdOk), 'should not match contact id by regexp list') + t.ok(await regexpListMatcher(contactNameOk), 'should match expected contact name by regexp list') + + const roomFilter = (room: Contact) => [ + room.id, + room.name(), + ].includes(TEXT_OK) + + const functionMatcher = contactMatcher(roomFilter) + + t.notOk(await functionMatcher(contactNotOk), 'should not match unexpected contact by function') + + t.ok(await functionMatcher(contactNameOk), 'should match expected name by function') + t.ok(await functionMatcher(contactIdOk), 'should match expected id by function') + + const functionListMatcher = contactMatcher([ roomFilter ]) + + t.notOk(await functionListMatcher(contactNotOk), 'should not match unexpected contact by function list') + + t.ok(await functionListMatcher(contactNameOk), 'should match expected name by function list') + t.ok(await functionListMatcher(contactIdOk), 'should match expected text by function list') +}) diff --git a/src/plugins/matchers/contact-matcher.ts b/src/plugins/matchers/contact-matcher.ts new file mode 100644 index 00000000..784bf830 --- /dev/null +++ b/src/plugins/matchers/contact-matcher.ts @@ -0,0 +1,51 @@ +import { + Contact, + log, +} from 'wechaty' + +type ContactMatcherFunction = (contact: Contact) => boolean | Promise +type ContactMatcherOption = boolean | string | RegExp | ContactMatcherFunction +export type ContactMatcherOptions = ContactMatcherOption | ContactMatcherOption[] + +type MatchContactFunction = (contact: Contact) => Promise + +export function contactMatcher ( + matcherOptions?: ContactMatcherOptions, +): MatchContactFunction { + log.verbose('WechatyPluginContrib', 'contactMatcher(%s)', JSON.stringify(matcherOptions)) + + if (!matcherOptions) { + return () => Promise.resolve(false) + } + + if (!Array.isArray(matcherOptions)) { + matcherOptions = [ matcherOptions ] + } + + const matcherOptionList = matcherOptions + + return async function matchContact (contact: Contact): Promise { + log.silly('WechatyPluginContrib', 'contactMatcher() matchContact(%s)', contact) + + let isMatch = false + for (const option of matcherOptionList) { + if (typeof option === 'boolean') { + isMatch = option + } else if (typeof option === 'string') { + isMatch = option === contact.id + } else if (option instanceof Function) { + isMatch = await option(contact) + } else if (option instanceof RegExp) { + isMatch = option.test(contact.name()) + } else { + throw new Error('unknown option: ' + option) + } + + if (isMatch) { + return true + } + } + // no match + return false + } +} diff --git a/src/plugins/matchers/language-matcher.spec.ts b/src/plugins/matchers/language-matcher.spec.ts new file mode 100755 index 00000000..c3186579 --- /dev/null +++ b/src/plugins/matchers/language-matcher.spec.ts @@ -0,0 +1,56 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' + +import { + detectLanguage, + includeLanguage, + languageMatcher, +} from './language-matcher.js' + +test('detectLanguage()', async t => { + const ENGLISH_TEXT = 'hello' + + const resultList = detectLanguage(ENGLISH_TEXT) + t.ok(Array.isArray(resultList), 'should return a array') + t.ok(resultList.length > 0, 'should get a non-empty array') +}) + +test('includeLanguage()', async t => { + const CHINESE_TEXT = '你好' + const ENGLISH_TEXT = 'hello' + + let resultList = detectLanguage(CHINESE_TEXT) + // console.info(resultList) + t.ok(includeLanguage(resultList, 'chinese'), 'should detect Chinese language') + + resultList = detectLanguage(ENGLISH_TEXT) + // console.info(resultList) + t.ok(includeLanguage(resultList, 'english'), 'should detect English language') +}) + +test('languageMatcher()', async t => { + const CHINESE_TEXT = '你好' + const ENGLISH_TEXT = 'hello' + + const matchLanguage = languageMatcher('chinese') + + let result = matchLanguage(CHINESE_TEXT) + t.ok(result, 'should match Chinese language') + + result = matchLanguage(ENGLISH_TEXT) + t.notOk(result, 'should not match English language') +}) + +test('languageMatcher() with array options', async t => { + const CHINESE_TEXT = '你好' + const ENGLISH_TEXT = 'hello' + + const matchLanguage = languageMatcher([ 'chinese', 'english' ]) + + let result = matchLanguage(CHINESE_TEXT) + t.ok(result, 'should match Chinese language') + + result = matchLanguage(ENGLISH_TEXT) + t.ok(result, 'should match English language') +}) diff --git a/src/plugins/matchers/language-matcher.ts b/src/plugins/matchers/language-matcher.ts new file mode 100644 index 00000000..a7a7c03e --- /dev/null +++ b/src/plugins/matchers/language-matcher.ts @@ -0,0 +1,85 @@ +import { + log, +} from 'wechaty' + +import languageMonitor from 'language-monitor' + +type LanguageCode = 'chinese' + | 'danish' + | 'norwegian' + | 'japanese' + | 'dutch' + | 'swedish' + | 'cebuano' + | 'tagalog' + | 'german' + | 'indonesian' + | 'turkish' + | 'korean' + | 'portuguese' + | 'azeri' + | 'english' + | 'estonian' + | 'latin' + | 'slovene' + | 'swahili' + | 'hungarian' + | 'spanish' + | 'lithuanian' + | 'finnish' + | 'hawaiian' + | 'icelandic' + | 'italian' + +interface GuessedLanguage { + code: LanguageCode, + rate: number, +} + +function detectLanguage (text: string, limit?: number): GuessedLanguage[] { + return languageMonitor(text, limit) +} + +function includeLanguage ( + resultList: GuessedLanguage[], + language: LanguageCode, + threshold = 0.1, +) { + const minThreshold = (min: number) => (result: GuessedLanguage) => result.rate > min + const toCode = (result: GuessedLanguage) => result.code + + return resultList + .filter(minThreshold(threshold)) + .map(toCode) + .includes(language) +} + +export type LanguageMatcherOptions = LanguageCode | LanguageCode[] + +type MatchLanguageFunction = (text: string) => boolean + +function languageMatcher ( + options: LanguageMatcherOptions, +): MatchLanguageFunction { + log.verbose('WechatyQnAMaker', 'languageMatcher(%s)', JSON.stringify(options)) + + let codeList: LanguageCode[] + + if (Array.isArray(options)) { + codeList = options + } else { + codeList = [ options ] + } + + return function matchLanguage (text: string) { + const resultList = detectLanguage(text) + return codeList.some(code => includeLanguage(resultList, code)) + } + +} + +export { + detectLanguage, + languageMatcher, + includeLanguage, +} diff --git a/src/plugins/matchers/language-monitor.d.ts b/src/plugins/matchers/language-monitor.d.ts new file mode 100644 index 00000000..89885ea2 --- /dev/null +++ b/src/plugins/matchers/language-monitor.d.ts @@ -0,0 +1 @@ +declare module 'language-monitor' diff --git a/src/plugins/matchers/message-matcher.spec.ts b/src/plugins/matchers/message-matcher.spec.ts new file mode 100755 index 00000000..9481e7f5 --- /dev/null +++ b/src/plugins/matchers/message-matcher.spec.ts @@ -0,0 +1,123 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' + +import type { Message } from 'wechaty' +import { messageMatcher } from './message-matcher.js' + +test('messageMatcher() smoke testing', async t => { + const matcher = messageMatcher(/test/i) + t.equal(typeof matcher, 'function', 'should return a match function') +}) + +test('messageMatcher() with string option', async t => { + const TEXT_OK = 'hello' + const TEXT_NOT_OK = 'world' + + const textOk = () => TEXT_OK + const roomOk = () => ({ id: TEXT_OK, topic: () => TEXT_OK }) + const talkerOk = () => ({ id: TEXT_OK, name: () => TEXT_OK }) + + const textNotOk = () => TEXT_NOT_OK + const roomNotOk = () => ({ id: TEXT_NOT_OK, topic: () => TEXT_NOT_OK }) + const talkerNotOk = () => ({ id: TEXT_NOT_OK, name: () => TEXT_NOT_OK }) + + const messageFromOk = { + id: TEXT_NOT_OK, + mentionText: textNotOk, + room: roomNotOk, + talker: talkerOk, + text: textNotOk, + } as any as Message + + const messageTextOk = { + id: TEXT_NOT_OK, + mentionText: textOk, + room: roomNotOk, + talker: talkerNotOk, + text: textOk, + } as any as Message + + const messageTopicOk = { + id: TEXT_NOT_OK, + mentionText: textNotOk, + room: roomOk, + talker: talkerNotOk, + text: textNotOk, + } as any as Message + + const messageIdOk = { + id: TEXT_OK, + room: roomOk, + talker: talkerNotOk, + text: textNotOk, + } as any as Message + + const messageNotOk = { + id: TEXT_NOT_OK, + mentionText: textNotOk, + room: roomNotOk, + talker: talkerNotOk, + text: textNotOk, + } as any as Message + + const falseMatcher = messageMatcher() + t.notOk(await falseMatcher(messageFromOk), 'should not match any message: from') + t.notOk(await falseMatcher(messageTopicOk), 'should not match any message: topic') + t.notOk(await falseMatcher(messageIdOk), 'should not match any message: text') + + const idMatcher = messageMatcher(TEXT_OK) + + t.notOk(await idMatcher(messageNotOk), 'should not match unexpected message by id') + + t.ok(await idMatcher(messageFromOk), 'should match expected from by id') + t.ok(await idMatcher(messageTopicOk), 'should match expected topic by id') + t.ok(await idMatcher(messageIdOk), 'should match expected text by id') + + const idListMatcher = messageMatcher([ TEXT_OK ]) + + t.notOk(await idListMatcher(messageNotOk), 'should not match unexpected message by id list') + + t.ok(await idListMatcher(messageFromOk), 'should match expected from by id list') + t.ok(await idListMatcher(messageTopicOk), 'should match expected topic by id list') + t.ok(await idListMatcher(messageIdOk), 'should match expected text by id list') + + const regexpMatcher = messageMatcher(new RegExp(TEXT_OK)) + + t.notOk(await regexpMatcher(messageNotOk), 'should not match unexpected message by regexp') + + t.ok(await regexpMatcher(messageFromOk), 'should match expected from by regexp') + t.ok(await regexpMatcher(messageTopicOk), 'should match expected topic by regexp') + t.ok(await regexpMatcher(messageTextOk), 'should match expected text by regexp') + + const regexpListMatcher = messageMatcher([ new RegExp(TEXT_OK) ]) + + t.notOk(await regexpListMatcher(messageNotOk), 'should not match unexpected message by regexp') + + t.ok(await regexpListMatcher(messageFromOk), 'should match expected from by regexp') + t.ok(await regexpListMatcher(messageTopicOk), 'should match expected topic by regexp') + t.ok(await regexpListMatcher(messageTextOk), 'should match expected text by regexp') + + const messageFilter = (message: Message) => [ + message.text(), + message.room()?.topic(), + message.talker().name(), + ].includes(TEXT_OK) + + const functionMatcher = messageMatcher(messageFilter) + + t.notOk(await functionMatcher(messageNotOk), 'should not match unexpected message by function') + + t.ok(await functionMatcher(messageFromOk), 'should match expected from by function') + t.ok(await functionMatcher(messageTopicOk), 'should match expected topic by function') + t.ok(await functionMatcher(messageTextOk), 'should match expected text by function') + + const functionListMatcher = messageMatcher([ messageFilter ]) + + t.notOk(await functionListMatcher(messageNotOk), 'should not match unexpected message by function list') + + t.ok(await functionListMatcher(messageFromOk), 'should match expected from by function list') + t.ok(await functionListMatcher(messageTopicOk), 'should match expected topic by function list') + t.ok(await functionListMatcher(messageTextOk), 'should match expected text by function list') + +}) diff --git a/src/plugins/matchers/message-matcher.ts b/src/plugins/matchers/message-matcher.ts new file mode 100644 index 00000000..21008bed --- /dev/null +++ b/src/plugins/matchers/message-matcher.ts @@ -0,0 +1,66 @@ +import { + Message, + log, +} from 'wechaty' + +type MessageMatcherFunction = (msg: Message) => boolean | Promise +type MessageMatcherOption = boolean | string | RegExp | MessageMatcherFunction +export type MessageMatcherOptions = MessageMatcherOption | MessageMatcherOption[] + +type MatchMessageFunction = (message: Message) => Promise + +function messageMatcher ( + matcherOptions?: MessageMatcherOptions, +): MatchMessageFunction { + log.verbose('WechatyPluginContrib', 'messageMatcher(%s)', JSON.stringify(matcherOptions)) + + if (!matcherOptions) { + return () => Promise.resolve(false) + } + + if (!Array.isArray(matcherOptions)) { + matcherOptions = [ matcherOptions ] + } + + const matcherOptionList = matcherOptions + + return async function matchMessage (message: Message): Promise { + log.verbose('WechatyPluginContrib', 'messageMatcher() matchMessage(%s)', message) + + let isMatch = false + for (const option of matcherOptionList) { + if (typeof option === 'boolean') { + isMatch = option + } else if (typeof option === 'string') { + const idCheckList = [ + message.talker().id, + message.room()?.id, + ] + isMatch = idCheckList.includes(option) + } else if (option instanceof RegExp) { + const text = await message.mentionText() + const textCheckList = [ + text, + message.talker().name(), + await message.room()?.topic(), + ] + isMatch = textCheckList.some(text => text && option.test(text)) + } else if (typeof option === 'function') { + isMatch = await option(message) + } else { + throw new Error('unknown matcher ' + option) + } + + if (isMatch) { + return true + } + + } + // no match + return false + } +} + +export { + messageMatcher, +} diff --git a/src/plugins/matchers/mod.ts b/src/plugins/matchers/mod.ts new file mode 100644 index 00000000..8a3ea0f2 --- /dev/null +++ b/src/plugins/matchers/mod.ts @@ -0,0 +1,38 @@ +/** + * Matchers + */ +import { + MessageMatcherOptions, + messageMatcher, +} from './message-matcher.js' +import { + RoomMatcherOptions, + roomMatcher, +} from './room-matcher.js' +import { + ContactMatcherOptions, + contactMatcher, +} from './contact-matcher.js' +import { + StringMatcherOptions, + stringMatcher, +} from './string-matcher.js' +import { + LanguageMatcherOptions, + languageMatcher, +} from './language-matcher.js' + +export type { + ContactMatcherOptions, + LanguageMatcherOptions, + MessageMatcherOptions, + RoomMatcherOptions, + StringMatcherOptions, +} +export { + contactMatcher, + languageMatcher, + messageMatcher, + roomMatcher, + stringMatcher, +} diff --git a/src/plugins/matchers/room-matcher.spec.ts b/src/plugins/matchers/room-matcher.spec.ts new file mode 100755 index 00000000..7219d07d --- /dev/null +++ b/src/plugins/matchers/room-matcher.spec.ts @@ -0,0 +1,133 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' + +import type { Room } from 'wechaty' + +import { roomMatcher } from './room-matcher.js' + +test('roomMatcher() smoke testing', async t => { + const matcher = roomMatcher(/test/i) + t.equal(typeof matcher, 'function', 'should return a match function') +}) + +test('roomMatcher() with string option', async t => { + const TEXT_OK = 'hello' + const TEXT_NOT_OK = 'world' + + const topicOk = () => TEXT_OK + const topicNotOk = () => TEXT_NOT_OK + + const roomIdOk = { + id: TEXT_OK, + topic: topicNotOk, + } as any as Room + + const roomTopicOk = { + id: TEXT_NOT_OK, + topic: topicOk, + } as any as Room + + const roomNotOk = { + id: TEXT_NOT_OK, + topic: topicNotOk, + } as any as Room + + const falseMatcher = roomMatcher() + t.notOk(await falseMatcher(roomIdOk), 'should not match any room without options') + t.notOk(await falseMatcher(roomTopicOk), 'should not match any room without options') + + const idMatcher = roomMatcher(TEXT_OK) + + t.notOk(await idMatcher(roomNotOk), 'should not match unexpected room by id') + + t.ok(await idMatcher(roomIdOk), 'should match expected room by id') + t.notOk(await idMatcher(roomTopicOk), 'should not match room by topic') + + const idListMatcher = roomMatcher([ TEXT_OK ]) + + t.notOk(await idListMatcher(roomNotOk), 'should not match unexpected room by id list') + + t.ok(await idListMatcher(roomIdOk), 'should match expected room by id list') + t.notOk(await idListMatcher(roomTopicOk), 'should not match room by topic list') + + const regexpMatcher = roomMatcher(new RegExp(TEXT_OK)) + + t.notOk(await regexpMatcher(roomNotOk), 'should not match unexpected room by regexp') + + t.notOk(await regexpMatcher(roomIdOk), 'should match room id by regexp') + t.ok(await regexpMatcher(roomTopicOk), 'should match expected room topic by regexp') + + const regexpListMatcher = roomMatcher([ new RegExp(TEXT_OK) ]) + + t.notOk(await regexpListMatcher(roomNotOk), 'should not match unexpected room by regexp list') + + t.notOk(await regexpListMatcher(roomIdOk), 'should not match room id by regexp list') + t.ok(await regexpListMatcher(roomTopicOk), 'should match expected room topic by regexp list') + + const roomFilter = (room: Room) => [ + room.id, + room.topic(), + ].includes(TEXT_OK) + + const functionMatcher = roomMatcher(roomFilter) + + t.notOk(await functionMatcher(roomNotOk), 'should not match unexpected room by function') + + t.ok(await functionMatcher(roomTopicOk), 'should match expected topic by function') + t.ok(await functionMatcher(roomIdOk), 'should match expected id by function') + + const functionListMatcher = roomMatcher([ roomFilter ]) + + t.notOk(await functionListMatcher(roomNotOk), 'should not match unexpected room by function list') + + t.ok(await functionListMatcher(roomTopicOk), 'should match expected topic by function list') + t.ok(await functionListMatcher(roomIdOk), 'should match expected text by function list') +}) + +test('roomMatcher with ReGexp[] options', async t => { + const TEXT_OK_1 = 'text_ok_1' + const TEXT_OK_2 = 'text_ok_2' + const TEXT_NOT_OK = 'text_not_ok' + + const topicOk1 = () => TEXT_OK_1 + const topicOk2 = () => TEXT_OK_2 + const topicNotOk = () => TEXT_NOT_OK + + const roomIdOk1 = { + id: TEXT_OK_1, + topic: topicNotOk, + } as any as Room + + const roomIdOk2 = { + id: TEXT_OK_2, + topic: topicNotOk, + } as any as Room + + const roomTopicOk1 = { + id: TEXT_NOT_OK, + topic: topicOk1, + } as any as Room + + const roomTopicOk2 = { + id: TEXT_NOT_OK, + topic: topicOk2, + } as any as Room + + const roomNotOk = { + id: TEXT_NOT_OK, + topic: topicNotOk, + } as any as Room + + const regexpMatcher = roomMatcher([ + new RegExp(TEXT_OK_1), + new RegExp(TEXT_OK_2), + ]) + + t.notOk(await regexpMatcher(roomNotOk), 'should not match unexpected room by regexp list') + + t.notOk(await regexpMatcher(roomIdOk1), 'should match room id by regexp list 1') + t.notOk(await regexpMatcher(roomIdOk2), 'should match room id by regexp list 2') + t.ok(await regexpMatcher(roomTopicOk1), 'should match expected room topic by regexp list 1') + t.ok(await regexpMatcher(roomTopicOk2), 'should match expected room topic by regexp list 2') +}) diff --git a/src/plugins/matchers/room-matcher.ts b/src/plugins/matchers/room-matcher.ts new file mode 100644 index 00000000..ed0c49e6 --- /dev/null +++ b/src/plugins/matchers/room-matcher.ts @@ -0,0 +1,60 @@ +import { + Room, + log, +} from 'wechaty' + +/** + * string here should be the room id only. + * topic should use the RegExp as the filter + */ +type RoomMatcherFunction = (room: Room) => boolean | Promise +type RoomMatcherOption = boolean | string | RegExp | RoomMatcherFunction +export type RoomMatcherOptions = RoomMatcherOption | RoomMatcherOption[] + +type MatchRoomFunction = (room: Room) => Promise + +export function roomMatcher ( + matcherOptions?: RoomMatcherOptions, +): MatchRoomFunction { + log.verbose('WechatyPluginContrib', 'roomMatcher(%s)', + matcherOptions instanceof RegExp + ? matcherOptions.toString() + : JSON.stringify(matcherOptions), + ) + + if (!matcherOptions) { + return () => Promise.resolve(false) + } + + if (!Array.isArray(matcherOptions)) { + matcherOptions = [ matcherOptions ] + } + + const matcherOptionList = matcherOptions + + return async function matchRoom (room: Room): Promise { + log.silly('WechatyPluginContrib', 'roomMatcher() matchRoom(%s)', room) + + let isMatch = false + for (const option of matcherOptionList) { + if (typeof option === 'boolean') { + isMatch = option + } else if (typeof option === 'string') { + isMatch = option === room.id + } else if (option instanceof Function) { + isMatch = await option(room) + } else if (option instanceof RegExp) { + isMatch = option.test(await room.topic()) + } else { + throw new Error('unknown option: ' + option) + } + + if (isMatch) { + return true + } + + } + // no match + return false + } +} diff --git a/src/plugins/matchers/string-matcher.spec.ts b/src/plugins/matchers/string-matcher.spec.ts new file mode 100755 index 00000000..f8c9ecd3 --- /dev/null +++ b/src/plugins/matchers/string-matcher.spec.ts @@ -0,0 +1,45 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' + +import { stringMatcher } from './string-matcher.js' + +test('stringMatcher() smoke testing', async t => { + const matcher = stringMatcher() + t.equal(typeof matcher, 'function', 'should return a match function') +}) + +test('stringMatcher()', async t => { + const TEXT_OK = 'hello' + const TEXT_NOT_OK = 'world' + + const falseMatcher = stringMatcher() + t.notOk(await falseMatcher(TEXT_OK), 'should not match any string: TEXT_OK') + t.notOk(await falseMatcher(TEXT_NOT_OK), 'should not match any string: TEXT_NOT_OK') + + const textMatcher = stringMatcher(TEXT_OK) + t.ok(await textMatcher(TEXT_OK), 'should match expected TEXT') + t.notOk(await textMatcher(TEXT_NOT_OK), 'should not match unexpected string') + + const textListMatcher = stringMatcher([ TEXT_OK ]) + t.ok(await textListMatcher(TEXT_OK), 'should match expected TEXT by list') + t.notOk(await textListMatcher(TEXT_NOT_OK), 'should not match unexpected string by list') + + const regexpMatcher = stringMatcher(new RegExp(TEXT_OK)) + t.notOk(await regexpMatcher(TEXT_NOT_OK), 'should not match unexpected string by regexp') + t.ok(await regexpMatcher(TEXT_OK), 'should match expected from by regexp') + + const regexpListMatcher = stringMatcher([ new RegExp(TEXT_OK) ]) + t.notOk(await regexpListMatcher(TEXT_NOT_OK), 'should not match unexpected string by regexp list') + t.ok(await regexpListMatcher(TEXT_OK), 'should match expected from by regexp list') + + const stringFilter = (text: string) => text === TEXT_OK + + const functionMatcher = stringMatcher(stringFilter) + t.notOk(await functionMatcher(TEXT_NOT_OK), 'should not match unexpected string by function') + t.ok(await functionMatcher(TEXT_OK), 'should match expected from by function') + + const functionListMatcher = stringMatcher([ stringFilter ]) + t.notOk(await functionListMatcher(TEXT_NOT_OK), 'should not match unexpected string by function list') + t.ok(await functionListMatcher(TEXT_OK), 'should match expected from by function list') +}) diff --git a/src/plugins/matchers/string-matcher.ts b/src/plugins/matchers/string-matcher.ts new file mode 100644 index 00000000..22bc3eec --- /dev/null +++ b/src/plugins/matchers/string-matcher.ts @@ -0,0 +1,51 @@ +import { + log, +} from 'wechaty' + +type StringMatcherFunction = (str: string) => boolean | Promise +type StringMatcherOption = boolean | string | RegExp | StringMatcherFunction +export type StringMatcherOptions = StringMatcherOption | StringMatcherOption[] + +type MatchStringFunction = (text: string) => Promise + +export function stringMatcher ( + options?: StringMatcherOptions, +): MatchStringFunction { + log.verbose('WechatyPluginContrib', 'stringMatcher(%s)', JSON.stringify(options)) + + if (!options) { + return () => Promise.resolve(false) + } + + if (!Array.isArray(options)) { + options = [ options ] + } + + const optionsList = options + + return async function matchString (str: string): Promise { + log.verbose('WechatyPluginContrib', 'stringMatcher() matchString(%s)', str) + + let isMatch = false + for (const option of optionsList) { + if (typeof option === 'boolean') { + isMatch = option + } if (typeof option === 'string') { + isMatch = (str === option) + } else if (option instanceof RegExp) { + isMatch = option.test(str) + } else if (option instanceof Function) { + isMatch = await option(str) + } else { + throw new Error('configPassword is unknown: ' + option) + } + + if (isMatch) { + return true + } + + } + // no match + return false + } +} diff --git a/src/plugins/mod.ts b/src/plugins/mod.ts new file mode 100644 index 00000000..ca8f6d1f --- /dev/null +++ b/src/plugins/mod.ts @@ -0,0 +1,39 @@ +import { VikaBot, TaskConfig } from './vika.js' +import { wxai } from './wxai.js' +import { sendNotice } from './group-notice.js' + +import { ChatDevice } from './chat-device.js' +import { propertyMessage, eventMessage } from './msg-format.js' +import { getFormattedRideInfo } from './riding.js' + +import { + exportContactsAndRoomsToCSV, + exportContactsAndRoomsToXLSX, +} from './contact-room.js' + +import { + getContact, + getRoom, + isThisContact, + isThisRoom, +} from './finder.js' + +export type { + TaskConfig, +} + +export { + exportContactsAndRoomsToCSV, + exportContactsAndRoomsToXLSX, + VikaBot, + getFormattedRideInfo, + sendNotice, + wxai, + ChatDevice, + propertyMessage, + eventMessage, + getContact, + getRoom, + isThisContact, + isThisRoom, +} diff --git a/src/plugins/msg-format.js b/src/plugins/msg-format.js deleted file mode 100644 index 4d5325a6..00000000 --- a/src/plugins/msg-format.js +++ /dev/null @@ -1,240 +0,0 @@ -import { v4 } from 'uuid' -import moment from 'moment' -// import { -// Contact, -// log, -// Message, -// ScanStatus, -// Wechaty, -// UrlLink, -// MiniProgram -// } from "wechaty" - -import * as PUPPET from 'wechaty-puppet' - -function getCurTime() { - //timestamp是整数,否则要parseInt转换 - let timestamp = new Date().getTime() - var timezone = 8; //目标时区时间,东八区 - var offset_GMT = new Date().getTimezoneOffset(); // 本地时间和格林威治的时间差,单位为分钟 - var time = timestamp + offset_GMT * 60 * 1000 + timezone * 60 * 60 * 1000 - return time -} - -async function wechaty2chatdev(message) { - let curTime = getCurTime() - let timeHms = moment(curTime).format("YYYY-MM-DD HH:mm:ss") - - let msg = { - "reqId": v4(), - "method": "thing.event.post", - "version": "1.0", - "timestamp": curTime, - "events": { - } - } - - const talker = message.talker() - - let text = '' - let messageType = '' - let textBox = {} - let file = '' - let msgId = message.id - - switch (message.type()) { - // 文本消息 - case PUPPET.types.Message.Text: - messageType = 'Text' - text = message.text() - break; - - // 图片消息 - case PUPPET.types.Message.Image: - messageType = 'Image' - file = await message.toImage().artwork() - break; - - // 链接卡片消息 - case PUPPET.types.Message.Url: - messageType = 'Url' - textBox = await message.toUrlLink() - text = JSON.stringify(JSON.parse(JSON.stringify(textBox)).payload) - break; - - // 小程序卡片消息 - case PUPPET.types.Message.MiniProgram: - messageType = 'MiniProgram' - textBox = await message.toMiniProgram(); - text = JSON.stringify(JSON.parse(JSON.stringify(textBox)).payload) - /* - miniProgram: 小程序卡片数据 - { - appid: "wx363a...", - description: "贝壳找房 - 真房源", - title: "美国白宫,10室8厅9卫,99999刀/月", - iconUrl: "http://mmbiz.qpic.cn/mmbiz_png/.../640?wx_fmt=png&wxfrom=200", - pagePath: "pages/home/home.html...", - shareId: "0_wx363afd5a1384b770_..._1615104758_0", - thumbKey: "84db921169862291...", - thumbUrl: "3051020100044a304802010002046296f57502033d14...", - username: "gh_8a51...@app" - } - */ - break; - - // 语音消息 - case PUPPET.types.Message.Audio: - messageType = 'Audio' - file = await message.toFileBox() - break; - - // 视频消息 - case PUPPET.types.Message.Video: - messageType = 'Video' - file = await message.toFileBox(); - break; - - // 动图表情消息 - case PUPPET.types.Message.Emoticon: - messageType = 'Emoticon' - file = await message.toFileBox(); - break; - - // 文件消息 - case PUPPET.types.Message.Attachment: - messageType = 'Attachment' - file = await message.toFileBox() - break; - - case PUPPET.types.Message.Contact: - messageType = 'Contact' - try { - textBox = await message.toContact() - } catch (err) { - - } - text = '联系人卡片消息' - break; - - // 其他消息 - default: - messageType = 'Unknown' - text = '未知的消息类型' - break; - } - - if (file) { - text = file.name - } - - // console.debug('textBox:', textBox) - - let room = message.room() - let roomInfo = {} - if (room && room.id) { - roomInfo.id = room.id - try { - let room_avatar = await room.avatar() - // console.debug('群头像room.avatar()============') - // console.debug(typeof room_avatar) - // console.debug(room_avatar) - // console.debug('END============') - - roomInfo.avatar = JSON.parse(JSON.stringify(room_avatar)).url - } catch (err) { - console.debug('群头像捕获了错误============') - // console.debug(typeof err) - // console.debug(err) - // console.debug('END============') - } - roomInfo.ownerId = room.owner()?.id||'' - - try { - roomInfo.topic = await room.topic() - } catch (err) { - roomInfo.topic = room.id - } - } - - let memberAlias = '' - try { - memberAlias = await room.alias(talker) - } catch (err) { - - } - - let avatar = '' - try { - - avatar = await talker.avatar() - // console.debug('好友头像talker.avatar()============') - // console.debug(avatar) - // console.debug('END============') - avatar = JSON.parse(JSON.stringify(avatar)).url - - } catch (err) { - console.debug('好友头像捕获了错误============') - // console.debug(err) - // console.debug('END============') - } - - let content = {} - content.messageType = messageType - content.text = text - content.raw = textBox.payload || textBox._payload || {} - - let _payload = { - "id": msgId, - "talker": { - "id": talker.id, - "gender": talker.gender() || '', - "name": talker.name() || '', - "alias": await talker.alias() || '', - "memberAlias": memberAlias, - "avatar": avatar - }, - "room": roomInfo, - "content": content, - "timestamp": curTime, - "timeHms": timeHms - } - - msg.events.message = _payload - msg = JSON.stringify(msg) - - return msg - -} - - -function propertyMessage(name, info) { - let message = { - "reqId": v4(), - "method": "thing.property.post", - "version": "1.0", - "timestamp": new Date().getTime(), - "properties": { - } - } - message.properties[name] = info - message = JSON.stringify(message) - return message -} - -function eventMessage(name, info) { - let message = { - "reqId": v4(), - "method": "thing.event.post", - "version": "1.0", - "timestamp": new Date().getTime(), - "events": { - } - } - message.events[name] = info - message = JSON.stringify(message) - return message -} - -export { wechaty2chatdev, propertyMessage, eventMessage } -export default wechaty2chatdev \ No newline at end of file diff --git a/src/plugins/msg-format.ts b/src/plugins/msg-format.ts new file mode 100644 index 00000000..7e031f75 --- /dev/null +++ b/src/plugins/msg-format.ts @@ -0,0 +1,232 @@ +/* eslint-disable sort-keys */ +import { v4 } from 'uuid' +import moment from 'moment' + +import { Message, types } from 'wechaty' +// import { FileBox } from 'file-box' + +function getCurTime () { + // timestamp是整数,否则要parseInt转换 + const timestamp = new Date().getTime() + const timezone = 8 // 目标时区时间,东八区 + const offsetGMT = new Date().getTimezoneOffset() // 本地时间和格林威治的时间差,单位为分钟 + const time = timestamp + offsetGMT * 60 * 1000 + timezone * 60 * 60 * 1000 + return time +} + +async function wechaty2chatdev (message:Message) { + const curTime = getCurTime() + const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss') + + let msg:any = { + reqId: v4(), + method: 'thing.event.post', + version: '1.0', + timestamp: curTime, + events: { + }, + } + + const talker = message.talker() + + let text = '' + let messageType = '' + let textBox:any = {} + let file: any + const msgId = message.id + + switch (message.type()) { + // 文本消息 + case types.Message.Text: + messageType = 'Text' + text = message.text() + break + + // 图片消息 + case types.Message.Image: + messageType = 'Image' + file = await message.toImage().artwork() + break + + // 链接卡片消息 + case types.Message.Url: + messageType = 'Url' + textBox = await message.toUrlLink() + text = JSON.stringify(JSON.parse(JSON.stringify(textBox)).payload) + break + + // 小程序卡片消息 + case types.Message.MiniProgram: + messageType = 'MiniProgram' + textBox = await message.toMiniProgram() + text = JSON.stringify(JSON.parse(JSON.stringify(textBox)).payload) + /* + miniProgram: 小程序卡片数据 + { + appid: "wx363a...", + description: "贝壳找房 - 真房源", + title: "美国白宫,10室8厅9卫,99999刀/月", + iconUrl: "http://mmbiz.qpic.cn/mmbiz_png/.../640?wx_fmt=png&wxfrom=200", + pagePath: "pages/home/home.html...", + shareId: "0_wx363afd5a1384b770_..._1615104758_0", + thumbKey: "84db921169862291...", + thumbUrl: "3051020100044a304802010002046296f57502033d14...", + username: "gh_8a51...@app" + } + */ + break + + // 语音消息 + case types.Message.Audio: + messageType = 'Audio' + file = await message.toFileBox() + break + + // 视频消息 + case types.Message.Video: + messageType = 'Video' + file = await message.toFileBox() + break + + // 动图表情消息 + case types.Message.Emoticon: + messageType = 'Emoticon' + file = await message.toFileBox() + break + + // 文件消息 + case types.Message.Attachment: + messageType = 'Attachment' + file = await message.toFileBox() + break + + case types.Message.Contact: + messageType = 'Contact' + try { + textBox = await message.toContact() + } catch (err) { + + } + text = '联系人卡片消息' + break + + // 其他消息 + default: + messageType = 'Unknown' + text = '未知的消息类型' + break + } + + if (file) { + text = file.name + } + + // console.debug('textBox:', textBox) + + const room = message.room() + const roomInfo:any = {} + if (room && room.id) { + roomInfo.id = room.id + try { + const roomAvatar = await room.avatar() + // console.debug('群头像room.avatar()============') + // console.debug(typeof roomAvatar) + // console.debug(roomAvatar) + // console.debug('END============') + + roomInfo.avatar = JSON.parse(JSON.stringify(roomAvatar)).url + } catch (err) { + // console.debug('群头像捕获了错误============') + // console.debug(typeof err) + // console.debug(err) + // console.debug('END============') + } + roomInfo.ownerId = room.owner()?.id || '' + + try { + roomInfo.topic = await room.topic() + } catch (err) { + roomInfo.topic = room.id + } + } + + let memberAlias:any = '' + try { + memberAlias = await room?.alias(talker) + } catch (err) { + + } + + let avatar:any = '' + try { + + avatar = await talker.avatar() + // console.debug('好友头像talker.avatar()============') + // console.debug(avatar) + // console.debug('END============') + avatar = JSON.parse(JSON.stringify(avatar)).url + + } catch (err) { + // console.debug('好友头像捕获了错误============') + // console.debug(err) + // console.debug('END============') + } + + const content:any = {} + content.messageType = messageType + content.text = text + content.raw = textBox.payload || textBox._payload || {} + + const _payload = { + id: msgId, + talker: { + id: talker.id, + gender: talker.gender() || '', + name: talker.name() || '', + alias: await talker.alias() || '', + memberAlias, + avatar, + }, + room: roomInfo, + content, + timestamp: curTime, + timeHms, + } + + msg.events.message = _payload + msg = JSON.stringify(msg) + + return msg + +} + +function propertyMessage (name:string, info:any) { + let message:any = { + reqId: v4(), + method: 'thing.property.post', + version: '1.0', + timestamp: new Date().getTime(), + properties: { + }, + } + message.properties[name] = info + message = JSON.stringify(message) + return message +} + +function eventMessage (name:string, info:any) { + let message:any = { + reqId: v4(), + method: 'thing.event.post', + version: '1.0', + timestamp: new Date().getTime(), + events: { + }, + } + message.events[name] = info + message = JSON.stringify(message) + return message +} + +export { wechaty2chatdev, propertyMessage, eventMessage } +export default wechaty2chatdev diff --git a/src/plugins/run.ts b/src/plugins/run.ts new file mode 100644 index 00000000..c9f3bd85 --- /dev/null +++ b/src/plugins/run.ts @@ -0,0 +1,17 @@ +import fs from 'fs' +import type { Plugin, PluginConfig } from '../types/interface' + +function loadPlugins (app: any) { + const config: PluginConfig[] = JSON.parse(fs.readFileSync('./config.json', 'utf8')) + config.forEach((pluginConfig: PluginConfig) => { + const plugin: Plugin = require(`./plugins/${pluginConfig.name}`) + plugin.init(app) + pluginConfig.active ? plugin.enable(app) : plugin.disable(app) + }) +} + +const app = {} +// 监听文件 +fs.watchFile('./config.json', (_curr, _prev) => { + loadPlugins(app) +}) diff --git a/src/plugins/store-messages-locally.ts b/src/plugins/store-messages-locally.ts index 06c31ace..83ff78e0 100644 --- a/src/plugins/store-messages-locally.ts +++ b/src/plugins/store-messages-locally.ts @@ -7,7 +7,6 @@ import { import * as PUPPET from 'wechaty-puppet' // 导入wechaty-puppet-wechat模块,用于连接网页版微信 -import { PuppetWeChat } from 'wechaty-puppet-wechat' // 导入qrcode-terminal模块,用于在终端显示二维码 import qrcodeTerminal from 'qrcode-terminal' // 引入fs模块,用于文件操作 diff --git a/src/plugins/talkers/contact-talker.spec.ts b/src/plugins/talkers/contact-talker.spec.ts new file mode 100755 index 00000000..c5c12176 --- /dev/null +++ b/src/plugins/talkers/contact-talker.spec.ts @@ -0,0 +1,70 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { + test, + sinon, +} from 'tstest' + +import type { Contact } from 'wechaty' + +import { + contactTalker, + ContactTalkerOptions, +} from './contact-talker.js' + +test('contactTalker()', async t => { + const spy1 = sinon.spy() + const spy2 = sinon.spy() + const spy3 = sinon.spy() + const spy4 = sinon.spy() + + const EXPECTED_TEXT = 'text' + + const OPTIONS_TEXT: ContactTalkerOptions = EXPECTED_TEXT + const OPTIONS_FUNCTION: ContactTalkerOptions = spy1 + const OPTIONS_FUNCTION_LIST: ContactTalkerOptions = [ spy2, spy3 ] + + const mockContact = { + say: spy4, + wechaty: { + sleep: () => undefined, + }, + } as any as Contact + + await contactTalker(OPTIONS_TEXT)(mockContact) + t.ok(spy4.called, 'should called the contact.say') + t.equal(spy4.args[0]![0], EXPECTED_TEXT, 'should say the expected text') + + await contactTalker(OPTIONS_FUNCTION)(mockContact) + t.ok(spy1.called, 'should called the function') + t.equal(spy1.args[0]![0], mockContact, 'should called the function with contact') + + const talkContact = contactTalker(OPTIONS_FUNCTION_LIST) + await talkContact(mockContact) + t.ok(spy2.called, 'should called the functions 1') + t.ok(spy3.called, 'should called the functions 2') + t.equal(spy2.args[0]![0], mockContact, 'should called the functions 1 with contact') + t.equal(spy3.args[0]![0], mockContact, 'should called the functions 2 with contact') +}) + +test('contactTalker() with mustache', async t => { + const EXPECTED_TEXT = 'Hello, world!' + const OPTIONS_TEXT: ContactTalkerOptions = 'Hello, {{ name }}!' + const VAR = 'world' + + const spy = sinon.spy() + const mockContact = { + say: spy, + wechaty: { + sleep: () => undefined, + }, + } as any as Contact + + const view = { name: VAR } + + const talkContact = contactTalker(OPTIONS_TEXT) + + await talkContact(mockContact, undefined, view) + t.ok(spy.called, 'should called the contact.say') + t.equal(spy.args[0]![0], EXPECTED_TEXT, 'should say the expected text') +}) diff --git a/src/plugins/talkers/contact-talker.ts b/src/plugins/talkers/contact-talker.ts new file mode 100644 index 00000000..e58289f0 --- /dev/null +++ b/src/plugins/talkers/contact-talker.ts @@ -0,0 +1,62 @@ +import { + Contact, + log, + Room, +} from 'wechaty' +import Mustache from 'mustache' + +import type * as types from '../types/mod.js' + +type ContactTalkerFunction = (contact: Contact, room?: Room) => types.TalkerMessage | Promise +type ContactTalkerOption = types.TalkerMessage | ContactTalkerFunction +export type ContactTalkerOptions = ContactTalkerOption | ContactTalkerOption[] + +export function contactTalker (options?: ContactTalkerOptions) { + log.verbose('WechatyPluginContrib', 'contactTalker(%s)', JSON.stringify(options)) + + if (!options) { + return () => undefined + } + + if (!Array.isArray(options)) { + options = [ options ] + } + + const optionList = options + + return async function talkContact (contact: Contact, room?: Room, mustacheView?: T): Promise { + log.silly('WechatyPluginContrib', 'contactTalker() talkContact(%s, %s)', + contact, + mustacheView + ? JSON.stringify(mustacheView) + : '', + ) + + for (const option of optionList) { + let msg + if (option instanceof Function) { + msg = await option(contact, room) + } else { + msg = option + } + + if (!msg) { continue } + + if (typeof msg === 'string') { + if (mustacheView) { + msg = Mustache.render(msg, mustacheView) + } + + await contact.say(msg) + + } else { + /** + * FIXME(huan): https://github.com/microsoft/TypeScript/issues/14107 + */ + await contact.say(msg as any) + } + + await contact.wechaty.sleep(1000) + } + } +} diff --git a/src/plugins/talkers/message-talker.spec.ts b/src/plugins/talkers/message-talker.spec.ts new file mode 100755 index 00000000..00138613 --- /dev/null +++ b/src/plugins/talkers/message-talker.spec.ts @@ -0,0 +1,69 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' +import sinon from 'sinon' + +import type { + Message, +} from 'wechaty' + +import { + messageTalker, + MessageTalkerOptions, +} from './message-talker.js' + +test('messageTalker()', async t => { + const spy2 = sinon.spy() + const spy3 = sinon.spy() + const spy4 = sinon.spy() + + const EXPECTED_TEXT = 'text' + + const OPTIONS_TEXT: MessageTalkerOptions = EXPECTED_TEXT + const OPTIONS_FUNCTION_LIST: MessageTalkerOptions = [ spy2, spy3 ] + + const mockMessage = { + say: spy4, + wechaty: { + sleep: () => undefined, + }, + } as any as Message + + let talkMessage = messageTalker(OPTIONS_TEXT) + spy4.resetHistory() + await talkMessage(mockMessage) + t.ok(spy4.called, 'should called the contact.say') + t.equal(spy4.args[0]![0], EXPECTED_TEXT, 'should say the expected text') + + talkMessage = messageTalker(OPTIONS_FUNCTION_LIST) + spy2.resetHistory() + spy3.resetHistory() + await talkMessage(mockMessage) + t.ok(spy2.called, 'should called the functions 1') + t.equal(spy2.args[0]![0], mockMessage, 'should called the functions 1/1 with mockMessage') + + t.ok(spy3.called, 'should called the functions 2') + t.equal(spy3.args[0]![0], mockMessage, 'should called the functions 2/1 with contact') +}) + +test('messageTalker() with mustache', async t => { + const EXPECTED_TEXT = 'Hello, world!' + const OPTIONS_TEXT: MessageTalkerOptions = 'Hello, {{ name }}!' + const VAR = 'world' + + const spy = sinon.spy() + const mockMessage = { + say: spy, + wechaty: { + sleep: () => undefined, + }, + } as any as Message + + const view = { name: VAR } + + const talkMessage = messageTalker(OPTIONS_TEXT) + + await talkMessage(mockMessage, view) + t.ok(spy.called, 'should called the contact.say') + t.equal(spy.args[0]![0], EXPECTED_TEXT, 'should say the expected text') +}) diff --git a/src/plugins/talkers/message-talker.ts b/src/plugins/talkers/message-talker.ts new file mode 100644 index 00000000..bbf8bfd5 --- /dev/null +++ b/src/plugins/talkers/message-talker.ts @@ -0,0 +1,52 @@ +/* eslint-disable brace-style */ +import { + Message, + log, +} from 'wechaty' +import Mustache from 'mustache' + +import * as mapper from '../mappers/message-mapper.js' + +export type MessageTalkerOptions = mapper.MessageMapperOptions + +export function messageTalker (options?: MessageTalkerOptions) { + log.verbose('WechatyPluginContrib', 'messageTalker(%s)', JSON.stringify(options)) + + if (!options) { + return () => undefined + } + + const mapMessage = mapper.messageMapper(options) + + return async function talkMessage (message: Message, mustacheView: T): Promise { + log.silly('WechatyPluginContrib', 'messageTalker() talkMessage(%s, %s)', + message, + mustacheView + ? JSON.stringify(mustacheView) + : '', + ) + + const msgList = await mapMessage(message) + + for (const msg of msgList) { + if (!msg) { continue } + + if (typeof msg === 'string') { + + let text = msg + if (mustacheView) { + text = Mustache.render(msg, mustacheView) + } + await message.say(text) + + } else { + /** + * FIXME(huan): https://github.com/microsoft/TypeScript/issues/14107 + */ + await message.say(msg as any) + } + + await message.wechaty.sleep(1000) + } + } +} diff --git a/src/plugins/talkers/mod.ts b/src/plugins/talkers/mod.ts new file mode 100644 index 00000000..e3c1efb1 --- /dev/null +++ b/src/plugins/talkers/mod.ts @@ -0,0 +1,26 @@ +/** + * Talkers + */ +import { + RoomTalkerOptions, + roomTalker, +} from './room-talker.js' +import { + ContactTalkerOptions, + contactTalker, +} from './contact-talker.js' +import { + MessageTalkerOptions, + messageTalker, +} from './message-talker.js' + +export type { + ContactTalkerOptions, + MessageTalkerOptions, + RoomTalkerOptions, +} +export { + contactTalker, + messageTalker, + roomTalker, +} diff --git a/src/plugins/talkers/room-talker.spec.ts b/src/plugins/talkers/room-talker.spec.ts new file mode 100755 index 00000000..34643aab --- /dev/null +++ b/src/plugins/talkers/room-talker.spec.ts @@ -0,0 +1,137 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' +import sinon from 'sinon' + +import type { + Contact, + Room, +} from 'wechaty' + +import { + roomTalker, + RoomTalkerOptions, +} from './room-talker.js' + +test('roomTalker()', async t => { + const spy2 = sinon.spy() + const spy3 = sinon.spy() + const spy4 = sinon.spy() + + const EXPECTED_TEXT = 'text' + + const OPTIONS_TEXT: RoomTalkerOptions = EXPECTED_TEXT + const OPTIONS_FUNCTION_LIST: RoomTalkerOptions = [ spy2, spy3 ] + + const mockContact = {} as any as Contact + const mockRoom = { + say: spy4, + wechaty: { + sleep: () => undefined, + }, + } as any as Room + + let talkRoom = roomTalker(OPTIONS_TEXT) + spy4.resetHistory() + await talkRoom(mockRoom, mockContact) + t.ok(spy4.called, 'should called the contact.say') + t.equal(spy4.args[0]![0], EXPECTED_TEXT, 'should say the expected text') + t.equal(spy4.args[0]![1], mockContact, 'should pass contact to say') + + talkRoom = roomTalker(OPTIONS_FUNCTION_LIST) + spy2.resetHistory() + spy3.resetHistory() + await talkRoom(mockRoom, mockContact) + t.ok(spy2.called, 'should called the functions 1') + t.equal(spy2.args[0]![0], mockRoom, 'should called the functions 1/1 with mockRoom') + t.equal(spy2.args[0]![1], mockContact, 'should called the functions 1/2 with mockContact') + + t.ok(spy3.called, 'should called the functions 2') + t.equal(spy3.args[0]![0], mockRoom, 'should called the functions 2/1 with contact') + t.equal(spy3.args[0]![1], mockContact, 'should called the functions 2/2 with mockContact') +}) + +test('roomTalker() with mustache', async t => { + const EXPECTED_TEXT = 'Hello, world!' + const OPTIONS_TEXT: RoomTalkerOptions = 'Hello, {{ name }}!' + const VAR = 'world' + + const spy = sinon.spy() + const mockContact = {} as any as Contact + const mockRoom = { + say: spy, + wechaty: { + sleep: () => undefined, + }, + } as any as Room + + const view = { name: VAR } + + const talkRoom = roomTalker(OPTIONS_TEXT) + + await talkRoom(mockRoom, mockContact, view) + t.ok(spy.called, 'should called the contact.say') + t.equal(spy.args[0]![0], EXPECTED_TEXT, 'should say the expected text') + t.equal(spy.args[0]![1], mockContact, 'should say with mockContact') +}) + +test('roomTalker() with room list', async t => { + const spy2 = sinon.spy() + const spy3 = sinon.spy() + const spy4 = sinon.spy() + const spy5 = sinon.spy() + + const EXPECTED_TEXT = 'text' + + const OPTIONS_TEXT: RoomTalkerOptions = EXPECTED_TEXT + const OPTIONS_FUNCTION_LIST: RoomTalkerOptions = [ spy2, spy3 ] + + const mockContact1 = {} as any as Contact + const mockContact2 = {} as any as Contact + + const mockRoom1 = { + say: spy4, + wechaty: { + sleep: () => undefined, + }, + } as any as Room + + const mockRoom2 = { + say: spy5, + wechaty: { + sleep: () => undefined, + }, + } as any as Room + + let talkRoom = roomTalker(OPTIONS_TEXT) + spy4.resetHistory() + await talkRoom([ mockRoom1, mockRoom2 ], [ mockContact1, mockContact2 ]) + t.ok(spy4.calledOnce, 'should called the room1.say once') + t.equal(spy4.args[0]![0], EXPECTED_TEXT, 'should say the expected text') + t.equal(spy4.args[0]![1], mockContact1, 'should pass contact1 to say') + t.equal(spy4.args[0]![2], mockContact2, 'should pass contact2 to say') + t.ok(spy5.calledOnce, 'should called the room2.say once') + t.equal(spy5.args[0]![0], EXPECTED_TEXT, 'should say the expected text') + t.equal(spy5.args[0]![1], mockContact1, 'should pass contact1 to say') + t.equal(spy5.args[0]![2], mockContact2, 'should pass contact2 to say') + + talkRoom = roomTalker(OPTIONS_FUNCTION_LIST) + spy2.resetHistory() + spy3.resetHistory() + await talkRoom([ mockRoom1, mockRoom2 ], [ mockContact1, mockContact2 ]) + t.ok(spy2.called, 'should called the functions 1') + t.equal(spy2.args[0]![0], mockRoom1, 'should called the functions 1/1 with mockRoom1') + t.equal(spy2.args[0]![1], mockContact1, 'should called the functions 1/2 with mockContact1') + t.equal(spy2.args[0]![2], mockContact2, 'should called the functions 1/3 with mockContact2') + t.equal(spy2.args[1]![0], mockRoom2, 'should called the functions 1/1 with mockRoom2') + t.equal(spy2.args[1]![1], mockContact1, 'should called the functions 1/2 with mockContact1') + t.equal(spy2.args[1]![2], mockContact2, 'should called the functions 1/3 with mockContact2') + + t.ok(spy3.called, 'should called the functions 2') + t.equal(spy3.args[0]![0], mockRoom1, 'should called the functions 2/1 with mockRoom1') + t.equal(spy3.args[0]![1], mockContact1, 'should called the functions 2/2 with mockContact1') + t.equal(spy3.args[0]![2], mockContact2, 'should called the functions 2/3 with mockContact2') + t.equal(spy3.args[1]![0], mockRoom2, 'should called the functions 2/1 with mockRoom2') + t.equal(spy3.args[1]![1], mockContact1, 'should called the functions 2/2 with mockContact1') + t.equal(spy3.args[1]![2], mockContact2, 'should called the functions 2/3 with mockContact2') +}) diff --git a/src/plugins/talkers/room-talker.ts b/src/plugins/talkers/room-talker.ts new file mode 100644 index 00000000..5de39762 --- /dev/null +++ b/src/plugins/talkers/room-talker.ts @@ -0,0 +1,79 @@ +/* eslint-disable brace-style */ +import { + log, + Room, + Contact, +} from 'wechaty' +import Mustache from 'mustache' + +import type * as types from '../types/mod.js' + +type RoomTalkerFunction = (room: Room, contact?: Contact) => types.TalkerMessage | Promise +type RoomTalkerOption = types.TalkerMessage | RoomTalkerFunction +export type RoomTalkerOptions = RoomTalkerOption | RoomTalkerOption[] + +export function roomTalker (options?: RoomTalkerOptions) { + log.verbose('WechatyPluginContrib', 'roomTalker(%s)', JSON.stringify(options)) + + if (!options) { + return () => undefined + } + + if (!Array.isArray(options)) { + options = [ options ] + } + + const optionList = options + + return async function talkRoom ( + rooms: Room | Room[], + contacts?: Contact | Contact[], + mustacheView?: T, + ): Promise { + log.verbose('WechatyPluginContrib', 'roomTalker() talkRoom(%s, %s, %s)', + rooms, + contacts || '', + mustacheView + ? JSON.stringify(mustacheView) + : '', + ) + + if (!Array.isArray(rooms)) { + rooms = [ rooms ] + } + if (typeof contacts === 'undefined') { + contacts = [] + } else if (!Array.isArray(contacts)) { + contacts = [ contacts ] + } + + for (const room of rooms) { + await loopOptionList(room, contacts) + await room.wechaty.sleep(1000) + } + + async function loopOptionList (room: Room, contactList: Contact[]): Promise { + for (const option of optionList) { + let msg + if (option instanceof Function) { + msg = await option(room, ...contactList) + } else { + msg = option + } + + if (!msg) { continue } + + if (typeof msg === 'string') { + if (mustacheView) { + msg = Mustache.render(msg, mustacheView) + } + await room.say(msg, ...contactList) + } else { + await room.say(msg) + } + + await room.wechaty.sleep(1000) + } + } + } +} diff --git a/src/plugins/types/mod.ts b/src/plugins/types/mod.ts new file mode 100644 index 00000000..d7f8844a --- /dev/null +++ b/src/plugins/types/mod.ts @@ -0,0 +1,11 @@ +import { + talkerMessageFrom, + TalkerMessage, +} from './talker-message.js' + +export type { + TalkerMessage, +} +export { + talkerMessageFrom, +} diff --git a/src/plugins/types/talker-message.ts b/src/plugins/types/talker-message.ts new file mode 100644 index 00000000..bb3e8822 --- /dev/null +++ b/src/plugins/types/talker-message.ts @@ -0,0 +1,45 @@ +import type { + Sayable, + Message, +} from 'wechaty' +import { + types, + log, +} from 'wechaty' + +/** + * 1. `void` & `undefined` means drop the message + * 1. `Message` means forward the original message + */ +type TalkerMessage = void | undefined | Sayable + +async function talkerMessageFrom (message: Message): Promise { + const msgType = message.type() + switch (msgType) { + case types.Message.Text: + return message.text() + case types.Message.Image: + case types.Message.Attachment: + case types.Message.Audio: + case types.Message.Video: + case types.Message.Emoticon: + return message.toFileBox() + case types.Message.Contact: + return message.toContact() + case types.Message.Url: + return message.toUrlLink() + case types.Message.MiniProgram: + return message.toMiniProgram() + + default: + log.silly('Wechaty', 'talkerMessageFrom(%s) non-convertible type: %s', message, msgType) + return undefined + } +} + +export type { + TalkerMessage, +} +export { + talkerMessageFrom, +} diff --git a/src/plugins/vika.ts b/src/plugins/vika.ts index 69d2d256..f75fab45 100644 --- a/src/plugins/vika.ts +++ b/src/plugins/vika.ts @@ -11,7 +11,6 @@ import moment from 'moment' // import schedule from 'node-schedule' import fs from 'fs' -import console from 'console' import { Contact, @@ -26,7 +25,7 @@ import { FileBox } from 'file-box' import type { Sheets, Field } from './lib/vikaModel/Model.js' import { sheets } from './lib/vikaModel/index.js' -import { waitForMs as wait } from '../util/tool.js' +import { waitForMs as wait } from '../utils/utils.js' // import { sheets } from './lib/dataModel.js' @@ -35,6 +34,17 @@ type VikaBotConfigTypes = { token: string, } +export interface TaskConfig { + id: string; + msg: string; + time: string; + cycle: string; + targetType: 'contact' | 'room'; + targetId: string; + targetName: string; + active: boolean; +} + class VikaBot { token!: string @@ -52,12 +62,14 @@ class VikaBot { contactWhiteListSheet!: string noticeSheet!: string msgStore!: any[] + envsOnVika!:any[] + switchsOnVika!:any[] constructor (config: VikaBotConfigTypes) { if (!config.token) { - console.error('未配置token,请在config.ts中配置') + log.error('未配置token,请在config.ts中配置') } else if (!config.spaceName) { - console.error('未配置空间名称,请在config.ts中配置') + log.error('未配置空间名称,请在config.ts中配置') } else { this.token = config.token this.spaceName = config.spaceName @@ -72,10 +84,10 @@ class VikaBot { // 获取当前用户的空间站列表 const spaceListResp = await this.vika.spaces.list() if (spaceListResp.success) { - // console.log(spaceListResp.data.spaces) + // log.info(spaceListResp.data.spaces) return spaceListResp.data.spaces } else { - console.error(spaceListResp) + log.error('获取空间列表失败:', spaceListResp) return spaceListResp } } @@ -100,7 +112,7 @@ class VikaBot { const nodeListResp = await this.vika.nodes.list({ spaceId: this.spaceId }) const tables: any = {} if (nodeListResp.success) { - // console.log(nodeListResp.data.nodes); + // log.info(nodeListResp.data.nodes); const nodes = nodeListResp.data.nodes nodes.forEach((node: any) => { // 当节点是文件夹时,可以执行下列代码获取文件夹下的文件信息 @@ -109,7 +121,7 @@ class VikaBot { } }) } else { - console.error(nodeListResp) + log.error('获取数据表失败:', nodeListResp) } return tables } @@ -119,10 +131,10 @@ class VikaBot { const fieldsResp = await datasheet.fields.list() let fields: any = [] if (fieldsResp.success) { - console.log(JSON.stringify(fieldsResp.data.fields)) + log.info('getSheetFields获取字段:', JSON.stringify(fieldsResp.data.fields)) fields = fieldsResp.data.fields } else { - console.error(fieldsResp) + log.error('获取字段失败:', fieldsResp) } return fields } @@ -137,42 +149,42 @@ class VikaBot { try { const res: any = await this.vika.space(this.spaceId).datasheets.create(datasheetRo) - console.log(`系统表【${name}】创建成功,表ID【${res.data.id}】`) + log.info(`系统表【${name}】创建成功,表ID【${res.data.id}】`) this[key as keyof VikaBot] = res.data.id this[name as keyof VikaBot] = res.data.id - const delres = await this.clearBlankLines(res.data.id) - console.log('删除空白行:', delres) + // 删除空白行 + await this.clearBlankLines(res.data.id) return res.data } catch (error) { - console.error(name, error) + log.error(name, error) return error // TODO: handle error } } async createRecord (datasheetId: string, records: ICreateRecordsReqParams) { - log.info('createRecord:', records) + log.info('写入维格表:', records.length) const datasheet = await this.vika.datasheet(datasheetId) try { const res = await datasheet.records.create(records) if (res.success) { - // console.log(res.data.records) + // log.info(res.data.records) } else { - console.error('记录写入维格表失败:', res) + log.error('记录写入维格表失败:', res) } } catch (err) { - console.error('请求维格表写入失败:', err) + log.error('请求维格表写入失败:', err) } } - async addChatRecord (msg: { talker: () => any; to: () => any; type: () => any; text: () => any; room: () => any; id: any }, uploadedAttachments: any, msgType: any, text: string) { - // console.debug(msg) - // console.debug(JSON.stringify(msg)) + async addChatRecord (msg: Message, uploadedAttachments: any, msgType: any, text: string) { + // log.info(msg) + // log.info(JSON.stringify(msg)) const talker = msg.talker() - // console.debug(talker) + // log.info(talker) // const to = msg.to() // const type = msg.type() text = text || msg.text() @@ -187,30 +199,39 @@ class VikaBot { // let msgType = msg.type() const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss') const files = [] - if (uploadedAttachments) { - files.push(uploadedAttachments) - text = JSON.stringify(uploadedAttachments) - } - const record = { - fields: { - timeHms, - name: talker ? talker.name() : '未知', - topic: topic || '--', - messagePayload: text, - wxid: talker.id !== 'null' ? talker.id : '--', - roomid: room && room.id ? room.id : '--', - messageType: msgType, - file: files, - }, + try { + if (uploadedAttachments) { + files.push(uploadedAttachments) + text = JSON.stringify(uploadedAttachments) + } + + const record = { + fields: { + timeHms, + name: talker.name(), + alias: await talker.alias(), + topic: topic || '--', + messagePayload: text, + wxid: talker.id !== 'null' ? talker.id : '--', + roomid: room && room.id ? room.id : '--', + messageType: msgType, + file: files, + messageId:msg.id, + }, + } + // log.info('addChatRecord:', JSON.stringify(record)) + this.msgStore.push(record) + log.info('最新消息池长度:', this.msgStore.length) + } catch (e) { + log.error('添加记录失败:', e) + } - // log.info('addChatRecord:', JSON.stringify(record)) - this.msgStore.push(record) - log.info('最新消息池长度:', this.msgStore.length) + } addRecord (record:any) { - log.info('addRecord:', JSON.stringify(record)) + log.info('消息入列:', JSON.stringify(record)) if (record.fields) { this.msgStore.push(record) log.info('最新消息池长度:', this.msgStore.length) @@ -242,16 +263,16 @@ class VikaBot { }, ] - log.info('addScanRecord:', records) + log.info('登录二维码消息:', records) const datasheet = this.vika.datasheet(this.messageSheet) datasheet.records.create(records).then((response) => { if (response.success) { - console.log('写入vika成功:', response.code) + log.info('写入vika成功:', response.code) } else { - console.error('调用vika写入接口成功,写入vika失败:', response) + log.error('调用vika写入接口成功,写入vika失败:', JSON.stringify(response)) } return response - }).catch(err => { console.error('调用vika写入接口失败:', err) }) + }).catch(err => { log.error('调用vika写入接口失败:', err) }) } async addHeartbeatRecord (text: string) { @@ -272,7 +293,7 @@ class VikaBot { file: files, }, } - log.info('addHeartbeatRecord:', JSON.stringify(record)) + log.info('心跳消息:', JSON.stringify(record)) this.msgStore.push(record) } @@ -282,25 +303,25 @@ class VikaBot { const resp = await datasheet.upload(file) if (resp.success) { const uploadedAttachments = resp.data - console.debug('上传成功', uploadedAttachments) + log.info('文件上传成功', uploadedAttachments) return uploadedAttachments } } catch (error: any) { - console.error(error.message) + log.error(error.message) return error } } async deleteRecords (datasheetId: string, recordsIds: string | any[]) { - // console.debug('操作数据表ID:', datasheetId) - // console.debug('待删除记录IDs:', recordsIds) + // log.info('操作数据表ID:', datasheetId) + // log.info('待删除记录IDs:', recordsIds) const datasheet = this.vika.datasheet(datasheetId) const response = await datasheet.records.delete(recordsIds) if (response.success) { - console.log(`删除${recordsIds.length}条记录`) + log.info(`删除${recordsIds.length}条记录`) } else { - console.error('删除记录失败:', response) + log.error('删除记录失败:', response) } } @@ -311,9 +332,9 @@ class VikaBot { const response = await datasheet.records.query(query) if (response.success) { records = response.data.records - // console.log(records) + // log.info(records) } else { - console.error(response) + log.error('获取数据记录失败:', response) records = response } return records @@ -323,15 +344,15 @@ class VikaBot { let records = [] const datasheet = await this.vika.datasheet(datasheetId) const response: any = await datasheet.records.queryAll() - // console.debug('原始返回:',response) + // log.info('原始返回:',response) if (response.next) { for await (const eachPageRecords of response) { - // console.debug('eachPageRecords:',eachPageRecords.length) + // log.info('eachPageRecords:',eachPageRecords.length) records.push(...eachPageRecords) } - // console.debug('records:',records.length) + // log.info('records:',records.length) } else { - console.error(response) + log.error(response) records = response } return records @@ -339,72 +360,163 @@ class VikaBot { async clearBlankLines (datasheetId: any) { const records = await this.getRecords(datasheetId, {}) - // console.debug(records) + // log.info(records) const recordsIds = [] for (const i in records) { recordsIds.push(records[i].recordId) } - // console.debug(recordsIds) + // log.info(recordsIds) await this.deleteRecords(datasheetId, recordsIds) } - async getConfig () { + async updateConfigToVika (config:any) { + const functionOnStatus = config.functionOnStatus + const botConfig = config.botConfig + log.info('维格表内功能开关状态', functionOnStatus) + log.info('维格表内基础配置信息', botConfig) + } + + async downConfigFromVika () { const configRecords = await this.getRecords(this.configSheet, {}) const switchRecords = await this.getRecords(this.switchSheet, {}) - // console.debug(configRecords) - // console.debug(switchRecords) - - // const sysConfig = { - // VIKA_ONOFF: config['消息上传到维格表'] === '开启', // 维格表开启 - // puppetName: config['puppet'], // 支持wechaty-puppet-wechat、wechaty-puppet-xp、wechaty-puppet-padlocal - // puppetToken: config['wechaty-token'] || '', - // WX_TOKEN: config['对话平台token'], // 微信对话平台token - // EncodingAESKey: config['对话平台EncodingAESKey'], // 微信对话平台EncodingAESKey - // WX_OPENAI_ONOFF: config['智能问答'] === '开启', // 微信对话平台开启 - // roomWhiteListOpen: config['群白名单'] === '开启', // 群白名单功能 - // contactWhiteListOpen: config['好友白名单'] === '开启', // 群白名单功能 - // AT_AHEAD: config['AT回复'] === '开启', // 只有机器人被@时回复 - // DIFF_REPLY_ONOFF: config['不同群个性回复'] === '开启', // 开启不同群个性化回复 - // imOpen: config['IM对话'] === '开启', // 是否开启uve-im客户端,设置为true时,需要先 cd ./vue-im 然后 npm install 启动服务 npm run dev - // mqtt_SUB_ONOFF: config['MQTT控制'] === '开启', - // mqtt_PUB_ONOFF: config['MQTT推送'] === '开启', - // mqttUsername: config['MQTT用户名'] || '', - // mqttPassword: config['MQTT密码'] || '', - // mqttEndpoint: config['MQTT接入地址'] || '', - // mqttPort: config['MQTT端口号'] || 1883, - // } + // log.info(configRecords) + // log.info(switchRecords) const sysConfig: any = {} - sysConfig.roomWhiteList = [] - sysConfig.contactWhiteList = [] + const botConfig: any = {} + const botConfigIdMap: any = {} + const functionOnStatus:any = {} + const functionOnStatusIdMap:any = {} + const roomWhiteList:any = [] + const contactWhiteList:any = [] + const welcomeList:any = [] for (let i = 0; i < configRecords.length; i++) { - if (configRecords[i].fields['标识']) { - sysConfig[configRecords[i].fields['标识']] = configRecords[i].fields['值(只修改此列)'] || '' + const fields = configRecords[i].fields + const recordId = configRecords[i].recordId + + if (!botConfig[fields['配置组标识']]) { + botConfig[fields['配置组标识']] = {} } + + botConfig[fields['配置组标识']][fields['标识']] = fields['值'] + botConfigIdMap[fields['配置组标识']][fields['标识']] = recordId } + this.envsOnVika = botConfigIdMap + for (let i = 0; i < switchRecords.length; i++) { - if (switchRecords[i].fields['标识']) { - sysConfig[switchRecords[i].fields['标识']] = switchRecords[i].fields['启用状态(只修改此列)'] === '开启' + const fields = switchRecords[i].fields + const recordId = switchRecords[i].recordId + + if (!functionOnStatus[fields['配置组标识']]) { + functionOnStatus[fields['配置组标识']] = {} } + functionOnStatus[fields['配置组标识']][fields['标识']] = fields['启用状态'] === '开启' + functionOnStatusIdMap[fields['配置组标识']][fields['标识']] = recordId } + this.switchsOnVika = functionOnStatusIdMap + const roomWhiteListRecords: any[] = await this.getRecords(this.roomWhiteListSheet, {}) for (let i = 0; i < roomWhiteListRecords.length; i++) { if (roomWhiteListRecords[i].fields['群ID']) { - sysConfig.roomWhiteList.push(roomWhiteListRecords[i].fields['群ID']) + roomWhiteList.push(roomWhiteListRecords[i].fields['群ID']) } } const contactWhiteListRecords = await this.getRecords(this.contactWhiteListSheet, {}) for (let i = 0; i < contactWhiteListRecords.length; i++) { if (contactWhiteListRecords[i].fields['好友ID']) { + contactWhiteList.push(contactWhiteListRecords[i].fields['好友ID']) + } + } + log.info('sysConfig:', JSON.stringify(sysConfig, null, '\t')) + log.info('botConfig', JSON.stringify(botConfig, null, '\t')) + log.info('functionOnStatus', JSON.stringify(functionOnStatus, null, '\t')) + + sysConfig.functionOnStatus = functionOnStatus + sysConfig.botConfig = botConfig + sysConfig.contactWhiteList = contactWhiteList + sysConfig.roomWhiteList = roomWhiteList + sysConfig.welcomeList = welcomeList + + return sysConfig + + } + + async getConfig () { + const configRecords = await this.getRecords(this.configSheet, {}) + await wait(1000) + const switchRecords = await this.getRecords(this.switchSheet, {}) + await wait(1000) + // log.info(configRecords) + // log.info(switchRecords) + + const sysConfig: any = { + contactWhiteList:[], + roomWhiteList:[], + } + const botConfig: any = {} + const botConfigIdMap: any = {} + const functionOnStatus:any = {} + const functionOnStatusIdMap:any = {} + sysConfig.roomWhiteList = [] + sysConfig.contactWhiteList = [] + + for (let i = 0; i < configRecords.length; i++) { + const fields = configRecords[i].fields + const recordId = configRecords[i].recordId + + if (fields['标识']) { + botConfig[configRecords[i].fields['标识']] = fields['值'] || '' + botConfigIdMap[configRecords[i].fields['标识']] = recordId + } + } + + this.envsOnVika = botConfigIdMap + + for (let i = 0; i < switchRecords.length; i++) { + const fields = switchRecords[i].fields + const recordId = switchRecords[i].recordId + if (fields['标识']) { + functionOnStatus[switchRecords[i].fields['标识']] = fields['启用状态'] === '开启' + functionOnStatusIdMap[switchRecords[i].fields['标识']] = recordId + } + } + + this.switchsOnVika = functionOnStatusIdMap + + const roomWhiteListRecords: any[] = await this.getRecords(this.roomWhiteListSheet, {}) + await wait(1000) + for (let i = 0; i < roomWhiteListRecords.length; i++) { + if (roomWhiteListRecords[i].fields['群名称']) { + sysConfig.roomWhiteList.push(roomWhiteListRecords[i].fields['群名称']) + } else { + sysConfig.roomWhiteList.push(roomWhiteListRecords[i].fields['群名ID']) + } + } + + const contactWhiteListRecords = await this.getRecords(this.contactWhiteListSheet, {}) + await wait(1000) + for (let i = 0; i < contactWhiteListRecords.length; i++) { + if (contactWhiteListRecords[i].fields['备注']) { + sysConfig.contactWhiteList.push(contactWhiteListRecords[i].fields['备注']) + } else if (contactWhiteListRecords[i].fields['昵称']) { + sysConfig.contactWhiteList.push(contactWhiteListRecords[i].fields['昵称']) + } else { sysConfig.contactWhiteList.push(contactWhiteListRecords[i].fields['好友ID']) } } - sysConfig.welcomeList = [] - log.info('sysConfig:', JSON.stringify(sysConfig)) + sysConfig['welcomeList'] = [] + sysConfig['botConfig'] = botConfig + sysConfig['botConfigIdMap'] = botConfigIdMap + sysConfig['functionOnStatus'] = functionOnStatus + sysConfig['functionOnStatusIdMap'] = functionOnStatusIdMap + + log.info('sysConfig:', JSON.stringify(sysConfig, null, '\t')) + log.info('botConfig', JSON.stringify(botConfig, null, '\t')) + log.info('functionOnStatus', JSON.stringify(functionOnStatus, null, '\t')) return sysConfig @@ -439,7 +551,7 @@ class VikaBot { file = await message.toFileBox() } catch (e) { - console.error('Image解析失败:', e) + log.error('Image解析失败:', e) file = '' } @@ -459,7 +571,7 @@ class VikaBot { text = JSON.stringify(JSON.parse(JSON.stringify(miniProgram)).payload) - // console.debug(miniProgram) + // log.info(miniProgram) /* miniProgram: 小程序卡片数据 { @@ -483,7 +595,7 @@ class VikaBot { file = await message.toFileBox() } catch (e) { - console.error('Audio解析失败:', e) + log.error('Audio解析失败:', e) file = '' } @@ -496,7 +608,7 @@ class VikaBot { file = await message.toFileBox() } catch (e) { - console.error('Video解析失败:', e) + log.error('Video解析失败:', e) file = '' } break @@ -508,7 +620,7 @@ class VikaBot { file = await message.toFileBox() } catch (e) { - console.error('Emoticon解析失败:', e) + log.error('Emoticon解析失败:', e) file = '' } @@ -521,7 +633,7 @@ class VikaBot { file = await message.toFileBox() } catch (e) { - console.error('Attachment解析失败:', e) + log.error('Attachment解析失败:', e) file = '' } @@ -542,20 +654,20 @@ class VikaBot { } if (file) { - filePath = './' + file.name + filePath = 'data/media/image/' + file.name try { const writeStream = fs.createWriteStream(filePath) await file.pipe(writeStream) - await wait(500) + await wait(1000) const readerStream = fs.createReadStream(filePath) uploadedAttachments = await this.upload(readerStream) fs.unlink(filePath, (err) => { - console.debug('上传vika完成删除文件:', filePath, err) + log.info('上传vika完成删除文件:', filePath, err) }) } catch { - console.debug('上传失败:', filePath) + log.info('上传失败:', filePath) fs.unlink(filePath, (err) => { - console.debug('上传vika失败删除文件', filePath, err) + log.info('上传vika失败删除文件', filePath, err) }) } @@ -566,7 +678,7 @@ class VikaBot { } } catch (e) { - console.log('vika 写入失败:', e) + log.info('vika 写入失败:', e) } } @@ -588,7 +700,7 @@ class VikaBot { try { const writeStream = fs.createWriteStream(filePath) await file.pipe(writeStream) - await wait(200) + await wait(1000) const readerStream = fs.createReadStream(filePath) uploadedAttachments = await this.upload(readerStream) const text = qrcodeImageUrl @@ -608,7 +720,7 @@ class VikaBot { } } else { - log.info('StarterBot', 'vika onScan: %s(%s)', ScanStatus[status], status) + log.info('机器人启动,二维码上传维格表', 'onScan: %s(%s)', ScanStatus[status], status) } } @@ -619,7 +731,7 @@ class VikaBot { log.info('当前微信最新联系人数量:', contacts.length) const recordsAll: any = [] const recordExisting = await this.getAllRecords(this.contactSheet) - log.info('云端好友数量:', recordExisting.length) + log.info('云端好友数量:', recordExisting.length || '0') const wxids: string[] = [] if (recordExisting.length) { recordExisting.forEach((record: { fields: any, id: any }) => { @@ -628,18 +740,22 @@ class VikaBot { } for (let i = 0; i < contacts.length; i++) { const item = contacts[i] - if (item && item.friend() && !wxids.includes(item.id)) { + const isFriend = item?.friend() + log.info('好友详情:', item?.name(), JSON.stringify(isFriend)) + if (item && isFriend && item.type() === types.Contact.Individual && !wxids.includes(item.id)) { + // log.info('云端不存在:', item.name()) let avatar = '' try { avatar = String(await item.avatar()) } catch (err) { - + log.error('获取好友头像失败:', err) } const fields = { alias: String(await item.alias() || ''), avatar, friend: item.friend(), gender: String(item.gender() || ''), + updated: new Date().toLocaleString(), id: item.id, name: item.name(), phone: String(await item.phone()), @@ -649,6 +765,8 @@ class VikaBot { fields, } recordsAll.push(record) + } else { + log.info('云端已存在:', item?.name()) } } @@ -657,10 +775,10 @@ class VikaBot { await this.createRecord(this.contactSheet, records) log.info('好友列表同步中...', i + 10) updateCount = updateCount + 10 - void await wait(250) + void await wait(1000) } - log.info('同步好友列表完成,更新好友数量:', updateCount) + log.info('同步好友列表完成,更新好友数量:', updateCount || '0') } catch (err) { log.error('更新好友列表失败:', err) @@ -683,19 +801,24 @@ class VikaBot { }) } for (let i = 0; i < rooms.length; i++) { - const item = rooms[i] + const item:Room|undefined = rooms[i] if (item && !wxids.includes(item.id)) { let avatar:any = 'null' try { - avatar = String(await item.avatar()) + avatar = await item.avatar() + avatar = avatar.name } catch (err) { log.error('获取群头像失败:', err) } + const members = await item.memberAll() + const ownerId = members.length ? members[0]?.name() : '' + log.info('第一个群成员:', ownerId) const fields = { avatar, id: item.id, - ownerId: String(item.owner()?.id || ''), + ownerId: ownerId || '', topic: await item.topic() || '', + updated: new Date().toLocaleString(), } const record = { fields, @@ -709,10 +832,10 @@ class VikaBot { await this.createRecord(this.roomListSheet, records) log.info('群列表同步中...', i + 10) updateCount = updateCount + 10 - void await wait(250) + void await wait(1000) } - log.info('同步群列表完成,更新群数量:', updateCount) + log.info('同步群列表完成,更新群数量:', updateCount || '0') } catch (err) { log.error('更新群列表失败:', err) @@ -722,9 +845,9 @@ class VikaBot { async getTimedTask () { const taskRecords = await this.getRecords(this.noticeSheet, {}) - // console.debug(taskRecords) + log.info('定时提醒任务列表:', JSON.stringify(taskRecords)) - const timedTasks: any = [] + let timedTasks: any = [] const taskFields: Field[] = sheets['noticeSheet']?.fields || [] const taskFieldDic: any = {} @@ -737,39 +860,33 @@ class VikaBot { } } - for (let i = 0; i < taskRecords.length; i++) { - const task = taskRecords[i] - const taskConfig: any = { + interface TaskRecord { + recordId: string; + fields: { + [key: string]: any; + }; + } + + // 优化后的代码 + timedTasks = taskRecords.map((task: TaskRecord) => { + const taskConfig: TaskConfig = { id: task.recordId, msg: task.fields['内容'], time: task.fields['时间'], - cycle: task.fields['周期'], - contacts: [], - rooms: [], + cycle: task.fields['周期'] || '无重复', + targetType: task.fields['通知目标类型'] === '好友' ? 'contact' : 'room', + targetId: task.fields['好友ID/群ID'], + targetName: task.fields['好友备注/昵称或群名称'], active: task.fields['启用状态'] === '开启', } - if (taskConfig.msg && taskConfig.time && (task.fields['接收好友'] || task.fields['接收群'])) { - - if (task.fields['接收群'] && task.fields['接收群'].length) { - const roomRecords = await this.getRecords(this.roomListSheet, { recordIds: task.fields['接收群'] }) - // console.debug(roomRecords) - roomRecords.forEach(async (item: any) => { - taskConfig.rooms.push(item.fields.id) - }) - } - if (task.fields['接收好友'] && task.fields['接收好友'].length) { - const contactRecords = await this.getRecords(this.contactSheet, { recordIds: task.fields['接收好友'] }) - // console.debug(contactRecords) - contactRecords.forEach(async (item: any) => { - taskConfig.contacts.push(item.fields.id) - }) - } - timedTasks.push(taskConfig) + if (taskConfig.active && taskConfig.msg && taskConfig.time && taskConfig.cycle && (taskConfig.targetId || taskConfig.targetName)) { + return taskConfig } - } - // console.debug(2, timedTasks) + return null + }).filter(Boolean) + // log.info(2, timedTasks) return timedTasks @@ -777,19 +894,19 @@ class VikaBot { async checkInit (msg: string) { this.spaceId = await this.getSpaceId() - // console.log('空间ID:', this.spaceId) + // log.info('空间ID:', this.spaceId) let sheetCount = 0 if (this.spaceId) { const tables = await this.getNodesList() - // console.debug(tables) + // log.info(tables) for (const k in sheets) { const sheet = sheets[k as keyof Sheets] - // console.log(k, sheet) + // log.info(k, sheet) if (sheet) { if (!tables[sheet.name]) { sheetCount = sheetCount + 1 - console.error(`缺少【${sheet.name}】表,请运行 npm run sys-init 自动创建系统表,然后再运行 npm start`) + log.error(`缺少【${sheet.name}】表,请运行 npm run sys-init 自动创建系统表,然后再运行 npm start`) } else { this[k as keyof VikaBot] = tables[sheet.name] } @@ -797,13 +914,13 @@ class VikaBot { } if (sheetCount === 0) { - console.log(`================================================\n\n${msg}\n\n================================================\n`) + log.info(`\n================================================\n\n${msg}\n\n================================================\n`) } else { return false } } else { - console.error('指定空间不存在,请先创建空间,并在config.ts中配置VIKA_SPACENAME') + log.error('指定空间不存在,请先创建空间,并在config.json中配置vika信息') return false } @@ -822,16 +939,16 @@ class VikaBot { try { datasheet.records.create(records).then((response) => { if (response.success) { - console.log('写入vika成功:', end, JSON.stringify(response.code)) + log.info('写入vika成功:', end, JSON.stringify(response.code)) } else { - console.error('调用vika写入接口成功,写入vika失败:', response) + log.error('调用vika写入接口成功,写入vika失败:', JSON.stringify(response)) } - }).catch(err => { console.error('调用vika写入接口失败:', err) }) + }).catch(err => { log.error('调用vika写入接口失败:', err) }) } catch (err) { - console.error('调用datasheet.records.create失败:', err) + log.error('调用datasheet.records.create失败:', err) } } - }, 250) + }, 1000) return true } @@ -839,19 +956,19 @@ class VikaBot { async init () { this.spaceId = await this.getSpaceId() - // console.log('空间ID:', this.spaceId) + // log.info('空间ID:', this.spaceId) if (this.spaceId) { const tables = await this.getNodesList() - // console.log(tables) + log.info('维格表文件列表:', JSON.stringify(tables)) await wait(1000) for (const k in sheets) { - // console.debug(this) + // log.info(this) const sheet = sheets[k as keyof Sheets] - // console.log(k, sheet) + // log.info(k, sheet) if (sheet && !tables[sheet.name]) { const fields = sheet.fields const newFields: Field[] = [] @@ -862,7 +979,7 @@ class VikaBot { name: field?.name || '', desc: field?.desc || '', } - console.debug(field) + log.info('字段定义:', field) let options switch (field?.type) { case 'SingleText': @@ -919,36 +1036,36 @@ class VikaBot { // } } - // console.debug(newFields) + // log.info(newFields) await this.createDataSheet(k, sheet.name, newFields) - await wait(200) + await wait(1000) const defaultRecords = sheet.defaultRecords // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (defaultRecords) { - // console.debug(defaultRecords.length) + // log.info(defaultRecords.length) const count = Math.ceil(defaultRecords.length / 10) for (let i = 0; i < count; i++) { const records = defaultRecords.splice(0, 10) - console.log('写入:', records.length) + log.info('写入:', records.length) await this.createRecord(this[k as keyof VikaBot], records) - await wait(200) + await wait(1000) } - console.log(sheet.name + '初始化数据写入完成...') + log.info(sheet.name, '初始化数据写入完成...') } - console.log(sheet.name + '数据表配置完成...') + log.info(sheet.name, '数据表配置完成...') } else if (sheet) { this[k as keyof VikaBot] = tables[sheet.name] this[sheet.name as keyof VikaBot] = tables[sheet.name] } else { /* empty */ } } - console.log('================================================\n\n初始化系统表完成,运行 npm start 启动系统\n\n================================================\n') + log.info('\n================================================\n\n初始化系统表完成,运行 npm start 启动系统\n\n================================================\n') // const tasks = await this.getTimedTask() return true } else { - console.error('指定空间不存在,请先创建空间,并在config.ts中配置VIKA_SPACENAME') + log.error('指定空间不存在,请先创建空间,并在.env文件或环境变量中配置vika信息') return false } } diff --git a/src/plugins/vue-im/.babelrc b/src/plugins/vue-im/.babelrc deleted file mode 100644 index 3a280ba3..00000000 --- a/src/plugins/vue-im/.babelrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "presets": [ - ["env", { - "modules": false, - "targets": { - "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] - } - }], - "stage-2" - ], - "plugins": ["transform-vue-jsx", "transform-runtime"] -} diff --git a/src/plugins/vue-im/.editorconfig b/src/plugins/vue-im/.editorconfig deleted file mode 100644 index e291365a..00000000 --- a/src/plugins/vue-im/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 4 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true diff --git a/src/plugins/vue-im/.gitignore b/src/plugins/vue-im/.gitignore deleted file mode 100644 index b1a5fa6d..00000000 --- a/src/plugins/vue-im/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -.DS_Store -node_modules/ -/dist/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* -/test/e2e/reports/ -selenium-debug.log -/static/upload/* - -# Editor directories and files -.idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln diff --git a/src/plugins/vue-im/.postcssrc.js b/src/plugins/vue-im/.postcssrc.js deleted file mode 100644 index eee3e92d..00000000 --- a/src/plugins/vue-im/.postcssrc.js +++ /dev/null @@ -1,10 +0,0 @@ -// https://github.com/michael-ciniawsky/postcss-load-config - -module.exports = { - "plugins": { - "postcss-import": {}, - "postcss-url": {}, - // to edit target browsers: use "browserslist" field in package.json - "autoprefixer": {} - } -} diff --git a/src/plugins/vue-im/LICENSE b/src/plugins/vue-im/LICENSE deleted file mode 100644 index 59e9324f..00000000 --- a/src/plugins/vue-im/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 polk6 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/plugins/vue-im/README.md b/src/plugins/vue-im/README.md deleted file mode 100644 index 93109790..00000000 --- a/src/plugins/vue-im/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# wechat客服系统 - -基于另一个开源项目[vue-im](https://github.com/polk6/vue-im)二次开发的客服系统,对接微信chatbot实现群消息转换为单聊模式,并支持快捷答复。 - -感谢作者[@fang mu](https://github.com/polk6) - -# Features -* 支持1客服对多用户 -* 当前仅支持文本消息 - -## im-server im服务端 - - - -## Usage -``` -npm install -npm run dev -``` - -启动后使用谷歌浏览器访问http://localhost:8080/#/imServer - -## Express-server - -./build/webpack.dev.conf.js 内置了一个Express服务,后台接口都在此处 diff --git a/src/plugins/vue-im/build/build.js b/src/plugins/vue-im/build/build.js deleted file mode 100644 index 8f2ad8ad..00000000 --- a/src/plugins/vue-im/build/build.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict' -require('./check-versions')() - -process.env.NODE_ENV = 'production' - -const ora = require('ora') -const rm = require('rimraf') -const path = require('path') -const chalk = require('chalk') -const webpack = require('webpack') -const config = require('../config') -const webpackConfig = require('./webpack.prod.conf') - -const spinner = ora('building for production...') -spinner.start() - -rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { - if (err) throw err - webpack(webpackConfig, (err, stats) => { - spinner.stop() - if (err) throw err - process.stdout.write(stats.toString({ - colors: true, - modules: false, - children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. - chunks: false, - chunkModules: false - }) + '\n\n') - - if (stats.hasErrors()) { - console.log(chalk.red(' Build failed with errors.\n')) - process.exit(1) - } - - console.log(chalk.cyan(' Build complete.\n')) - console.log(chalk.yellow( - ' Tip: built files are meant to be served over an HTTP server.\n' + - ' Opening index.html over file:// won\'t work.\n' - )) - }) -}) diff --git a/src/plugins/vue-im/build/check-versions.js b/src/plugins/vue-im/build/check-versions.js deleted file mode 100644 index 3ef972a0..00000000 --- a/src/plugins/vue-im/build/check-versions.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict' -const chalk = require('chalk') -const semver = require('semver') -const packageConfig = require('../package.json') -const shell = require('shelljs') - -function exec (cmd) { - return require('child_process').execSync(cmd).toString().trim() -} - -const versionRequirements = [ - { - name: 'node', - currentVersion: semver.clean(process.version), - versionRequirement: packageConfig.engines.node - } -] - -if (shell.which('npm')) { - versionRequirements.push({ - name: 'npm', - currentVersion: exec('npm --version'), - versionRequirement: packageConfig.engines.npm - }) -} - -module.exports = function () { - const warnings = [] - - for (let i = 0; i < versionRequirements.length; i++) { - const mod = versionRequirements[i] - - if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { - warnings.push(mod.name + ': ' + - chalk.red(mod.currentVersion) + ' should be ' + - chalk.green(mod.versionRequirement) - ) - } - } - - if (warnings.length) { - console.log('') - console.log(chalk.yellow('To use this template, you must update following to modules:')) - console.log() - - for (let i = 0; i < warnings.length; i++) { - const warning = warnings[i] - console.log(' ' + warning) - } - - console.log() - process.exit(1) - } -} diff --git a/src/plugins/vue-im/build/utils.js b/src/plugins/vue-im/build/utils.js deleted file mode 100644 index e534fb0f..00000000 --- a/src/plugins/vue-im/build/utils.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict' -const path = require('path') -const config = require('../config') -const ExtractTextPlugin = require('extract-text-webpack-plugin') -const packageConfig = require('../package.json') - -exports.assetsPath = function (_path) { - const assetsSubDirectory = process.env.NODE_ENV === 'production' - ? config.build.assetsSubDirectory - : config.dev.assetsSubDirectory - - return path.posix.join(assetsSubDirectory, _path) -} - -exports.cssLoaders = function (options) { - options = options || {} - - const cssLoader = { - loader: 'css-loader', - options: { - sourceMap: options.sourceMap - } - } - - const postcssLoader = { - loader: 'postcss-loader', - options: { - sourceMap: options.sourceMap - } - } - - // generate loader string to be used with extract text plugin - function generateLoaders (loader, loaderOptions) { - const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] - - if (loader) { - loaders.push({ - loader: loader + '-loader', - options: Object.assign({}, loaderOptions, { - sourceMap: options.sourceMap - }) - }) - } - - // Extract CSS when that option is specified - // (which is the case during production build) - if (options.extract) { - return ExtractTextPlugin.extract({ - use: loaders, - fallback: 'vue-style-loader' - }) - } else { - return ['vue-style-loader'].concat(loaders) - } - } - - // https://vue-loader.vuejs.org/en/configurations/extract-css.html - return { - css: generateLoaders(), - postcss: generateLoaders(), - less: generateLoaders('less'), - sass: generateLoaders('sass', { indentedSyntax: true }), - scss: generateLoaders('sass'), - stylus: generateLoaders('stylus'), - styl: generateLoaders('stylus') - } -} - -// Generate loaders for standalone style files (outside of .vue) -exports.styleLoaders = function (options) { - const output = [] - const loaders = exports.cssLoaders(options) - - for (const extension in loaders) { - const loader = loaders[extension] - output.push({ - test: new RegExp('\\.' + extension + '$'), - use: loader - }) - } - - return output -} - -exports.createNotifierCallback = () => { - const notifier = require('node-notifier') - - return (severity, errors) => { - if (severity !== 'error') return - - const error = errors[0] - const filename = error.file && error.file.split('!').pop() - - notifier.notify({ - title: packageConfig.name, - message: severity + ': ' + error.name, - subtitle: filename || '', - icon: path.join(__dirname, 'logo.png') - }) - } -} diff --git a/src/plugins/vue-im/build/vue-loader.conf.js b/src/plugins/vue-im/build/vue-loader.conf.js deleted file mode 100644 index 33ed58bc..00000000 --- a/src/plugins/vue-im/build/vue-loader.conf.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' -const utils = require('./utils') -const config = require('../config') -const isProduction = process.env.NODE_ENV === 'production' -const sourceMapEnabled = isProduction - ? config.build.productionSourceMap - : config.dev.cssSourceMap - -module.exports = { - loaders: utils.cssLoaders({ - sourceMap: sourceMapEnabled, - extract: isProduction - }), - cssSourceMap: sourceMapEnabled, - cacheBusting: config.dev.cacheBusting, - transformToRequire: { - video: ['src', 'poster'], - source: 'src', - img: 'src', - image: 'xlink:href' - } -} diff --git a/src/plugins/vue-im/build/webpack.base.conf.js b/src/plugins/vue-im/build/webpack.base.conf.js deleted file mode 100644 index 6163223e..00000000 --- a/src/plugins/vue-im/build/webpack.base.conf.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict' -const path = require('path') -const utils = require('./utils') -const config = require('../config') -const vueLoaderConfig = require('./vue-loader.conf') - -function resolve (dir) { - return path.join(__dirname, '..', dir) -} - -const createLintingRule = () => ({ - test: /\.(js|vue)$/, - loader: 'eslint-loader', - enforce: 'pre', - include: [resolve('src'), resolve('test')], - options: { - formatter: require('eslint-friendly-formatter'), - emitWarning: !config.dev.showEslintErrorsInOverlay - } -}) - -module.exports = { - context: path.resolve(__dirname, '../'), - entry: { app: [ 'babel-polyfill', './src/main.js' ] }, - output: { - path: config.build.assetsRoot, - filename: '[name].js', - publicPath: process.env.NODE_ENV === 'production' - ? config.build.assetsPublicPath - : config.dev.assetsPublicPath - }, - resolve: { - extensions: ['.js', '.vue', '.json'], - alias: { - 'vue$': 'vue/dist/vue.esm.js', - '@': resolve('src'), - '@@': resolve('static'), - } - }, - module: { - rules: [ - ...(config.dev.useEslint ? [createLintingRule()] : []), - { - test: /\.vue$/, - loader: 'vue-loader', - options: vueLoaderConfig - }, - { - test: /\.js$/, - loader: 'babel-loader', - include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] - }, - { - test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, - loader: 'url-loader', - options: { - limit: 10000, - name: utils.assetsPath('img/[name].[hash:7].[ext]') - } - }, - { - test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, - loader: 'url-loader', - options: { - limit: 10000, - name: utils.assetsPath('media/[name].[hash:7].[ext]') - } - }, - { - test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, - loader: 'url-loader', - options: { - limit: 10000, - name: utils.assetsPath('fonts/[name].[hash:7].[ext]') - } - } - ] - }, - node: { - // prevent webpack from injecting useless setImmediate polyfill because Vue - // source contains it (although only uses it if it's native). - setImmediate: false, - // prevent webpack from injecting mocks to Node native modules - // that does not make sense for the client - dgram: 'empty', - fs: 'empty', - net: 'empty', - tls: 'empty', - child_process: 'empty' - } -} diff --git a/src/plugins/vue-im/build/webpack.dev.conf.js b/src/plugins/vue-im/build/webpack.dev.conf.js deleted file mode 100644 index 7223c6c5..00000000 --- a/src/plugins/vue-im/build/webpack.dev.conf.js +++ /dev/null @@ -1,224 +0,0 @@ -'use strict'; -const utils = require('./utils'); -const webpack = require('webpack'); -const config = require('../config'); -const merge = require('webpack-merge'); -const path = require('path'); -const baseWebpackConfig = require('./webpack.base.conf'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); -const portfinder = require('portfinder'); - -const HOST = process.env.HOST; -const PORT = process.env.PORT && Number(process.env.PORT); - -const devWebpackConfig = merge(baseWebpackConfig, { - module: { - rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) - }, - // cheap-module-eval-source-map is faster for development - devtool: config.dev.devtool, - - // these devServer options should be customized in /config/index.js - devServer: { - clientLogLevel: 'warning', - historyApiFallback: { - rewrites: [{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }] - }, - hot: true, - contentBase: false, // since we use CopyWebpackPlugin. - compress: true, - host: HOST || config.dev.host, - port: PORT || config.dev.port, - open: config.dev.autoOpenBrowser, - overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, - publicPath: config.dev.assetsPublicPath, - proxy: config.dev.proxyTable, - quiet: true, // necessary for FriendlyErrorsPlugin - watchOptions: { - poll: config.dev.poll - } - }, - plugins: [ - new webpack.DefinePlugin({ - 'process.env': require('../config/dev.env') - }), - new webpack.HotModuleReplacementPlugin(), - new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. - new webpack.NoEmitOnErrorsPlugin(), - // https://github.com/ampedandwired/html-webpack-plugin - new HtmlWebpackPlugin({ - filename: 'index.html', - template: 'index.html', - inject: true - }), - // copy custom static assets - new CopyWebpackPlugin([ - { - from: path.resolve(__dirname, '../static'), - to: config.dev.assetsSubDirectory, - ignore: ['.*'] - } - ]) - ] -}); - -module.exports = new Promise((resolve, reject) => { - portfinder.basePort = process.env.PORT || config.dev.port; - portfinder.getPort((err, port) => { - if (err) { - reject(err); - } else { - // publish the new Port, necessary for e2e tests - process.env.PORT = port; - // add port to devServer config - devWebpackConfig.devServer.port = port; - - // Add FriendlyErrorsPlugin - devWebpackConfig.plugins.push( - new FriendlyErrorsPlugin({ - compilationSuccessInfo: { - messages: [ - ` -Your application is running here: - im-server: http://localhost:${port}/#/imServer - im-client: http://localhost:${port}/#/imclient - ` - ] - }, - onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined - }) - ); - - resolve(devWebpackConfig); - } - }); -}); - -// express -const app = require('express')(); -const fileUpload = require('express-fileupload'); -app.use(fileUpload()); // for parsing multipart/form-data -app.use(function(req, res, next) { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Headers', 'X-Requested-With'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Pragma'); - res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS'); - if (req.method === 'OPTIONS') { - res.sendStatus(204); - } else { - next(); - } -}); -// 上传文件 -app.post('/upload', function(req, res) { - if (!req.files) { - return res.status(400).send('No files were uploaded.'); - } - // save file - // - let file = req.files.uploadFile; - let encodeFileName = Number.parseInt(Date.now() + Math.random()) + file.name; - file.mv(path.resolve(__dirname, '../static/upload/') + '/' + encodeFileName, function(err) { - if (err) { - return res.status(500).send({ - code: err.code, - data: err, - message: '文件上传失败' - }); - } - res.send({ - code: 0, - data: { - fileName: file.name, - fileUrl: `http://${devWebpackConfig.devServer.host}:3000/static/upload/${encodeFileName}` - }, - message: '文件上传成功' - }); - }); -}); - -// 获取文件 -app.get('/static/upload/:fileName', function(req, res) { - res.sendFile(path.resolve(__dirname, '../static/upload') + '/' + req.params.fileName); -}); -// 获取im客服列表 -app.get('/getIMServerList', function(req, res) { - res.json({ - code: 0, - data: Array.from(serverChatDic.values()).map((item) => { - return item.serverChatEn; - }) // 只需要serverChatDic.values内的serverChatEn - }); -}); -app.listen(3000); - -// socket -var server = require('http').createServer(); -var io = require('socket.io')(server); -var serverChatDic = new Map(); // 服务端 -var clientChatDic = new Map(); // 客户端 -io.on('connection', function(socket) { - // 服务端上线 - socket.on('SERVER_ON', function(data) { - let serverChatEn = data.serverChatEn; - console.log(`有新的服务端socket连接了,服务端Id:${serverChatEn.serverChatId}`); - serverChatDic.set(serverChatEn.serverChatId, { - serverChatEn: serverChatEn, - socket: socket - }); - }); - - // 服务端下线 - socket.on('SERVER_OFF', function(data) { - let serverChatEn = data.serverChatEn; - serverChatDic.delete(serverChatEn.serverChatId); - }); - - // 服务端发送了信息 - socket.on('SERVER_SEND_MSG', function(data) { - if (clientChatDic.has(data.clientChatId)) { - clientChatDic.get(data.clientChatId).socket.emit('SERVER_SEND_MSG', { msg: data.msg }); - } - }); - - // 客户端事件;'CLIENT_ON'(上线), 'CLIENT_OFF'(离线), 'CLIENT_SEND_MSG'(发送消息) - ['CLIENT_ON', 'CLIENT_OFF', 'CLIENT_SEND_MSG'].forEach((eventName) => { - socket.on(eventName, (data) => { - let clientChatEn = data.clientChatEn; - let serverChatId = data.serverChatId; - // 1.通知服务端 - if (serverChatDic.has(serverChatId)) { - serverChatDic.get(serverChatId).socket.emit(eventName, { - clientChatEn: clientChatEn, - msg: data.msg - }); - } else { - socket.emit('SERVER_SEND_MSG', { - msg: { - content: '未找到客服' - } - }); - } - - // 2.对不同的事件特殊处理 - if (eventName === 'CLIENT_ON') { - // 1)'CLIENT_ON',通知客户端正确连接 - console.log(`有新的客户端socket连接了,客户端Id:${clientChatEn.clientChatId}`); - clientChatDic.set(clientChatEn.clientChatId, { - clientChatEn: clientChatEn, - socket: socket - }); - serverChatDic.has(serverChatId) && - socket.emit('SERVER_CONNECTED', { - serverChatEn: serverChatDic.get(serverChatId).serverChatEn - }); - } else if (eventName === 'CLIENT_OFF') { - // 2)'CLIENT_OFF',删除连接 - clientChatDic.delete(clientChatEn.clientChatId); - } - }); - }); -}); -server.listen(3001); diff --git a/src/plugins/vue-im/build/webpack.prod.conf.js b/src/plugins/vue-im/build/webpack.prod.conf.js deleted file mode 100644 index d9f99f65..00000000 --- a/src/plugins/vue-im/build/webpack.prod.conf.js +++ /dev/null @@ -1,145 +0,0 @@ -'use strict' -const path = require('path') -const utils = require('./utils') -const webpack = require('webpack') -const config = require('../config') -const merge = require('webpack-merge') -const baseWebpackConfig = require('./webpack.base.conf') -const CopyWebpackPlugin = require('copy-webpack-plugin') -const HtmlWebpackPlugin = require('html-webpack-plugin') -const ExtractTextPlugin = require('extract-text-webpack-plugin') -const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') -const UglifyJsPlugin = require('uglifyjs-webpack-plugin') - -const env = require('../config/prod.env') - -const webpackConfig = merge(baseWebpackConfig, { - module: { - rules: utils.styleLoaders({ - sourceMap: config.build.productionSourceMap, - extract: true, - usePostCSS: true - }) - }, - devtool: config.build.productionSourceMap ? config.build.devtool : false, - output: { - path: config.build.assetsRoot, - filename: utils.assetsPath('js/[name].[chunkhash].js'), - chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') - }, - plugins: [ - // http://vuejs.github.io/vue-loader/en/workflow/production.html - new webpack.DefinePlugin({ - 'process.env': env - }), - new UglifyJsPlugin({ - uglifyOptions: { - compress: { - warnings: false - } - }, - sourceMap: config.build.productionSourceMap, - parallel: true - }), - // extract css into its own file - new ExtractTextPlugin({ - filename: utils.assetsPath('css/[name].[contenthash].css'), - // Setting the following option to `false` will not extract CSS from codesplit chunks. - // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. - // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, - // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 - allChunks: true, - }), - // Compress extracted CSS. We are using this plugin so that possible - // duplicated CSS from different components can be deduped. - new OptimizeCSSPlugin({ - cssProcessorOptions: config.build.productionSourceMap - ? { safe: true, map: { inline: false } } - : { safe: true } - }), - // generate dist index.html with correct asset hash for caching. - // you can customize output by editing /index.html - // see https://github.com/ampedandwired/html-webpack-plugin - new HtmlWebpackPlugin({ - filename: config.build.index, - template: 'index.html', - inject: true, - minify: { - removeComments: true, - collapseWhitespace: true, - removeAttributeQuotes: true - // more options: - // https://github.com/kangax/html-minifier#options-quick-reference - }, - // necessary to consistently work with multiple chunks via CommonsChunkPlugin - chunksSortMode: 'dependency' - }), - // keep module.id stable when vendor modules does not change - new webpack.HashedModuleIdsPlugin(), - // enable scope hoisting - new webpack.optimize.ModuleConcatenationPlugin(), - // split vendor js into its own file - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - minChunks (module) { - // any required modules inside node_modules are extracted to vendor - return ( - module.resource && - /\.js$/.test(module.resource) && - module.resource.indexOf( - path.join(__dirname, '../node_modules') - ) === 0 - ) - } - }), - // extract webpack runtime and module manifest to its own file in order to - // prevent vendor hash from being updated whenever app bundle is updated - new webpack.optimize.CommonsChunkPlugin({ - name: 'manifest', - minChunks: Infinity - }), - // This instance extracts shared chunks from code splitted chunks and bundles them - // in a separate chunk, similar to the vendor chunk - // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk - new webpack.optimize.CommonsChunkPlugin({ - name: 'app', - async: 'vendor-async', - children: true, - minChunks: 3 - }), - - // copy custom static assets - new CopyWebpackPlugin([ - { - from: path.resolve(__dirname, '../static'), - to: config.build.assetsSubDirectory, - ignore: ['.*'] - } - ]) - ] -}) - -if (config.build.productionGzip) { - const CompressionWebpackPlugin = require('compression-webpack-plugin') - - webpackConfig.plugins.push( - new CompressionWebpackPlugin({ - asset: '[path].gz[query]', - algorithm: 'gzip', - test: new RegExp( - '\\.(' + - config.build.productionGzipExtensions.join('|') + - ')$' - ), - threshold: 10240, - minRatio: 0.8 - }) - ) -} - -if (config.build.bundleAnalyzerReport) { - const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin - webpackConfig.plugins.push(new BundleAnalyzerPlugin()) -} - -module.exports = webpackConfig diff --git a/src/plugins/vue-im/config/dev.env.js b/src/plugins/vue-im/config/dev.env.js deleted file mode 100644 index 1e22973a..00000000 --- a/src/plugins/vue-im/config/dev.env.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' -const merge = require('webpack-merge') -const prodEnv = require('./prod.env') - -module.exports = merge(prodEnv, { - NODE_ENV: '"development"' -}) diff --git a/src/plugins/vue-im/config/index.js b/src/plugins/vue-im/config/index.js deleted file mode 100644 index 64629072..00000000 --- a/src/plugins/vue-im/config/index.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict' -// Template version: 1.3.1 -// see http://vuejs-templates.github.io/webpack for documentation. - -const path = require('path') - -module.exports = { - dev: { - - // Paths - assetsSubDirectory: 'static', - assetsPublicPath: '/', - proxyTable: {}, - - // Various Dev Server settings - host: '', // can be overwritten by process.env.HOST - port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined - autoOpenBrowser: false, - errorOverlay: true, - notifyOnErrors: true, - poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- - - // Use Eslint Loader? - // If true, your code will be linted during bundling and - // linting errors and warnings will be shown in the console. - useEslint: false, - // If true, eslint errors and warnings will also be shown in the error overlay - // in the browser. - showEslintErrorsInOverlay: false, - - /** - * Source Maps - */ - - // https://webpack.js.org/configuration/devtool/#development - devtool: 'cheap-module-eval-source-map', - - // If you have problems debugging vue-files in devtools, - // set this to false - it *may* help - // https://vue-loader.vuejs.org/en/options.html#cachebusting - cacheBusting: true, - - cssSourceMap: true - }, - - build: { - // Template for index.html - index: path.resolve(__dirname, '../dist/index.html'), - - // Paths - assetsRoot: path.resolve(__dirname, '../dist'), - assetsSubDirectory: 'static', - assetsPublicPath: '/', - - /** - * Source Maps - */ - - productionSourceMap: true, - // https://webpack.js.org/configuration/devtool/#production - devtool: '#source-map', - - // Gzip off by default as many popular static hosts such as - // Surge or Netlify already gzip all static assets for you. - // Before setting to `true`, make sure to: - // npm install --save-dev compression-webpack-plugin - productionGzip: false, - productionGzipExtensions: ['js', 'css'], - - // Run the build command with an extra argument to - // View the bundle analyzer report after build finishes: - // `npm run build --report` - // Set to `true` or `false` to always turn it on or off - bundleAnalyzerReport: process.env.npm_config_report - } -} diff --git a/src/plugins/vue-im/config/prod.env.js b/src/plugins/vue-im/config/prod.env.js deleted file mode 100644 index a6f99761..00000000 --- a/src/plugins/vue-im/config/prod.env.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict' -module.exports = { - NODE_ENV: '"production"' -} diff --git a/src/plugins/vue-im/config/test.env.js b/src/plugins/vue-im/config/test.env.js deleted file mode 100644 index c2824a30..00000000 --- a/src/plugins/vue-im/config/test.env.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' -const merge = require('webpack-merge') -const devEnv = require('./dev.env') - -module.exports = merge(devEnv, { - NODE_ENV: '"testing"' -}) diff --git a/src/plugins/vue-im/index.html b/src/plugins/vue-im/index.html deleted file mode 100644 index 3513d427..00000000 --- a/src/plugins/vue-im/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - wechat客服系统 - - - -
- - - diff --git a/src/plugins/vue-im/package.json b/src/plugins/vue-im/package.json deleted file mode 100644 index 5fc520ce..00000000 --- a/src/plugins/vue-im/package.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "简单客服系统", - "version": "1.0.0", - "description": "A Vue.js project", - "author": "polk6", - "private": true, - "scripts": { - "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", - "start": "npm run dev", - "build": "node build/build.js" - }, - "dependencies": { - "karma-chai": "^0.1.0", - "net": "^1.0.2", - "vue": "^2.5.2", - "vue-router": "^3.0.1" - }, - "devDependencies": { - "autoprefixer": "^7.2.6", - "axios": "^0.18.1", - "babel-core": "^6.22.1", - "babel-helper-vue-jsx-merge-props": "^2.0.3", - "babel-loader": "^7.1.4", - "babel-plugin-syntax-jsx": "^6.18.0", - "babel-plugin-transform-runtime": "^6.22.0", - "babel-plugin-transform-vue-jsx": "^3.7.0", - "babel-polyfill": "^6.26.0", - "babel-preset-env": "^1.3.2", - "babel-preset-stage-2": "^6.22.0", - "chalk": "^2.3.2", - "copy-webpack-plugin": "^4.5.0", - "css-loader": "^0.28.10", - "element-ui": "^2.2.1", - "express": "^4.16.2", - "express-fileupload": "^1.1.9", - "extract-text-webpack-plugin": "^3.0.0", - "file-loader": "^1.1.11", - "font-awesome": "^4.7.0", - "friendly-errors-webpack-plugin": "^1.6.1", - "html-webpack-plugin": "^2.30.1", - "less": "^2.7.3", - "less-loader": "^4.0.6", - "node-notifier": ">=8.0.1", - "optimize-css-assets-webpack-plugin": "^3.2.0", - "ora": "^1.2.0", - "portfinder": "^1.0.13", - "postcss-import": "^11.1.0", - "postcss-loader": "^2.1.1", - "postcss-url": "^7.3.1", - "rimraf": "^2.6.0", - "semver": "^5.3.0", - "shelljs": ">=0.8.5", - "socket.io": "^2.1.0", - "socket.io-client": "^2.4.0", - "uglifyjs-webpack-plugin": "^1.2.2", - "url-loader": "^0.5.8", - "vue-loader": "^13.3.0", - "vue-style-loader": "^3.0.1", - "vue-template-compiler": "^2.5.2", - "vuex": "^3.0.1", - "webpack": "^3.11.0", - "webpack-bundle-analyzer": "^3.3.2", - "webpack-dev-server": "^2.11.2", - "webpack-merge": "^4.1.2" - }, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - }, - "browserslist": [ - "> 1%", - "last 2 versions", - "not ie <= 8" - ] -} diff --git a/src/plugins/vue-im/src/App.vue b/src/plugins/vue-im/src/App.vue deleted file mode 100644 index 3202dd36..00000000 --- a/src/plugins/vue-im/src/App.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - - - diff --git a/src/plugins/vue-im/src/assets/qqEmoji.png b/src/plugins/vue-im/src/assets/qqEmoji.png deleted file mode 100644 index 0f15d17d..00000000 Binary files a/src/plugins/vue-im/src/assets/qqEmoji.png and /dev/null differ diff --git a/src/plugins/vue-im/src/common/ak.js b/src/plugins/vue-im/src/common/ak.js deleted file mode 100644 index e1891b90..00000000 --- a/src/plugins/vue-im/src/common/ak.js +++ /dev/null @@ -1,795 +0,0 @@ -/** - * 工具模块,不依赖第三方代码 - */ -var ak = ak || {}; - -ak.Base_URL = location.host; - -/** - * 工具模块,不依赖第三方代码 - * 包含:类型判断 - */ -ak.Utils = { - /** - * 是否为JSON字符串 - * @param {String} - * @return {Boolean} - */ - - isJSON(str) { - if (typeof str == 'string') { - try { - var obj = JSON.parse(str); - if (str.indexOf('{') > -1) { - return true; - } else { - return false; - } - } catch (e) { - return false; - } - } - return false; - }, - /** - * 去除字符串首尾两端空格 - * @param {String} str - * @return {String} - */ - trim(str) { - if (str) { - return str.replace(/(^\s*)|(\s*$)/g, ''); - } else { - return ''; - } - }, - /** - * 脱敏 - * @param {String} value 脱敏的对象 - * @return {String} - */ - desensitization: function(value) { - if (value) { - var valueNew = ''; - const length = value.length; - valueNew = value - .split('') - .map((number, index) => { - // 脱敏:从倒数第五位开始向前四位脱敏 - const indexMin = length - 8; - const indexMax = length - 5; - - if (index >= indexMin && index <= indexMax) { - return '*'; - } else { - return number; - } - }) - .join(''); - return valueNew; - } else { - return ''; - } - }, - - /** - * 判断是否Array对象 - * @param {Object} value 判断的对象 - * @return {Boolean} - */ - isArray: function(value) { - return toString.call(value) === '[object Array]'; - }, - - /** - * 判断是否日期对象 - * @param {Object} value 判断的对象 - * @return {Boolean} - */ - isDate: function(value) { - return toString.call(value) === '[object Date]'; - }, - - /** - * 判断是否Object对象 - * @param {Object} value 判断的对象 - * @return {Boolean} - */ - isObject: function(value) { - return toString.call(value) === '[object Object]'; - }, - - /** - * 判断是否为空 - * @param {Object} value 判断的对象 - * @return {Boolean} - */ - isEmpty: function(value) { - return value === null || value === undefined || value === '' || (this.isArray(value) && value.length === 0); - }, - - /** - * 判断是否移动电话 - * @param {Number} value 判断的值 - * @return {Boolean} - */ - isMobilePhone: function(value) { - value = Number.parseInt(value); - // 1)是否非数字 - if (Number.isNaN(value)) { - return false; - } - - // 2)时候移动电话 - return /^1[3|4|5|7|8|9|6][0-9]\d{4,8}$/.test(value); - }, - - /** - * 判断是否为邮箱 - * @param {String} value 判断的值 - * @return {Boolean} - */ - isEmail: function(value) { - return /^[a-zA-Z\-_0-9]+@[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+$/.test(value); - }, - - /** - * 转换服务器请求的对象为Js的对象:包含首字母转换为小写;属性格式转换为Js支持的格式 - * @param {Object} en 服务器的获取的数据对象 - */ - transWebServerObj: function(en) { - if (toString.call(en) == '[object Array]') { - for (var i = 0, len = en.length; i < len; i++) { - ak.Utils.transWebServerObj(en[i]); - } - } else { - for (propertyName in en) { - /* - // 1.创建一个小写的首字母属性并赋值:ABC => aBC - var newPropertyName = propertyName.charAt(0).toLowerCase() + propertyName.substr(1); - en[newPropertyName] = en[propertyName]; - */ - var tmpName = propertyName; - // 2.判断此属性是否为数组,若是就执行递归 - if (toString.call(en[tmpName]) == '[object Array]') { - for (var i = 0, len = en[tmpName].length; i < len; i++) { - ak.Utils.transWebServerObj(en[tmpName][i]); // 数组里的每个对象再依次进行转换 - } - } else if (toString.call(en[tmpName]) == '[object Object]') { - ak.Utils.transWebServerObj(en[tmpName]); // 若属性的值是一个对象,也要进行转换 - } else { - // 3.若不是其他类型,把此属性的值转换为Js的数据格式 - // 3.1)日期格式:后台为2015-12-08T09:23:23.917 => 2015-12-08 09:23:23 - if (new RegExp(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/).test(en[propertyName])) { - // en[propertyName] = new RegExp(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/).exec(en[propertyName])[0].replace('T', ' '); - - // 若为0001年,表示时间为空,就返回''空字符串 - if (en[propertyName].indexOf('0001') >= 0) { - en[propertyName] = ''; - } - } else if (toString.call(en[propertyName]) == '[object Number]' && new RegExp(/\d+[.]\d{3}/).test(en[propertyName])) { - // 3.2)溢出的float格式:1.33333 = > 1.33 - en[propertyName] = en[propertyName].toFixed(2); - } else if (en[propertyName] == null) { - // 3.3)null值返回空 - en[propertyName] = ''; - } else if ( - ['imgPath', 'loopImgPath', 'clubIcon', 'headImgPath'].indexOf(propertyName) >= 0 && - en[propertyName] && - en[propertyName].length > 0 - ) { - en[propertyName] = ak.Base_URL + en[propertyName].replace('..', ''); - } - } - } - } - return en; - }, - - /** - *设置SessionStorage的值 - * @param key:要存的键 - * @param value :要存的值 - */ - setSessionStorage: function(key, value) { - if (this.isObject(value) || this.isArray(value)) { - value = this.toJsonStr(value); - } - sessionStorage[key] = value; - }, - - /** - *获取SessionStorage的值 - * @param key:存的键 - */ - getSessionStorage: function(key) { - var rs = sessionStorage[key]; - try { - if (rs != undefined) { - var obj = this.toJson(rs); - rs = obj; - } - } catch (error) {} - return rs; - }, - - /** - * 清除SessionStorage的值 - * @param key:存的键 - */ - removeSessionStorage: function(key) { - return sessionStorage.removeItem(key); - }, - - /** - *设置LocalStorage的值 - * @param key:要存的键 - * @param value :要存的值 - */ - setLocalStorage: function(key, value) { - if (this.isObject(value) || this.isArray(value)) { - value = this.toJsonStr(value); - } - localStorage[key] = value; - }, - - /** - *获取LocalStorage的值 - * @param key:存的键 - */ - getLocalStorage: function(key) { - var rs = localStorage[key]; - try { - if (rs != undefined) { - var obj = this.toJson(rs); - rs = obj; - } - } catch (error) {} - return rs; - }, - - /** - * 对传入的时间值进行格式化。后台传入前台的时间有两种个是:Sql时间和.Net时间 - * @param {String|Date} sValue 传入的时间字符串 - * @param {dateFormat | bool} dateFormat 日期格式,日期格式:eg:'Y-m-d H:i:s' - * @return {String} 2014-03-01 这种格式 - * @example - * 1) Sql时间格式:2015-02-24T00:00:00 - * 2) .Net时间格式:/Date(1410744626000)/ - */ - getDateTimeStr: function(sValue, dateFormat) { - if (dateFormat == undefined) { - dateFormat = 'Y-m-d'; // 默认显示年月日 - } - - var dt; - // 1.先解析传入的时间对象, - if (sValue) { - if (toString.call(sValue) !== '[object Date]') { - // 不为Date格式,就转换为DateTime类型 - sValue = sValue + ''; - if (sValue.indexOf('T') > 0) { - // 1)格式:2015-02-24T00:00:00 - var timestr = sValue.replace('T', ' ').replace(/-/g, '/'); //=> 2015/02/24 00:00:00 - dt = new Date(timestr); - } else if (sValue.indexOf('Date') >= 0) { - // 2).Net格式:/Date(1410744626000)/ - //Convert date type that .NET can bind to DateTime - //var date = new Date(parseInt(sValue.substr(6))); - var timestr = sValue.toString().replace(/\/Date\((\d+)\)\//gi, '$1'); // - dt = new Date(Math.abs(timestr)); - } else { - dt = new Date(sValue); - } - } else { - dt = sValue; - } - } - - // 2.转换 - // 1)转换成对象 'Y-m-d H:i:s' - var obj = {}; //返回的对象,包含了 year(年)、month(月)、day(日) - obj.Y = dt.getFullYear(); //年 - obj.m = dt.getMonth() + 1; //月 - obj.d = dt.getDate(); //日期 - obj.H = dt.getHours(); - obj.i = dt.getMinutes(); - obj.s = dt.getSeconds(); - //2.2单位的月、日都转换成双位 - if (obj.m < 10) { - obj.m = '0' + obj.m; - } - if (obj.d < 10) { - obj.d = '0' + obj.d; - } - if (obj.H < 10) { - obj.H = '0' + obj.H; - } - if (obj.i < 10) { - obj.i = '0' + obj.i; - } - if (obj.s < 10) { - obj.s = '0' + obj.s; - } - // 3.解析 - var rs = dateFormat - .replace('Y', obj.Y) - .replace('m', obj.m) - .replace('d', obj.d) - .replace('H', obj.H) - .replace('i', obj.i) - .replace('s', obj.s); - - return rs; - }, - - /** - * 把总秒数转换为时分秒 - */ - getSFM: function(seconds, dateFormat) { - if (dateFormat == undefined) { - dateFormat = 'H:i:s'; // 默认格式 - } - var obj = {}; - obj.H = Number.parseInt(seconds / 3600); - obj.i = Number.parseInt((seconds - obj.H * 3600) / 60); - obj.s = Number.parseInt(seconds - obj.H * 3600 - obj.i * 60); - if (obj.H < 10) { - obj.H = '0' + obj.H; - } - if (obj.i < 10) { - obj.i = '0' + obj.i; - } - if (obj.s < 10) { - obj.s = '0' + obj.s; - } - - // 3.解析 - var rs = dateFormat - .replace('H', obj.H) - .replace('i', obj.i) - .replace('s', obj.s); - return rs; - }, - - /** - * 是否同一天 - */ - isSomeDay: function(dt1, dt2) { - if (dt1.getFullYear() == dt2.getFullYear() && dt1.getMonth() == dt2.getMonth() && dt1.getDate() == dt2.getDate()) { - return true; - } - return false; - }, - - /** - * 对象转换为json字符串 - * @param {jsonObj} jsonObj Json对象 - * @return {jsonStr} Json字符串 - */ - toJsonStr: function(jsonObj) { - return JSON.stringify(jsonObj); - }, - - /** - * 讲json字符串转换为json对象 - * @param {String} jsonStr Json对象字符串 - * @return {jsonObj} Json对象 - */ - toJson: function(jsonStr) { - return JSON.parse(jsonStr); - }, - - /** - * @private - */ - getCookieVal: function(offset) { - var endstr = document.cookie.indexOf(';', offset); - if (endstr == -1) { - endstr = document.cookie.length; - } - return unescape(document.cookie.substring(offset, endstr)); - }, - - /** - * 获取指定key的cookie - * @param {String} key cookie的key - */ - getCookie: function(key) { - var arg = key + '=', - alen = arg.length, - clen = document.cookie.length, - i = 0, - j = 0; - - while (i < clen) { - j = i + alen; - if (document.cookie.substring(i, j) == arg) { - return this.getCookieVal(j); - } - i = document.cookie.indexOf(' ', i) + 1; - if (i === 0) { - break; - } - } - return null; - }, - - /** - * 设置cookie - * @param {String} key cookie的key - * @param {String} value cookie的value - */ - setCookie: function(key, value) { - var argv = arguments, - argc = arguments.length, - expires = argc > 2 ? argv[2] : null, - path = argc > 3 ? argv[3] : '/', - domain = argc > 4 ? argv[4] : null, - secure = argc > 5 ? argv[5] : false; - - document.cookie = - key + - '=' + - escape(value) + - (expires === null ? '' : '; expires=' + expires.toGMTString()) + - (path === null ? '' : '; path=' + path) + - (domain === null ? '' : '; domain=' + domain) + - (secure === true ? '; secure' : ''); - }, - - /** - * 是否含有特殊字符 - * @param {String} value 传入的值 - * @return {Boolean} true 含有特殊符号;false 不含有特殊符号 - */ - isHaveSpecialChar: function(value) { - var oldLength = value.length; - var newLength = value.replace(/[`~!@#$%^&*_+=\\{}:"<>?\[\];',.\/~!@#¥%……&*——+『』:“”《》?【】;‘’,。? \[\]()()]/g, '').length; - if (newLength < oldLength) { - return true; - } - return false; - }, - - /** - * 合并数组内成员的某个对象 - * @param {Array} arr 需要合并的数组 - * @param {String} fieldName 数组成员内的指定字段 - * @param {String} split 分隔符,默认为',' - * @example - * var arr = [{name:'tom',age:13},{name:'jack',age:13}] => (arr, 'name') => tom,jack - */ - joinArray: function(arr, fieldName, split) { - split = split == undefined ? ',' : split; - var rs = arr - .map((item) => { - return item[fieldName]; - }) - .join(split); - return rs; - } -}; - -/** - * http交互模块 - * 包含:ajax - */ -ak.Http = { - /** - * 将`name` - `value`对转换为支持嵌套结构的对象数组 - * - * var objects = toQueryObjects('hobbies', ['reading', 'cooking', 'swimming']); - * - * // objects then equals: - * [ - * { name: 'hobbies', value: 'reading' }, - * { name: 'hobbies', value: 'cooking' }, - * { name: 'hobbies', value: 'swimming' }, - * ]; - * - * var objects = toQueryObjects('dateOfBirth', { - * day: 3, - * month: 8, - * year: 1987, - * extra: { - * hour: 4 - * minute: 30 - * } - * }, true); // Recursive - * - * // objects then equals: - * [ - * { name: 'dateOfBirth[day]', value: 3 }, - * { name: 'dateOfBirth[month]', value: 8 }, - * { name: 'dateOfBirth[year]', value: 1987 }, - * { name: 'dateOfBirth[extra][hour]', value: 4 }, - * { name: 'dateOfBirth[extra][minute]', value: 30 }, - * ]; - * - * @param {String} name - * @param {object | Array} value - * @param {boolean} [recursive=false] 是否递归 - * @return {array} - */ - toQueryObjects: function(name, value, recursive) { - var objects = [], - i, - ln; - - if (ak.Utils.isArray(value)) { - for (i = 0, ln = value.length; i < ln; i++) { - if (recursive) { - objects = objects.concat(toQueryObjects(name + '[' + i + ']', value[i], true)); - } else { - objects.push({ - name: name, - value: value[i] - }); - } - } - } else if (ak.Utils.isObject(value)) { - for (i in value) { - if (value.hasOwnProperty(i)) { - if (recursive) { - objects = objects.concat(toQueryObjects(name + '[' + i + ']', value[i], true)); - } else { - objects.push({ - name: name, - value: value[i] - }); - } - } - } - } else { - objects.push({ - name: name, - value: value - }); - } - - return objects; - }, - - /** - * 把对象转换为查询字符串 - * e.g.: - * toQueryString({foo: 1, bar: 2}); // returns "foo=1&bar=2" - * toQueryString({foo: null, bar: 2}); // returns "foo=&bar=2" - * toQueryString({date: new Date(2011, 0, 1)}); // returns "date=%222011-01-01T00%3A00%3A00%22" - * @param {Object} object 需要转换的对象 - * @param {Boolean} [recursive=false] 是否递归 - * @return {String} queryString - */ - toQueryString: function(object, recursive) { - var paramObjects = [], - params = [], - i, - j, - ln, - paramObject, - value; - - for (i in object) { - if (object.hasOwnProperty(i)) { - paramObjects = paramObjects.concat(this.toQueryObjects(i, object[i], recursive)); - } - } - - for (j = 0, ln = paramObjects.length; j < ln; j++) { - paramObject = paramObjects[j]; - value = paramObject.value; - - if (ak.Utils.isEmpty(value)) { - value = ''; - } else if (ak.Utils.isDate(value)) { - value = - value.getFullYear() + - '-' + - Ext.String.leftPad(value.getMonth() + 1, 2, '0') + - '-' + - Ext.String.leftPad(value.getDate(), 2, '0') + - 'T' + - Ext.String.leftPad(value.getHours(), 2, '0') + - ':' + - Ext.String.leftPad(value.getMinutes(), 2, '0') + - ':' + - Ext.String.leftPad(value.getSeconds(), 2, '0'); - } - - params.push(encodeURIComponent(paramObject.name) + '=' + encodeURIComponent(String(value))); - } - - return params.join('&'); - }, - - /** - * 以get方式请求获取JSON数据 - * @param {Object} opts 配置项,可包含以下成员: - * @param {String} opts.url 请求地址 - * @param {Object} opts.params 附加的请求参数 - * @param {Boolean} opts.isHideLoading 是否关闭'载入中'提示框,默认false - * @param {String} opts.loadingTitle '载入中'提示框title,e.g. 提交中、上传中 - * @param {Function} opts.successCallback 成功接收内容时的回调函数 - * @param {Function} opts.failCallback 失败的回调函数 - */ - get: function(opts) { - if (!opts.isHideLoading) { - ak.Msg.showLoading(opts.loadingTitle); - } - if (opts.url.substr(0, 1) == '/') { - opts.url = opts.url.substr(1); - } - opts.url = ak.Base_URL + opts.url; - if (opts.params) { - opts.url = opts.url + '?' + this.toQueryString(opts.params); - } - // Jquery、Zepto - $.getJSON( - opts.url, - function(res, status, xhr) { - ak.Msg.hideLoading(); - if (res.resultCode == '0') { - if (opts.successCallback) { - opts.successCallback(res); - } - } else { - ak.Msg.toast(res.resultText, 'error'); - if (opts.failCallback) { - opts.failCallback(res); - } - } - }, - 'json' - ); - }, - - /** - * 以get方式请求获取JSON数据 - * @param {Object} opts 配置项,可包含以下成员: - * @param {String} opts.url 请求地址 - * @param {Object} opts.params 附加的请求参数 - * @param {Boolean} opts.ignoreFail 忽略错误,默认false,不管返回的结果如何,都执行 successCallback - * @param {Boolean} opts.ignoreEmptyParam 忽略空值,默认true - * @param {Boolean} opts.isHideLoading 是否关闭'载入中'提示框,默认false - * @param {String} opts.loadingTitle '载入中'提示框title,e.g. 提交中、上传中 - * @param {Function} opts.successCallback 成功接收内容时的回调函数 - * @param {Function} opts.failCallback 失败的回调函数 - */ - post: function(opts) { - opts.ignoreFail = opts.ignoreFail == undefined ? false : opts.ignoreFail; - opts.ignoreEmptyParam = opts.ignoreEmptyParam == undefined ? true : opts.ignoreEmptyParam; - if (!opts.isHideLoading) { - ak.Msg.showLoading(opts.loadingTitle); - } - if (opts.url.substr(0, 1) == '/') { - opts.url = opts.url.substr(1); - } - opts.url = ak.Base_URL + opts.url; // test - - // 去除params的空值 - if (opts.ignoreEmptyParam) { - for (var key in opts.params) { - if (opts.params[key] == undefined || opts.params[key] == '') { - delete opts.params[key]; - } - } - } - // Jquery、Zepto - $.post( - opts.url, - opts.params, - function(res, status, xhr) { - ak.Msg.hideLoading(); - if (res.resultCode == '0' || opts.ignoreFail) { - if (opts.successCallback) { - opts.successCallback(res); - } - } else { - ak.Msg.toast(res.resultText, 'error'); - if (opts.failCallback) { - opts.failCallback(res); - } - } - }, - 'json' - ); - }, - - /** - * 上传文件 - * @param {Object} opts 配置项,可包含以下成员: - * @param {Object} opts.params 上传的参数 - * @param {Object} opts.fileParams 上传文件参数 - * @param {String} opts.url 请求地址 - * @param {Function} opts.successCallback 成功接收内容时的回调函数 - * @param {Function} opts.failCallback 失败的回调函数 - */ - uploadFile: function(opts) { - // 1.解析url - if (opts.url.substr(0, 1) == '/') { - opts.url = opts.url.substr(1); - } - opts.url = ak.Base_URL + opts.url; - if (opts.params) { - opts.url = opts.url + '?' + this.toQueryString(opts.params); - } - - // 2.文件参数 - var formData = new FormData(); - for (var key in opts.fileParams) { - formData.append(key, opts.fileParams[key]); - } - - // 3.发起ajax - $.ajax({ - url: opts.url, - type: 'POST', - cache: false, - data: formData, - processData: false, - contentType: false, - dataType: 'json' - }) - .done(function(res) { - if (res.resultCode != '0') { - ak.Msg.toast(res.resultText, 'error'); - } - if (opts.successCallback) { - opts.successCallback(res); - } - }) - .fail(function(res) { - if (opts.failCallback) { - opts.failCallback(res); - } - }); - } -}; - -/** - * 消息模块 - * 包含:确认框、信息提示框 - */ -ak.Msg = { - /** - * 提示框 - * msg {string} :信息内容 - */ - alert: function(msg) {}, - - /** - * 确认框 - * msg {string} :信息内容 - * callback {function} :点击'确定'时的回调函数。 - */ - confirm: function(msg, callback) { - - }, - - /** - * 显示正在加载 - * @param {String} title 显示的title - */ - showLoading: function(title) { - - }, - - /** - * 关闭正在加载 - */ - hideLoading: function() {}, - - /** - * 自动消失的提示框 - * @param {String} msg 信息内容 - */ - toast: function(msg) {} -}; - -/** - * 业务相关逻辑 - */ -ak.BLL = {}; - -export default ak; \ No newline at end of file diff --git a/src/plugins/vue-im/src/common/css/base.less b/src/plugins/vue-im/src/common/css/base.less deleted file mode 100644 index aec41d69..00000000 --- a/src/plugins/vue-im/src/common/css/base.less +++ /dev/null @@ -1,129 +0,0 @@ -// 公共类 -#common-wrapper { - .hide { - display: none !important; - } - .show { - display: initial !important; - } - .float-left { - float: left; - } - .float-right { - float: right; - } - .text-right { - text-align: right; - } - .text-center { - text-align: center; - } - .text-left { - text-align: left; - } - .red { - color: red; - } - ::-webkit-scrollbar { - width: 10px; - background: transparent; - } - ::-webkit-scrollbar-track-piece { - background: none; - } - ::-webkit-scrollbar-thumb { - height: 50px; - border: 2px solid rgba(0, 0, 0, 0); - border-radius: 12px; - background-clip: padding-box; - background-color: #ccd4d4; - box-shadow: inset -1px -1px 0px #ccd4d4, inset 1px 1px 0px #ccd4d4; - } - .position-h-mid { - position: absolute; - left: 50%; - transform: translate(-50%, 0); - } - .position-v-mid { - position: absolute; - top: 50%; - transform: translate(0, -50%); - } - .position-h-v-mid { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - } -} - -// elemUI相关 -#common-wrapper { - .el-button--text { - margin: 0px; - padding: 0px; - color: #00a8d7; - } - .el-button--primary { - background-color: #00a8d7; - border-color: #00a8d7; - &.is-disabled { - color: #ffffff; - cursor: not-allowed; - background-image: none; - background-color: #b8e9f8; - border-color: #b8e9f8; - } - } - .el-textarea__inner { - resize: none; - } - .el-select, - .el-slider__runway { - z-index: 0; - } - .el-input__inner { - &:hover { - border-color: #00a8d7; - } - } - .el-select { - .el-tag--primary { - background-color: #f4f4f4; - border-color: #dfe4e6; - color: #6e6e6e; - } - &:hover { - .el-input__inner { - border-color: #00a8d7; - } - } - } - .el-dropdown { - .el-icon-caret-bottom { - font-size: 12px; - margin-left: 16px; - } - } - .el-tag { - background-color: #f4f4f4; - color: #454545; - border-color: #e6e6e6; - padding: 0px 10px; - } - .el-pager { - li.active { - border-color: #00a8d7; - background-color: #00a8d7; - } - } - .el-dialog__wrapper { - .el-dialog__body { - padding: 0px; - } - } -} - -body { - font-family: 'Microsoft YaHei', 'CaviarDreams Bold', Helvetica, Arial, sans-serif, 'STHeiti'; -} diff --git a/src/plugins/vue-im/src/common/http.js b/src/plugins/vue-im/src/common/http.js deleted file mode 100644 index de93a35d..00000000 --- a/src/plugins/vue-im/src/common/http.js +++ /dev/null @@ -1,92 +0,0 @@ -import Vue from 'vue'; -import axios from 'axios'; - -var axiosInstance = axios.create({ - baseURL: location.origin.replace(/:\d+/, ':3000'), - timeout: 1000 * 5 -}); - -axiosInstance.interceptors.request.use( - function(config) { - // Do something before request is sent - return config; - }, - function(error) { - // Do something with request error - return Promise.reject(error); - } -); - -/** - * http请求响应处理函数 - */ -var httpResponseHandle = function() { - var self = this; - if (self.res.code == '0') { - self.successCallback && self.successCallback(self.res.data); - } else { - self.failCallback && self.failCallback(self.res.data); - } -}; - -var http = { - /** - * 以get方式请求获取JSON数据 - * @param {Object} opts 配置项,可包含以下成员: - * @param {String} opts.url 请求地址 - * @param {Object} opts.params 附加的请求参数 - * @param {Function} opts.successCallback 成功接收内容时的回调函数 - */ - get: function(opts) { - if (opts.params) { - opts.url = opts.url + '?' + this.toQueryString(opts.params); - } - axiosInstance - .get(opts.url, { params: opts.params }) - .then(function(res) { - opts.res = res.data; - httpResponseHandle.call(opts); - }) - .catch(function(err) {}); - }, - - /** - * 以get方式请求获取JSON数据 - * @param {Object} opts 配置项,可包含以下成员: - * @param {String} opts.url 请求地址 - * @param {Object} opts.params 附加的请求参数 - * @param {Function} opts.successCallback 成功接收内容时的回调函数 - */ - post: function(opts) { - axiosInstance - .post(opts.url, opts.params) - .then(function(res) { - opts.res = res.data; - httpResponseHandle.call(opts); - }) - .catch(function(err) {}); - }, - - /** - * 上传文件 - * @param {Object} opts 配置项,可包含以下成员: - * @param {String} opts.url 请求地址 - * @param {Object} opts.params 上传的参数 - * @param {Function} opts.successCallback 成功接收内容时的回调函数 - */ - uploadFile: function(opts) { - axiosInstance - .post('/upload', opts.params, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }) - .then(function(res) { - opts.res = res.data; - httpResponseHandle.call(opts); - }) - .catch(function() {}); - } -}; - -export default http; diff --git a/src/plugins/vue-im/src/components/common/common_chat.vue b/src/plugins/vue-im/src/components/common/common_chat.vue deleted file mode 100644 index 5cafdc96..00000000 --- a/src/plugins/vue-im/src/components/common/common_chat.vue +++ /dev/null @@ -1,952 +0,0 @@ - - - - - - diff --git a/src/plugins/vue-im/src/components/common/common_chat_emoji.vue b/src/plugins/vue-im/src/components/common/common_chat_emoji.vue deleted file mode 100644 index 3d484c14..00000000 --- a/src/plugins/vue-im/src/components/common/common_chat_emoji.vue +++ /dev/null @@ -1,549 +0,0 @@ - - - - - - diff --git a/src/plugins/vue-im/src/components/imClient/imClient.vue b/src/plugins/vue-im/src/components/imClient/imClient.vue deleted file mode 100644 index 6dd31861..00000000 --- a/src/plugins/vue-im/src/components/imClient/imClient.vue +++ /dev/null @@ -1,498 +0,0 @@ - - - - - - diff --git a/src/plugins/vue-im/src/components/imClient/imLeave.vue b/src/plugins/vue-im/src/components/imClient/imLeave.vue deleted file mode 100644 index c2440c04..00000000 --- a/src/plugins/vue-im/src/components/imClient/imLeave.vue +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - diff --git a/src/plugins/vue-im/src/components/imClient/imRate.vue b/src/plugins/vue-im/src/components/imClient/imRate.vue deleted file mode 100644 index f50bb833..00000000 --- a/src/plugins/vue-im/src/components/imClient/imRate.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - diff --git a/src/plugins/vue-im/src/components/imClient/imTransfer.vue b/src/plugins/vue-im/src/components/imClient/imTransfer.vue deleted file mode 100644 index c8120681..00000000 --- a/src/plugins/vue-im/src/components/imClient/imTransfer.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - diff --git a/src/plugins/vue-im/src/components/imServer/faqList.js b/src/plugins/vue-im/src/components/imServer/faqList.js deleted file mode 100644 index 164c0012..00000000 --- a/src/plugins/vue-im/src/components/imServer/faqList.js +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable sort-keys */ -// 配置文件,所有配置必须齐全,补充空白配置项,其他配置项可按需要修改 -const faqList = [ - { - title: '到货信息相关', - content: [ - '一般三天内发货,详细请查看群公告', - '预计明天到', - '翅中严重缺货,只能安排下一批同时到。下周二到。' - ] - }, - { - title: '其他', - content: [ - '今天周五' - ] - } -] - -export default faqList diff --git a/src/plugins/vue-im/src/components/imServer/imChat.vue b/src/plugins/vue-im/src/components/imServer/imChat.vue deleted file mode 100644 index 918bef88..00000000 --- a/src/plugins/vue-im/src/components/imServer/imChat.vue +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - diff --git a/src/plugins/vue-im/src/components/imServer/imRecord.vue b/src/plugins/vue-im/src/components/imServer/imRecord.vue deleted file mode 100644 index cd4bd627..00000000 --- a/src/plugins/vue-im/src/components/imServer/imRecord.vue +++ /dev/null @@ -1,283 +0,0 @@ - - - - - - diff --git a/src/plugins/vue-im/src/components/imServer/imServer.vue b/src/plugins/vue-im/src/components/imServer/imServer.vue deleted file mode 100644 index ec866a9a..00000000 --- a/src/plugins/vue-im/src/components/imServer/imServer.vue +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - diff --git a/src/plugins/vue-im/src/main.js b/src/plugins/vue-im/src/main.js deleted file mode 100644 index 81cf16e7..00000000 --- a/src/plugins/vue-im/src/main.js +++ /dev/null @@ -1,32 +0,0 @@ -// The Vue build version to load with the `import` command -// (runtime-only or standalone) has been set in webpack.base.conf with an alias. -import Vue from 'vue'; -import App from './App'; -import router from './router'; -import { imServerStore } from './store/imServerStore.js'; -// axios -import http from '@/common/http.js'; -Vue.prototype.$http = http; -// ak -import ak from '@/common/ak.js'; -Vue.prototype.$ak = ak; -// element-ui -import ElementUI from 'element-ui'; -import 'element-ui/lib/theme-chalk/index.css'; -Vue.use(ElementUI); -// font-awesome -import 'font-awesome/css/font-awesome.min.css' - -// config -Vue.config.productionTip = false; - -/* eslint-disable no-new */ -window.polkVue = new Vue({ - el: '#app', - router, - components: { App }, - store: { - imServerStore: imServerStore - }, - template: '' -}); diff --git a/src/plugins/vue-im/src/router/index.js b/src/plugins/vue-im/src/router/index.js deleted file mode 100644 index 1e985271..00000000 --- a/src/plugins/vue-im/src/router/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import Vue from 'vue' -import Router from 'vue-router' -import imServer from '@/components/imServer/imServer' -import imClient from '@/components/imClient/imClient' - -Vue.use(Router) - -export default new Router({ - routes: [ - { path: '/', redirect: 'imServer' }, - { path: '/imServer', name: 'imServer', component: imServer }, - { path: '/imClient', name: 'imClient', component: imClient }, - ] -}) \ No newline at end of file diff --git a/src/plugins/vue-im/src/store/imServerStore.js b/src/plugins/vue-im/src/store/imServerStore.js deleted file mode 100644 index 0c000cdc..00000000 --- a/src/plugins/vue-im/src/store/imServerStore.js +++ /dev/null @@ -1,495 +0,0 @@ -/*! - * im服务端Store - */ - -import Vue from 'vue'; -import Vuex from 'vuex'; -import ak from '@/common/ak.js'; - -Vue.use(Vuex); -export const imServerStore = new Vuex.Store({ - state: { - serverChatEn: { - serverChatId: 'xiaop', - serverChatName: '小P', - avatarUrl: '/static/image/im_server_avatar.png' - }, - selectedChatEn: null, // 选取的会话对象 - currentChatEnlist: [], // 当前chat实体集合 - notificationChatEnlist: [], // 通知chat实体集合 - haveNewMsgDelegate: null, // 当前已选中的用户含有新消息 - socket: null - }, - mutations: { - /** - * 触发当前选择的chat含有新的消息 - * @param {Object} payload 载荷对象 - */ - triggerHaveNewMsgDelegate: function (state, payload) { - state.haveNewMsgDelegate = Date.now(); - }, - - /** - * 排序当前会话列表 - */ - sortCurrentChatEnlist: function (state, payload) { - var enlist = state.currentChatEnlist.concat(); - - // 排序规则: - // 1)已关注放最前面,关注状态下按最后一条获取时间正序 - // 2)非关注状态下,按最后一条获取时间正序 - - // 1.首先按最后一次更新时间排序 - for (var i = 0; i < enlist.length; i++) { - for (var j = i; j < enlist.length; j++) { - var iTimeSpan = Date.parse(enlist[i].lastMsgTime); - var jTimeSpan = Date.parse(enlist[j].lastMsgTime); - if (iTimeSpan < jTimeSpan) { - var tmp = enlist[i]; - enlist[i] = enlist[j]; - enlist[j] = tmp; - } - } - } - - // 2.已关注的排在最前面并按最后一次时间倒序 - var followEnlist = []; - var unfollowEnlist = []; - for (var i = 0; i < enlist.length; i++) { - var en = enlist[i]; - if (en.isFollow) { - followEnlist.push(en); - } else { - unfollowEnlist.push(en); - } - } - - // 3.合并 - state.currentChatEnlist = followEnlist.concat(unfollowEnlist); - }, - - /** - * 清除通知chat - */ - clearNotificationChat: function (state) { - state.notificationChatEnlist = []; - } - }, - actions: { - /** - * 添加访客端chat对象 - * @param {Object} payload 载荷对象 - * @param {String} payload.newChatEn 新的chat对象 - */ - addClientChat: function (context, { newChatEn }) { - context.dispatch('getChatEnByChatId', { clientChatId: newChatEn.clientChatId }).then((chatEn) => { - if (chatEn == null) { - // 1)公共属性 - newChatEn.msgList = []; - newChatEn.state = 'on'; - newChatEn.accessTime = new Date(); // 访问时间 - newChatEn.inputContent = ''; // 输入框内容 - newChatEn.newMsgCount = 0; - newChatEn.isFollow = false; // 是否关注 - newChatEn.lastMsgTime = null; - newChatEn.lastMsgShowTime = null; // 最后一个消息的显示时间 - context.state.currentChatEnlist.push(newChatEn); - } - - // 2)增加消息 - context.dispatch('addChatMsg', { - clientChatId: newChatEn.clientChatId, - msg: { - role: 'sys', - contentType: 'text', - content: chatEn == null ? '新客户接入' : '重新连接' - } - }); - }); - }, - /** - * 根据jobId获取chat对象 - * @param {String} clientChatId 需要修改的chatEn的id,根据此id匹配当前集合或历史集合 - * @param {String} listName 指定的集合名称;e.g. currentChatEnlist、historyChatEnlist、allHistoryChatEnlist - */ - getChatEnByChatId: function (context, { clientChatId, listName }) { - var chatEn = null; - - if (listName) { - // 1.指定了列表 - var targetList = context.state[listName]; - for (var i = 0; i < targetList.length; i++) { - var tmpEn = targetList[i]; - if (tmpEn.clientChatId == clientChatId) { - chatEn = tmpEn; - break; - } - } - } else { - // 2.未指定列表 - // 1)从当前会话列表查找 - for (var i = 0; i < context.state.currentChatEnlist.length; i++) { - var tmpEn = context.state.currentChatEnlist[i]; - if (tmpEn.clientChatId == clientChatId) { - chatEn = tmpEn; - break; - } - } - } - - return chatEn; - }, - - /** - * 修改Chat对象的属性 - * @param {Object} payload 载荷对象 - * @param {Object} payload.clientChatId 需要修改的chatEn的id,根据此id匹配当前集合或历史集合 - * @param {Array} payload.extends Chat需要变更的属性对象数组 - */ - extendChatEn: function (context, payload) { - return context.dispatch('getChatEnByChatId', { clientChatId: payload.clientChatId }).then((chatEn) => { - // 1.若没有,就附加到当前会话列表里 - if (chatEn == null) { - return; - } - - // 2.extend属性 - for (var key in payload.extends) { - Vue.set(chatEn, key, payload.extends[key]); - } - - // 3.若选中的当前chatEn 与 传入的一直,更新选中额chatEn - if (context.state.selectedChatEn && context.state.selectedChatEn.clientChatId == chatEn.clientChatId) { - context.state.selectedChatEn = Object.assign({}, chatEn); - Vue.nextTick(function () { }); - } - return chatEn; - }); - }, - - /** - * 添加chat对象的msg - * @param {String} clientChatId 会话Id - * @param {Object} msg 消息对象;eg:{role:'sys',content:'含有新的消息'} - * @param {String} msg.role 消息所有者身份;eg:'sys'系统消息; - * @param {String} msg.contentType 消息类型;text:文本(默认);image:图片 - * @param {String} msg.content 消息内容 - * @param {Function} successCallback 添加消息后的回调 - */ - addChatMsg: function (context, { clientChatId, msg, successCallback }) { - context.dispatch('getChatEnByChatId', { clientChatId: clientChatId }).then((chatEn) => { - if (chatEn == null) { - return; - } - - // 1.设定默认值 - msg.createTime = msg.createTime == undefined ? new Date() : msg.createTime; - - var msgList = chatEn.msgList ? chatEn.msgList : []; - - // 2.插入消息 - // 1)插入日期 - // 实际场景中,在消息上方是否显示时间是由后台传递给前台的消息中附加上的,可参考 微信Web版 - // 此处进行手动设置,5分钟之内的消息,只显示一次消息 - msg.createTime = new Date(msg.createTime); - if (chatEn.lastMsgShowTime == null || msg.createTime.getTime() - chatEn.lastMsgShowTime.getTime() > 1000 * 60 * 5) { - msgList.push({ - role: 'sys', - contentType: 'text', - content: ak.Utils.getDateTimeStr(msg.createTime, 'H:i') - }); - chatEn.lastMsgShowTime = msg.createTime; - } - - // 2)插入消息 - msgList.push(msg); - - // 3.设置chat对象相关属性 - chatEn.msgList = msgList; - chatEn.lastMsgTime = msg.createTime; - switch (msg.contentType) { - case 'text': - chatEn.lastMsgContent = msg.content; - break; - case 'image': - chatEn.lastMsgContent = '[图片]'; - break; - case 'file': - chatEn.lastMsgContent = '[文件]'; - break; - case 'sound': - chatEn.lastMsgContent = '[语音]'; - break; - } - // 更新列表 - if (context.state.selectedChatEn && chatEn.clientChatId == context.state.selectedChatEn.clientChatId) { - chatEn.newMsgCount = 0; - context.state.selectedChatEn = Object.assign({}, chatEn); - context.commit('triggerHaveNewMsgDelegate'); - } else { - chatEn.newMsgCount++; - } - - // 4.排序 - context.commit('sortCurrentChatEnlist', {}); - - // 5.加入通知 - if (msg.isNewMsg && msg.role == 'client' && msg.contentType != 'preInput') { - context.dispatch('addNotificationChat', { - chatEn: chatEn, - oprType: 'msg' - }); - } - - // 6.回调 - successCallback && successCallback(); - }); - }, - - /** - * 选中会话 - * @param {String} clientChatId 选中会话Id - */ - selectChat: function (context, { clientChatId }) { - context.dispatch('getChatEnByChatId', { clientChatId: clientChatId }).then((chatEn) => { - var state = context.state; - chatEn.newMsgCount = 0; // 设置新消息为0 - // 1.设置当前选中的会话 - context.state.selectedChatEn = Object.assign({}, chatEn); - - // 2.刷新当前会话集合 - for (var i = 0; i < state.currentChatEnlist.length; i++) { - var tmpEn = state.currentChatEnlist[i]; - if (tmpEn.clientChatId == chatEn.clientChatId) { - state.currentChatEnlist[i] = state.selectedChatEn; - break; - } - } - }); - }, - - /** - * 添加通知chat - * @param {Object} chatEn 会话对象 - * @param {String} oprType 操作类型;eg:chat(添加会话)、msg(添加消息) - */ - addNotificationChat: function (context, { chatEn, oprType }) { - var state = context.state; - // 当前的路由是否在im模块里,若不在im模块里,才显示通知 - if (window.polkVue.$route.name == 'im') { - return; - } - - // 1.判断当前通知集合里是否已存在次会话,若已存在去除此会话 - for (var i = 0; i < state.notificationChatEnlist.length; i++) { - if (state.notificationChatEnlist[i].clientChatId == chatEn.clientChatId) { - state.notificationChatEnlist.splice(i, 1); - break; - } - } - - // 2.集合最多只能有5个 - if (state.notificationChatEnlist.length > 5) { - state.notificationChatEnlist = state.notificationChatEnlist.splice(4); - } - - // 3.转换后加入到当前通知集合里 - var tmpChatEn = { - clientChatId: chatEn.clientChatId, - sourceInfo_way: chatEn.sourceInfo_way, - site: window.location.host - }; - if (oprType == 'chat') { - tmpChatEn.title = '新用户'; - tmpChatEn.content = '客户 ' + chatEn.clientChatName + ' 接入新会话'; - } else if (oprType == 'msg') { - tmpChatEn.title = '客户 ' + chatEn.clientChatName + ' ' + chatEn.newMsgCount + '条新消息'; - tmpChatEn.content = chatEn.lastMsgContent; - } - - // 4.内容大于25个截断 - if (tmpChatEn.content.length > 25) { - tmpChatEn.content = tmpChatEn.content.substr(0, 24) + '...'; - } - - // 5.加入到集合里 - state.notificationChatEnlist.push(tmpChatEn); - - // 6.当通知数量大于5个时清除通知 - window.imServerStore_notificationList = window.imServerStore_notificationList || []; - if (window.imServerStore_notificationList.length > 5) { - window.imServerStore_notificationList.forEach((item, index) => { - item.close(); - }); - window.imServerStore_notificationList = []; - } - - // 7.显示通知 - for (var i = 0; i < state.notificationChatEnlist.length; i++) { - const item = state.notificationChatEnlist[i]; - // 1)已存在的通知列表是否包含此会话,若存在就关闭并移除 - for (var j = 0; j < window.imServerStore_notificationList.length; j++) { - if (window.imServerStore_notificationList[j].data == item.clientChatId) { - window.imServerStore_notificationList[j].close(); - break; - } - } - - // 2)创建新的通知 - const notification = new Notification(item.title, { - body: item.content, - data: item.clientChatId, - tag: Date.now(), - icon: ak.BLL.getPngFromWay(item.sourceInfo_way) - }); - notification.onclick = function (e) { - window.focus(); - window.polkVue.$router.push('im'); - context.commit('clearNotificationChat'); - context.dispatch('selectChat', { clientChatId: item.clientChatId }); - notification.close(); - imServerStore_notificationList = []; - }; - - notification.onclose = function (e) { - // remove en - for (var i = 0; i < state.notificationChatEnlist.length; i++) { - if (state.notificationChatEnlist[i].clientChatId == item.clientChatId) { - state.notificationChatEnlist.splice(i, 1); - break; - } - } - // remove notification - for (var i = 0; i < window.imServerStore_notificationList.length; i++) { - if (window.imServerStore_notificationList[i].tag == notification.tag) { - window.imServerStore_notificationList.splice(i, 1); - break; - } - } - }; - - setTimeout(function () { - notification && notification.close(); - }, 1000 * 10); - - window.imServerStore_notificationList.push(notification); - } - }, - - /** - * 服务端上线 - */ - SERVER_ON: function (context, payload) { - context.state.socket = require('socket.io-client')('http://localhost:3001'); - context.state.socket.on('connect', function () { - // 服务端上线 - context.state.socket.emit('SERVER_ON', { - serverChatEn: { - serverChatId: context.state.serverChatEn.serverChatId, - serverChatName: context.state.serverChatEn.serverChatName, - avatarUrl: context.state.serverChatEn.avatarUrl - } - }); - - // 访客端上线 - context.state.socket.on('CLIENT_ON', function (data) { - // 1)增加客户列表 - context.dispatch('addClientChat', { - newChatEn: { - clientChatId: data.clientChatEn.clientChatId, - clientChatName: data.clientChatEn.clientChatName - } - }); - }); - - // 访客端离线 - context.state.socket.on('CLIENT_OFF', function (data) { - // 1)修改客户状态为离线 - context.dispatch('extendChatEn', { - clientChatId: data.clientChatEn.clientChatId, - extends: { - state: 'off' - } - }); - - // 2)增加消息 - context.dispatch('addChatMsg', { - clientChatId: data.clientChatEn.clientChatId, - msg: { - role: 'sys', - contentType: 'text', - content: '客户断开连接' - } - }); - }); - - // 访客端发送了信息 - context.state.socket.on('CLIENT_SEND_MSG', function (data) { - context.dispatch('addChatMsg', { - clientChatId: data.clientChatEn.clientChatId, - msg: data.msg - }); - }); - - // 离开 - window.addEventListener('beforeunload', () => { - context.dispatch('SERVER_OFF'); - }); - }); - }, - - /** - * 服务端离线 - */ - SERVER_OFF: function (context, payload) { - context.state.socket.emit('SERVER_OFF', { - serverChatEn: { - serverChatId: context.state.serverChatEn.serverChatId, - serverChatName: context.state.serverChatEn.serverChatName - } - }); - context.state.socket.close(); - context.state.socket = null; - }, - - /** - * 发送消息 - */ - sendMsg: function (context, { clientChatId, msg }) { - console.log(clientChatId); - context.state.socket.emit('SERVER_SEND_MSG', { - clientChatId: clientChatId, - msg: msg - }); - } - }, - getters: { - /** - * 获取选中的会话对象 - */ - selectedChatEn: function (state) { - return state.selectedChatEn; - }, - - /** - * 当前会话集合 - */ - currentChatEnlist: function (state) { - return state.currentChatEnlist; - }, - - /** - * 选中的chat含有新消息 - */ - haveNewMsgDelegate: function (state) { - return state.haveNewMsgDelegate; - }, - - /** - * 客服chat信息 - */ - serverChatEn: function (state) { - return state.serverChatEn; - } - } -}); diff --git a/src/plugins/vue-im/static/css/reset.css b/src/plugins/vue-im/static/css/reset.css deleted file mode 100644 index 7d8b2156..00000000 --- a/src/plugins/vue-im/static/css/reset.css +++ /dev/null @@ -1,140 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -html, -body, -div, -span, -applet, -object, -iframe, -h1, -h2, -h3, -h4, -h5, -h6, -p, -blockquote, -pre, -a, -abbr, -acronym, -address, -big, -cite, -code, -del, -dfn, -em, -img, -ins, -kbd, -q, -s, -samp, -small, -strike, -strong, -sub, -sup, -tt, -var, -b, -u, -i, -center, -dl, -dt, -dd, -ol, -ul, -li, -fieldset, -form, -label, -legend, -table, -caption, -tbody, -tfoot, -thead, -tr, -th, -td, -article, -aside, -canvas, -details, -embed, -figure, -figcaption, -footer, -header, -hgroup, -menu, -nav, -output, -ruby, -section, -summary, -time, -mark, -audio, -video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} - - -/* HTML5 display-role reset for older browsers */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -menu, -nav, -section { - display: block; -} - -body { - width: 100%; - height: 100%; - line-height: 1; - position: absolute; -} - -ol, -ul { - list-style: none; -} - -blockquote, -q { - quotes: none; -} - -blockquote:before, -blockquote:after, -q:before, -q:after { - content: ''; - content: none; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} \ No newline at end of file diff --git a/src/plugins/vue-im/static/image/im_client_avatar.png b/src/plugins/vue-im/static/image/im_client_avatar.png deleted file mode 100644 index 7b99ff1f..00000000 Binary files a/src/plugins/vue-im/static/image/im_client_avatar.png and /dev/null differ diff --git a/src/plugins/vue-im/static/image/im_emoji_spacer.gif b/src/plugins/vue-im/static/image/im_emoji_spacer.gif deleted file mode 100644 index 35d42e80..00000000 Binary files a/src/plugins/vue-im/static/image/im_emoji_spacer.gif and /dev/null differ diff --git a/src/plugins/vue-im/static/image/im_robot_avatar.png b/src/plugins/vue-im/static/image/im_robot_avatar.png deleted file mode 100644 index eaa58c3c..00000000 Binary files a/src/plugins/vue-im/static/image/im_robot_avatar.png and /dev/null differ diff --git a/src/plugins/vue-im/static/image/im_server_avatar.png b/src/plugins/vue-im/static/image/im_server_avatar.png deleted file mode 100644 index 60d0f08a..00000000 Binary files a/src/plugins/vue-im/static/image/im_server_avatar.png and /dev/null differ diff --git a/src/plugins/vue-im/static/js/socket.io.js b/src/plugins/vue-im/static/js/socket.io.js deleted file mode 100644 index 827407bd..00000000 --- a/src/plugins/vue-im/static/js/socket.io.js +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * Socket.IO v2.1.0 - * (c) 2014-2018 Guillermo Rauch - * Released under the MIT License. - */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t,e){"object"===("undefined"==typeof t?"undefined":o(t))&&(e=t,t=void 0),e=e||{};var n,r=i(t),s=r.source,p=r.id,h=r.path,f=u[p]&&h in u[p].nsps,l=e.forceNew||e["force new connection"]||!1===e.multiplex||f;return l?(c("ignoring socket cache for %s",s),n=a(s,e)):(u[p]||(c("new io instance for %s",s),u[p]=a(s,e)),n=u[p]),r.query&&!e.query&&(e.query=r.query),n.socket(r.path,e)}var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=n(1),s=n(7),a=n(12),c=n(3)("socket.io-client");t.exports=e=r;var u=e.managers={};e.protocol=s.protocol,e.connect=r,e.Manager=n(12),e.Socket=n(37)},function(t,e,n){(function(e){"use strict";function r(t,n){var r=t;n=n||e.location,null==t&&(t=n.protocol+"//"+n.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?n.protocol+t:n.host+t),/^(https?|wss?):\/\//.test(t)||(i("protocol-less url %s",t),t="undefined"!=typeof n?n.protocol+"//"+t:"https://"+t),i("parse %s",t),r=o(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var s=r.host.indexOf(":")!==-1,a=s?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+a+":"+r.port,r.href=r.protocol+"://"+a+(n&&n.port===r.port?"":":"+r.port),r}var o=n(2),i=n(3)("socket.io-client:url");t.exports=r}).call(e,function(){return this}())},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");o!=-1&&i!=-1&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s=n.exec(t||""),a={},c=14;c--;)a[r[c]]=s[c]||"";return o!=-1&&i!=-1&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a}},function(t,e,n){(function(r){function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(t){var n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+e.humanize(this.diff),n){var r="color: "+this.color;t.splice(1,0,r,"color: inherit");var o=0,i=0;t[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(o++,"%c"===t&&(i=o))}),t.splice(i,0,r)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{null==t?e.storage.removeItem("debug"):e.storage.debug=t}catch(n){}}function c(){var t;try{t=e.storage.debug}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function u(){try{return window.localStorage}catch(t){}}e=t.exports=n(5),e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],e.formatters.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},e.enable(c())}).call(e,n(4))},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(p===setTimeout)return setTimeout(t,0);if((p===n||!p)&&setTimeout)return p=setTimeout,setTimeout(t,0);try{return p(t,0)}catch(e){try{return p.call(null,t,0)}catch(e){return p.call(this,t,0)}}}function i(t){if(h===clearTimeout)return clearTimeout(t);if((h===r||!h)&&clearTimeout)return h=clearTimeout,clearTimeout(t);try{return h(t)}catch(e){try{return h.call(null,t)}catch(e){return h.call(this,t)}}}function s(){y&&l&&(y=!1,l.length?d=l.concat(d):m=-1,d.length&&a())}function a(){if(!y){var t=o(s);y=!0;for(var e=d.length;e;){for(l=d,d=[];++m1)for(var n=1;n100)){var e=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*p;case"days":case"day":case"d":return n*u;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){return t>=u?Math.round(t/u)+"d":t>=c?Math.round(t/c)+"h":t>=a?Math.round(t/a)+"m":t>=s?Math.round(t/s)+"s":t+"ms"}function o(t){return i(t,u,"day")||i(t,c,"hour")||i(t,a,"minute")||i(t,s,"second")||t+" ms"}function i(t,e,n){if(!(t0)return n(t);if("number"===i&&isNaN(t)===!1)return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(){}function o(t){var n=""+t.type;if(e.BINARY_EVENT!==t.type&&e.BINARY_ACK!==t.type||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data){var r=i(t.data);if(r===!1)return g;n+=r}return f("encoded %j as %s",t,n),n}function i(t){try{return JSON.stringify(t)}catch(e){return!1}}function s(t,e){function n(t){var n=d.deconstructPacket(t),r=o(n.packet),i=n.buffers;i.unshift(r),e(i)}d.removeBlobs(t,n)}function a(){this.reconstructor=null}function c(t){var n=0,r={type:Number(t.charAt(0))};if(null==e.types[r.type])return h("unknown packet type "+r.type);if(e.BINARY_EVENT===r.type||e.BINARY_ACK===r.type){for(var o="";"-"!==t.charAt(++n)&&(o+=t.charAt(n),n!=t.length););if(o!=Number(o)||"-"!==t.charAt(n))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(n+1))for(r.nsp="";++n;){var i=t.charAt(n);if(","===i)break;if(r.nsp+=i,n===t.length)break}else r.nsp="/";var s=t.charAt(n+1);if(""!==s&&Number(s)==s){for(r.id="";++n;){var i=t.charAt(n);if(null==i||Number(i)!=i){--n;break}if(r.id+=t.charAt(n),n===t.length)break}r.id=Number(r.id)}if(t.charAt(++n)){var a=u(t.substr(n)),c=a!==!1&&(r.type===e.ERROR||y(a));if(!c)return h("invalid payload");r.data=a}return f("decoded %s as %j",t,r),r}function u(t){try{return JSON.parse(t)}catch(e){return!1}}function p(t){this.reconPack=t,this.buffers=[]}function h(t){return{type:e.ERROR,data:"parser error: "+t}}var f=n(3)("socket.io-parser"),l=n(8),d=n(9),y=n(10),m=n(11);e.protocol=4,e.types=["CONNECT","DISCONNECT","EVENT","ACK","ERROR","BINARY_EVENT","BINARY_ACK"],e.CONNECT=0,e.DISCONNECT=1,e.EVENT=2,e.ACK=3,e.ERROR=4,e.BINARY_EVENT=5,e.BINARY_ACK=6,e.Encoder=r,e.Decoder=a;var g=e.ERROR+'"encode error"';r.prototype.encode=function(t,n){if(f("encoding packet %j",t),e.BINARY_EVENT===t.type||e.BINARY_ACK===t.type)s(t,n);else{var r=o(t);n([r])}},l(a.prototype),a.prototype.add=function(t){var n;if("string"==typeof t)n=c(t),e.BINARY_EVENT===n.type||e.BINARY_ACK===n.type?(this.reconstructor=new p(n),0===this.reconstructor.reconPack.attachments&&this.emit("decoded",n)):this.emit("decoded",n);else{if(!m(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");n=this.reconstructor.takeBinaryData(t),n&&(this.reconstructor=null,this.emit("decoded",n))}},a.prototype.destroy=function(){this.reconstructor&&this.reconstructor.finishedReconstruction()},p.prototype.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=d.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null},p.prototype.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]}},function(t,e,n){function r(t){if(t)return o(t)}function o(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,o=0;o0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},r.prototype.cleanup=function(){h("cleanup");for(var t=this.subs.length,e=0;e=this._reconnectionAttempts)h("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();h("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(h("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(h("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(h("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(14),t.exports.parser=n(21)},function(t,e,n){(function(e){function r(t,n){if(!(this instanceof r))return new r(t,n);n=n||{},t&&"object"==typeof t&&(n=t,t=null),t?(t=p(t),n.hostname=t.host,n.secure="https"===t.protocol||"wss"===t.protocol,n.port=t.port,t.query&&(n.query=t.query)):n.host&&(n.hostname=p(n.host).host),this.secure=null!=n.secure?n.secure:e.location&&"https:"===location.protocol,n.hostname&&!n.port&&(n.port=this.secure?"443":"80"),this.agent=n.agent||!1,this.hostname=n.hostname||(e.location?location.hostname:"localhost"),this.port=n.port||(e.location&&location.port?location.port:this.secure?443:80),this.query=n.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==n.upgrade,this.path=(n.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!n.forceJSONP,this.jsonp=!1!==n.jsonp,this.forceBase64=!!n.forceBase64,this.enablesXDR=!!n.enablesXDR,this.timestampParam=n.timestampParam||"t",this.timestampRequests=n.timestampRequests,this.transports=n.transports||["polling","websocket"],this.transportOptions=n.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=n.policyPort||843,this.rememberUpgrade=n.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=n.onlyBinaryUpgrades,this.perMessageDeflate=!1!==n.perMessageDeflate&&(n.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=n.pfx||null,this.key=n.key||null,this.passphrase=n.passphrase||null,this.cert=n.cert||null,this.ca=n.ca||null,this.ciphers=n.ciphers||null,this.rejectUnauthorized=void 0===n.rejectUnauthorized||n.rejectUnauthorized,this.forceNode=!!n.forceNode;var o="object"==typeof e&&e;o.global===o&&(n.extraHeaders&&Object.keys(n.extraHeaders).length>0&&(this.extraHeaders=n.extraHeaders),n.localAddress&&(this.localAddress=n.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,this.open()}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(15),s=n(8),a=n(3)("engine.io-client:socket"),c=n(36),u=n(21),p=n(2),h=n(30);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=u.protocol,r.Socket=r,r.Transport=n(20),r.transports=n(15),r.parser=n(21),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=u.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),p.send([{type:"ping",data:"probe"}]),p.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",p),!p)return;r.priorWebsocketSuccess="websocket"===p.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),u(),f.setTransport(p),p.send([{type:"upgrade"}]),f.emit("upgrade",p),p=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=p.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,u(),p.close(),p=null)}function o(e){var r=new Error("probe error: "+e);r.transport=p.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){p&&t.name!==p.name&&(a('"%s" works - aborting "%s"',t.name,p.name),n())}function u(){p.removeListener("open",e),p.removeListener("error",o),p.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var p=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,p.once("open",e),p.once("error",o),p.once("close",i),this.once("close",s),this.once("upgrading",c),p.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:w}var i=new Uint8Array(t),o=i[0],s=f(t,1);return k&&"blob"===n&&(s=new k([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!u)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=u.decode(t.substr(1));return"blob"===e&&k&&(r=new k([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?k&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(w,0,1);for(var i,s,a="",c=0,u=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",u=1;255!==s[u];u++){if(c.length>310)return r(w,0,1);c+=s[u]}o=f(o,2+c.length),c=parseInt(c);var p=f(o,0,c);if(a)try{p=String.fromCharCode.apply(null,new Uint8Array(p))}catch(h){var l=new Uint8Array(p);p="";for(var u=0;ur&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=w(e>>>10&1023|55296),e=56320|1023&e),o+=w(e);return o}function c(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function u(t,e){return w(t>>e&63|128)}function p(t,e){if(0==(4294967168&t))return w(t);var n="";return 0==(4294965248&t)?n=w(t>>6&31|192):0==(4294901760&t)?(c(t,e)||(t=65533),n=w(t>>12&15|224),n+=u(t,6)):0==(4292870144&t)&&(n=w(t>>18&7|240),n+=u(t,12),n+=u(t,6)),n+=w(63&t|128)}function h(t,e){e=e||{};for(var n,r=!1!==e.strict,o=s(t),i=o.length,a=-1,c="";++a=v)throw Error("Invalid byte index");var t=255&g[b];if(b++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function l(t){var e,n,r,o,i;if(b>v)throw Error("Invalid byte index");if(b==v)return!1;if(e=255&g[b],b++,0==(128&e))return e;if(192==(224&e)){if(n=f(),i=(31&e)<<6|n,i>=128)return i;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=f(),r=f(),i=(15&e)<<12|n<<6|r,i>=2048)return c(i,t)?i:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=f(),r=f(),o=f(),i=(7&e)<<18|n<<12|r<<6|o,i>=65536&&i<=1114111))return i;throw Error("Invalid UTF-8 detected")}function d(t,e){e=e||{};var n=!1!==e.strict;g=s(t),v=g.length,b=0;for(var r,o=[];(r=l(n))!==!1;)o.push(r);return a(o)}var y="object"==typeof e&&e,m=("object"==typeof t&&t&&t.exports==y&&t,"object"==typeof o&&o);m.global!==m&&m.window!==m||(i=m);var g,v,b,w=String.fromCharCode,k={version:"2.1.2",encode:h,decode:d};r=function(){return k}.call(e,n,e,t),!(void 0!==r&&(t.exports=r))}(this)}).call(e,n(27)(t),function(){return this}())},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,u=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var p=new ArrayBuffer(a),h=new Uint8Array(p);for(e=0;e>4,h[u++]=(15&o)<<4|i>>2,h[u++]=(3&i)<<6|63&s;return p}}()},function(t,e){(function(e){function n(t){for(var e=0;e0);return e}function r(t){var e=0;for(p=0;p';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),p=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=p,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(u,"\\\n"),this.area.value=t.replace(c,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){(function(e){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=h&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=o),i.call(this,t)}var o,i=n(20),s=n(21),a=n(30),c=n(31),u=n(32),p=n(3)("engine.io-client:websocket"),h=e.WebSocket||e.MozWebSocket;if("undefined"==typeof window)try{o=n(35)}catch(f){}var l=h;l||"undefined"!=typeof window||(l=o),t.exports=r,c(r,i),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function n(){r.emit("flush"),setTimeout(function(){r.writable=!0,r.emit("drain")},0)}var r=this;this.writable=!1;for(var o=t.length,i=0,a=o;i0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])}); -//# sourceMappingURL=socket.io.js.map \ No newline at end of file diff --git a/src/plugins/vue-im/static/upload/.gitignore b/src/plugins/vue-im/static/upload/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/src/plugins/vue-im/static/upload/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/src/plugins/vue-im/wechat-client.js b/src/plugins/vue-im/wechat-client.js deleted file mode 100644 index 6ef909ee..00000000 --- a/src/plugins/vue-im/wechat-client.js +++ /dev/null @@ -1,141 +0,0 @@ -const io = require("socket.io-client"); - -const socket = io.connect("http://localhost:3001"); - -const serverChatId = 'xiaop' - -const configData = { - socket: socket, - chatInfoEn: { - chatState: 'agent', // chat状态;robot 机器人、agent 客服 - inputContent: '', // 输入框内容 - msgList: [], // 消息列表 - state: 'on', // 连接状态;on :在线;off:离线 - lastMsgShowTime: null // 最后一个消息的显示时间 - }, // 会话信息,包括聊天记录、状态 - clientChatEn: { - clientChatId: '123456789', - clientChatName: '张三', - avatarUrl: 'static/image/im_client_avatar.png' - }, // 当前账号的信息 - serverChatEn: { - serverChatName: '小P', - avatarUrl: 'static/image/im_robot_avatar.png' - }, // 服务端chat信息 - robotEn: { - robotName: '小旺', - avatarUrl: 'static/image/im_robot_avatar.png' - }, // 机器人信息 - faqList: [ - { title: '今天周几', content: '今天周一' }, - { title: '今天周几', content: '今天周二' }, - { title: '今天周几', content: '今天周三' }, - { title: '今天周几', content: '今天周四' }, - { title: '今天周几', content: '今天周五' } - ], - faqSelected: '-1', - inputContent_setTimeout: null, // 输入文字时在输入结束才修改具体内容 - selectionRange: null, // 输入框选中的区域 - shortcutMsgList: [], // 聊天区域的快捷回复列表 - logoutDialogVisible: false, // 结束会话显示 - transferDialogVisible: false, // 转接人工dialog - rateDialogVisible: false, // 评价dialog - leaveDialogVisible: false // 留言dialog -}; - -socket.on('connect', () => { - // 客户端上线 - socket.emit('CLIENT_ON', { - clientChatEn: configData.clientChatEn, - serverChatId: serverChatId - }); - - // 服务端链接 - socket.on('SERVER_CONNECTED', (data) => { - // 1)获取客服消息 - configData.serverChatEn = data.serverChatEn; - - // 2)添加消息 - addChatMsg({ - role: 'sys', - contentType: 'text', - content: '客服 ' + configData.serverChatEn.serverChatName + ' 为你服务' - }); - }); - - // 接受服务端信息 - socket.on('SERVER_SEND_MSG', (data) => { - console.debug(data) - if (data.msg && data.msg.role == 'server') { - data.msg.role = 'client' - sendMsg(data) - } - - // configData.msg.avatarUrl = data.serverChatEn.avatarUrl; - }); -}); - -/** - * 添加chat对象的msg - * @param {Object} msg 消息对象;eg:{role:'sys',content:'含有新的消息'} - * @param {String} msg.role 消息所有者身份;eg:'sys'系统消息; - * @param {String} msg.contentType 消息类型;text:文本(默认);image:图片 - * @param {String} msg.content 消息内容 - * @param {Function} successCallback 添加消息后的回调 - */ -function addChatMsg(msg, successCallback) { - // 1.设定默认值 - msg.role = msg.role == undefined ? 'sys' : msg.role; - msg.contentType = msg.contentType == undefined ? 'text' : msg.contentType; - msg.createTime = msg.createTime == undefined ? new Date() : msg.createTime; - - var msgList = configData.chatInfoEn.msgList ? configData.chatInfoEn.msgList : []; - - // 2.插入消息 - // 1)插入日期 - // 实际场景中,在消息上方是否显示时间是由后台传递给前台的消息中附加上的,可参考 微信Web版 - // 此处进行手动设置,5分钟之内的消息,只显示一次消息 - msg.createTime = new Date(msg.createTime); - if (configData.chatInfoEn.lastMsgShowTime == null || msg.createTime.getTime() - configData.chatInfoEn.lastMsgShowTime.getTime() > 1000 * 60 * 5) { - msgList.push({ - role: 'sys', - contentType: 'text', - content: '2022-5-30 20:00:00' - }); - configData.chatInfoEn.lastMsgShowTime = msg.createTime; - } - - // 2)插入消息 - msgList.push(msg); - - // 3.设置chat对象相关属性 - configData.chatInfoEn.msgList = msgList; - - // 4.回调 - successCallback && successCallback(); -} - -/** - * 发送消息 - * @param {Object} rs 回调对象 - */ -function sendMsg(rs) { - var msg = rs.msg; - msg.role = 'client'; - msg.avatarUrl = configData.clientChatEn.avatarUrl; - if (configData.chatInfoEn.chatState == 'robot') { - // 机器人发送接口 - } else if (configData.chatInfoEn.chatState == 'agent') { - // 客服接口 - configData.socket.emit('CLIENT_SEND_MSG', { - serverChatId: configData.serverChatEn.serverChatId, - clientChatEn: configData.clientChatEn, - msg: msg - }); - - console.debug(configData.serverChatEn.serverChatId) - } - // 2.添加到消息集合李 - addChatMsg(msg); -} - diff --git a/src/plugins/wxai.ts b/src/plugins/wxai.ts index 59b79d4f..95778f51 100644 --- a/src/plugins/wxai.ts +++ b/src/plugins/wxai.ts @@ -23,25 +23,15 @@ import { Wechaty, } from 'wechaty' -// import path from 'path' -// import os from 'os' - import { // waitForMs as wait, formatSentMessage, -} from '../util/tool.js' - -import { ChatGPTAPI } from 'chatgpt' +} from '../utils/utils.js' -const botTpyes = [ 'WxOpenai', 'ChatGPT' ] -const useBot = 0 -const callBot = botTpyes[useBot] +// import { ChatGPTAPI } from 'chatgpt' +import Api2d from 'api2d' +// import axios from 'axios' -const config = { - AutoReply: true, - MakeFriend: true, - ChatGPTSessionToken: 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..IMz6n01YT2ZrlL-c.ZRM6h_EhsvDTKBsfJv2l8pkCiQKaZ_-QdrvyJFUVsydnfs8QvgtxScpCfCzSNtPbo4SG9am5miwWQmseRyTjNoN3pNhGnWWWSc3FMNb1w9Ok_fbUokUf_H2YjcuAMqYpsb0YPieykFznAEiWwqdnpOHkvrxIVr2J71NTGzgBQ805oJXey92r-_btktR-uSaI5vhLQxoOBabSAcRCuEPG0k7_ChsaXd8p932UOzFeyAeh26xDock6-baLLYNbJ6nrQmnfx0nc-MjBEWU1wYXgfVqReeM_W_zRKM9rL0KsVg7GpuL5k_oiNUYpm1iEvbEfEFOhhK6zzR_j8awZ_qKEbVQRkuo9gH-OaLAEPib0kwTrwirbF2rOiiaLA9AE-nEQoQZ5CKkrRMGploFjx0sGXwmqrjNh1IzuVgf11NmHUCNYW-TweOdWo_3Wge1jjRUkamShFGYL478zK6Ve5BgyQZ3MZD5aAof6hWL8ELFu0THDio8cQUMQNP7RoBpQAFSLud1nyB4L5VB9BRafAClstO5Tn50o3obGtVY_mMl5WwFOTsofHutEiXhbP2JeuGAruwKdE5Ks8l5VEuv-36r7-5utIcvoBnJhXuyZaNq4xdyOf4rdXQDUNcI6wS9YR4AKOyCJKHiZ91RuxTR5Tx9Tz8JWCZAYzP3HJNh9ql6wjFSytqdDj2QbD1yDctlY4juzcp_4SiE0yJDrBkRgZ6u734FM_zJJeqGjpbn88rZ_ItXfJjGjXXP8ifSjZfSd4W1UuVxE8XH67BiRbd9d2m8nOPfnELhepIK14ICMnDhyZ-_m7j25I54yyj5ISKojC6noZMvh4KKjlqH7ASAKUBcGoMsk66L9D_6zymE7NrPTy9gRMsbVF5G6-YDAQ0FfWdA8b6jVFDLVvILprPoQzdWCUuY2XDLJ8-MBEEC_sGryLAR01DX3xuMPDBzA6cC8zUAqK-tZvJefL0s7Nv40ABdDhsUIa3EsNe1qJigw-53GeTSqkdO4QihyW3LTXX6QK7BDBA6IJJ5Ry5jYAvS-RvnjXE7hCqYygNuwXbmWZJZ4xLwYmc6iDDgrg8nxfwdq8i3WKFI4EA45afKLuzJTyUQzZAEoQonqVKReqAA1rCRLyuhbgzTi01TuOI6ncCVsOJR_mSdT3QDsZODhRDVjbDpBigyAMVpOEuIiSEwew4B3iP1dsudNoPpqGJKo3-8zwVHN9NZrlm0mnuFvcX5nRN4APl6n6TJAyQ-_dJS0ibi-bTuwlOGvXXxXaguSzgrNSyqVHetuOEp7k7bkOfTrkqEP0TH4xL5Hl5dqsv-KmbBHrj1bkfTd-2NKnBaSZyajflW2Kyx8MBnu9cEVeGNt6RpuITB9CE83ZEL-W99oEpURkCdc64x546PSNUnRuyDTphpfIHCFCqt5yoXg13i82x9EfF88ERdj1FDV-gSsOJoGB_hqI5gJkM3ch6qVDwye4pQAGMDaViTOesPLghlbjoCskQ-cTHHUPdiHfxZ3fW8bdhG1KanR5oJ6E00t7b9eKlfzzScmmr4fDqErV1FZX-F6EnaoqeoX5Caai7AE9TmPNu8XNNDR5k7pzHzpCErryQWHrSo6KSK_1cirncKNcGl0AeX2CwtgDolmPnvHcUmZT_aLW6dbiqmtX5ZWeVNoB6qpbR6d5zwMcSL-NNCqwj6q1CLakHpUgepka5n1Si64jb-9ZGZFmBDxlcYcK1qqMX_gA62ak3IEakGT_Rrp14-e4d4NrnlVNZsPVUVCLLF924hirlhO5vOXRdVlovZpJSpf9QG5kAEI7ZlxuLFHydODAE9c8XXywUNmJAf8BqMjrSujpjeM6hDpGcuO6rLEBURuYGslfQk_z1A6f94r3gel6LYH1iGS-_GJTyXhD3oVHm-PbGAiwHmBN9_IIQc-IKiDh3-cyrPy0UexXmJI79UHrBjuz2q9kwGDkT2X7zcpPSMEwa2BaAZvwOW_zCz54LRMOS-OXz4UsZXCb_vgZkrp6LQ3_eAHRzHro_eXuTtIQJRxmNRdqLSMICWRQZJFx7fk0eFPu9Zw4APT_HtWrcEioA1l_nEZfveebn6VSVQyb6nIUOJghyWvECK5agwCeHoJ4ma6nYThGDD06qszvkRJkVg1pob_GscwDbE15OhpjeYfP8lGVBIo6MVuqbMZSGZlZ8dbYG29gaxN0NC11MUkpCAwam7a18usjg0lL_sAr0WXo517LovIgzBT3KhqMZxL8RG3UaQP_vwWt4TkBvN7wWuudEd__70rgAk1gEbJ1fTuDsYeUJq93CKoN5Wc4o05jK2LfcBzIaNopxq5je38rWaECCLYHOlPlVy9eA.uVxf9FJ31LUx0p8hAp2wyA', -} // const __dirname = path.resolve() // const userInfo = os.userInfo() // const rootPath = `${userInfo.homedir}\\Documents\\WeChat Files\\` @@ -163,16 +153,17 @@ async function aibot (sysConfig: any, talker: any, room: any, query: any) { const wxid = talker.id const nickName = talker.name() const topic = await room?.topic() - // log.info(opt) + log.info('query:', query) const content = query + const callBot = sysConfig.botConfig.autoQa.type let answerJson switch (callBot) { - case 'WxOpenai': + case 'wxOpenai': // log.info('开始请求微信对话平台...') init({ - EncodingAESKey: sysConfig.EncodingAESKey, - TOKEN: sysConfig.WX_TOKEN, + EncodingAESKey: sysConfig.WXOPENAI_ENCODINGAESKEY, + TOKEN: sysConfig.WXOPENAI_TOKEN, }) try { @@ -258,23 +249,25 @@ async function aibot (sysConfig: any, talker: any, room: any, query: any) { log.error(JSON.stringify(err)) } break - case 'ChatGPT': + case 'chatGpt': try { - const api = new ChatGPTAPI({ sessionToken: config.ChatGPTSessionToken }) - // ensure the API is properly authenticated (optional) - await api.ensureAuth() - const t0 = new Date().getTime() - console.log('content: ', content) - // send a message and wait for the response - const response = await api.sendMessage(content) - // TODO: format response to compatible with wechat messages - const t1 = new Date().getTime() - console.log('response: ', response) - console.log('耗时: ', (t1 - t0) / 1000, 's') - // response is a markdown-formatted string + const timeout = 1000 * 60 + const api = new Api2d(sysConfig.botConfig.chatGpt.key, sysConfig.botConfig.chatGpt.endpoint, timeout) + const body = { + model: 'gpt-3.5-turbo', + messages: [ { role: 'user', content } ], + temperature: 1, + n: 1, + stream: false, + } + log.info('body:', JSON.stringify(body)) + const completion: any = await api.completion(body) + const responseMessage = completion + + log.info('responseMessage', responseMessage) answer = { messageType: types.Message.Text, - text: response, + text: responseMessage.choices[0].message.content, } } catch (err) { console.error(err) diff --git a/src/puppet-checker.ts b/src/puppet-checker.ts deleted file mode 100644 index 07a034bb..00000000 --- a/src/puppet-checker.ts +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env -S node --no-warnings --loader ts-node/esm -/* eslint-disable @typescript-eslint/no-unnecessary-condition */ -/* eslint-disable no-console */ -/* eslint-disable eqeqeq */ -/* eslint-disable camelcase */ -/* eslint-disable sort-keys */ -import 'dotenv/config.js' - -import { - Contact, - // Room, - Message, - ScanStatus, - WechatyBuilder, - log, - // types, -} from 'wechaty' - -import qrcodeTerminal from 'qrcode-terminal' -import WechatyVikaPlugin from './plugins/index.js' - -import configs from './config.js' -import { VikaBot } from './plugins/vika.js' - -let bot: any -let sysConfig: any - -if (process.env['VIKA_SPACENAME']) { - configs.VIKA_SPACENAME = process.env['VIKA_SPACENAME'] -} - -if (process.env['VIKA_TOKEN']) { - configs.VIKA_TOKEN = process.env['VIKA_TOKEN'] -} - -const vikaConfig = { - spaceName: configs.VIKA_SPACENAME, - token: configs.VIKA_TOKEN, -} -// console.debug(vikaConfig) -const vika = new VikaBot(vikaConfig) - -async function getConfig (vika: any) { - sysConfig = await vika.getConfig() - console.debug(sysConfig) - return sysConfig -} - -async function main () { - const isReady = await vika.checkInit('主程序载入系统配置成功,等待插件初始化...') - - if (!isReady) { - return - } - // 获取系统配置信息 - await getConfig(vika) - - const wechatyConfig: any = { - // 网页版微信 - 'wechaty-puppet-wechat': { - name: 'openai-qa-bot', - puppet: 'wechaty-puppet-wechat', - puppetOptions: { - uos: true, - }, - }, - // Windows桌面版微信 - 'wechaty-puppet-xp': { - name: 'openai-qa-bot', - puppet: 'wechaty-puppet-xp', - }, - // pad-local - 'wechaty-puppet-padlocal': { - name: 'openai-qa-bot', - puppet: 'wechaty-puppet-padlocal', - puppetOptions: { - token: sysConfig.puppetToken, - }, - }, - // wechaty-puppet-service - 'wechaty-puppet-service': { - name: 'openai-qa-bot', - puppet: 'wechaty-puppet-service', - puppetOptions: { - token: sysConfig.puppetToken, - }, - }, - } - - const ops = wechatyConfig[sysConfig.puppetName] - console.debug(ops) - - bot = WechatyBuilder.build(ops) - - async function onScan (qrcode: string, status: ScanStatus) { - console.debug(qrcode) - if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) { - const qrcodeUrl = encodeURIComponent(qrcode) - - const qrcodeImageUrl = [ - 'https://wechaty.js.org/qrcode/', - qrcodeUrl, - ].join('') - log.info('StarterBot', 'onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl) - - qrcodeTerminal.generate(qrcode, { small: true }) // show qrcode on console - - } else { - log.info('StarterBot', 'onScan: %s(%s)', ScanStatus[status], status) - } - } - - async function onLogin (user: Contact) { - log.info('StarterBot', '%s login', user.payload) - await user.say('上线:' + new Date().toLocaleString()) - log.info(JSON.stringify(user.payload)) - - console.log('================================================\n\n登录启动成功,程序准备就绪\n\n================================================\n') - } - - function onLogout (user: Contact) { - log.info('StarterBot', '%s logout', user) - } - - async function onMessage (message: Message) { - - log.info('onMessage', JSON.stringify(message)) - console.debug('onMessage', JSON.stringify(message)) - - } - - async function roomJoin (room: { topic: () => any; id: any; say: (arg0: string, arg1: any) => any }, inviteeList: any[], inviter: any) { - const nameList = inviteeList.map(c => c.name()).join(',') - log.info(`Room ${await room.topic()} got new member ${nameList}, invited by ${inviter}`) - } - - const missingConfiguration = [] - - for (const key in configs) { - if (!configs[key] && ![ 'imOpen', 'DIFF_REPLY_ONOFF' ].includes(key)) { - missingConfiguration.push(key) - } - } - - if (missingConfiguration.length === 0) { - bot.use( - WechatyVikaPlugin(vika), - ) - bot.on('scan', onScan) - bot.on('login', onLogin) - bot.on('logout', onLogout) - bot.on('message', onMessage) - bot.on('room-join', roomJoin) - - bot.start() - .then(() => log.info('Starter Bot Started.')) - .catch((e: any) => log.error(JSON.stringify(e))) - - } else { - log.error('\n======================================\n\n', `错误提示:\n缺少${missingConfiguration.join()}配置参数,请检查config.js文件\n\n======================================`) - log.info(configs) - } - -} - -void main() diff --git a/src/schemas/config.ts b/src/schemas/config.ts deleted file mode 100644 index 66c6b850..00000000 --- a/src/schemas/config.ts +++ /dev/null @@ -1,88 +0,0 @@ -type AppConfig = { - config: Record; - isOpen: boolean; - }; - - type AppConfigs = { - [appName: string]: AppConfig; - }; - - type BotConfig = { - adminRoomId: string; - adminRoomTopic: string; - apps: AppConfigs; - bot: { - puppet: string; - token: string; - VIKA_TOKEN?: string - VIKA_SPACENAME?: string - [key : string]:any - }; - command: { - contact: Record; - room: Record; - bot: Record; - }; - }; - - type ContactConfig = { - [contactId: string]: { - app: string; - apps: AppConfigs; - }; - }; - - type RoomConfig = { - [roomId: string]: { - app: string; - apps: AppConfigs; - }; - }; - - type Config = { - botConfig: BotConfig; - contactConfig: ContactConfig; - roomConfig: RoomConfig; - }; - - type AIType = 'WxOpenai' | string; - -interface SysConfig { - adminRoomTopic:string - welcomeList: string[]; - roomWhiteList: string[]; - contactWhiteList: string[]; - puppetName: string; - puppetToken: string; - aiType: AIType; - WX_TOKEN: string; - EncodingAESKey: string; - ChatGPTAEmail: string; - ChatGPTAPassword: string; - ChatGPTASessionToken: string; - mqttUsername: string; - mqttPassword: string; - mqttEndpoint: string; - mqttPort: string; - WEB_HOOK: string; - WX_OPENAI_ONOFF: boolean; - AT_AHEAD: boolean; - DIFF_REPLY_ONOFF: boolean; - roomWhiteListOpen: boolean; - contactWhiteListOpen: boolean; - VIKA_ONOFF: boolean; - WEB_HOOK_ONOFF: boolean; - mqtt_PUB_ONOFF: boolean; - mqtt_SUB_ONOFF: boolean; - imOpen: boolean; -} - -export { - type AppConfig, - type AppConfigs, - type BotConfig, - type ContactConfig, - type RoomConfig, - type Config, - type SysConfig, -} diff --git a/src/sdk/openai/example/index.d.ts b/src/sdk/openai/example/index.d.ts deleted file mode 100644 index 336ce12b..00000000 --- a/src/sdk/openai/example/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {} diff --git a/src/sdk/openai/example/index.js b/src/sdk/openai/example/index.js deleted file mode 100644 index dea56526..00000000 --- a/src/sdk/openai/example/index.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -var index_1 = require("../index"); -(0, index_1.init)({ - TOKEN: 'PWj9xdSdGU3PPnqUUrTf7uGgQ9Jvn7', - EncodingAESKey: '4jzHSI2p3EHXh3qBao5onJ39HcOO00ZoiGVNVvjFkPW' -}); -(0, index_1.chat)({ - username: "uid", - msg: "你好吗" -}).then(function (res) { - console.log('机器人返回:', res); -}, function (res) { - console.log('reject res:', res); -}).catch(function (e) { - console.log('error', e); -}); -// 分词 -index_1.nlp.tokenize({ - uid: "uid", - data: { - q: "我的家乡叫中国。" - } -}).then(function (res) { - console.log('词法分析返回:', res); -}, function (res) { - console.log('reject res:', res); -}).catch(function (e) { - console.log('error', e); -}); -// 数字日期时间识别 -index_1.nlp.ner({ - uid: "uid", - data: { - q: "帮我订两张后天上午的火车票" - } -}).then(function (res) { - console.log('数字日期时间识别返回:', res); -}, function (res) { - console.log('reject res:', res); -}).catch(function (e) { - console.log('error', e); -}); -// 情感分析 -index_1.nlp.sentiment({ - uid: "uid", - data: { - q: "恭喜小张脱单成功", - mode: "6class" - } -}).then(function (res) { - console.log('情感分析返回:', res); -}, function (res) { - console.log('reject res:', res); -}).catch(function (e) { - console.log('error', e); -}); -// 敏感词识别 -index_1.nlp.sensitive({ - uid: "uid", - data: { - q: "楼主真垃圾,祝你早日死全家" - } -}).then(function (res) { - console.log('敏感词识别返回:', res); -}, function (res) { - console.log('reject res:', res); -}).catch(function (e) { - console.log('error', e); -}); diff --git a/src/services/config.ts b/src/services/config.ts new file mode 100644 index 00000000..fc2aa0c7 --- /dev/null +++ b/src/services/config.ts @@ -0,0 +1,128 @@ +/* eslint-disable sort-keys */ +import type { configTypes } from '../types/mod.js' +import { EnvironmentVariables } from '../types/mod.js' + +const config: configTypes.Config = { + botInfo: {}, + functionOnStatus: { + autoQa: { + autoReply: process.env[EnvironmentVariables.AUTOQA_AUTOREPLY] === 'true', + atReply: process.env[EnvironmentVariables.AUTOQA_ATREPLY] === 'true', + customReply: process.env[EnvironmentVariables.AUTOQA_CUSTOMREPLY] === 'true', + roomWhitelist: process.env[EnvironmentVariables.AUTOQA_ROOMWHITELIST] === 'true' || true, + contactWhitelist: process.env[EnvironmentVariables.AUTOQA_CONTACTWHITELIST] === 'true' || true, + }, + vika: { + useVika: process.env[EnvironmentVariables.VIKA_USEVIKA] === 'true', + uploadMessageToVika: process.env[EnvironmentVariables.VIKA_UPLOADMESSAGETOVIKA] === 'true', + autoMaticCloud: process.env[EnvironmentVariables.VIKA_AUTOMATICCLOUD] === 'true', + }, + webHook: { + webhookMessagePush: process.env[EnvironmentVariables.WEBHOOK_WEBHOOKMESSAGEPUSH] === 'true', + }, + mqtt: { + mqttMessagePush: process.env[EnvironmentVariables.MQTT_MQTTMESSAGEPUSH] === 'true', + mqttControl: process.env[EnvironmentVariables.MQTT_MQTTCONTROL] === 'true' || true, + }, + im: { + imChat: process.env[EnvironmentVariables.IM_IMCHAT] === 'true', + }, + }, + botConfig: { + base: { + welcomeMessageForJoinRoom: process.env[EnvironmentVariables.BASE_WELCOMEMESSAGEFORJOINROOM] || '', + welcomeMessageForAddFriend: process.env[EnvironmentVariables.BASE_WELCOMEMESSAGEFORADDFRIEND] || '', + }, + wechaty: { + puppet: process.env[EnvironmentVariables.WECHATY_PUPPET] || 'wechaty-puppet-wechat', + token: process.env[EnvironmentVariables.WECHATY_TOKEN] || '', + }, + vika: { + spaceName: process.env[EnvironmentVariables.VIKA_SPACE_NAME] || '', + token: process.env[EnvironmentVariables.VIKA_TOKEN] || '', + }, + adminRoom: { + adminRoomId: process.env[EnvironmentVariables.ADMINROOM_ADMINROOMID] || '', + adminRoomTopic: process.env[EnvironmentVariables.ADMINROOM_ADMINROOMTOPIC] || '', + }, + autoQa: { + type: process.env[EnvironmentVariables.AUTOQA_TYPE] || '', + }, + wxOpenAi: { + token: process.env[EnvironmentVariables.WXOPENAI_TOKEN] || '', + encodingAesKey: process.env[EnvironmentVariables.WXOPENAI_ENCODINGAESKEY] || '', + }, + chatGpt: { + key: process.env[EnvironmentVariables.CHATGPT_KEY] || '', + endpoint: process.env[EnvironmentVariables.CHATGPT_ENDPOINT] || '', + }, + mqtt: { + username: process.env[EnvironmentVariables.MQTT_USERNAME] || '', + password: process.env[EnvironmentVariables.MQTT_PASSWORD] || '', + endpoint: process.env[EnvironmentVariables.MQTT_ENDPOINT] || '', + port: parseInt(process.env[EnvironmentVariables.MQTT_PORT] || '1883', 10), + }, + webHook: { + url: process.env[EnvironmentVariables.WEBHOOK_URL] || '', + token: process.env[EnvironmentVariables.WEBHOOK_TOKEN] || '', + username: process.env[EnvironmentVariables.WEBHOOK_USERNAME] || '', + password: process.env[EnvironmentVariables.WEBHOOK_PASSWORD] || '', + }, + yuQue: { + token: process.env[EnvironmentVariables.YUQUE_TOKEN] || '', + nameSpace: process.env[EnvironmentVariables.YUQUE_NAMESPACE] || '', + }, + }, + apps: { + riding: { + config: {}, + isOpen: true, + }, + }, + command: { + bot: { + reboot: '#重启机器人', + selfInfo: '#机器人信息', + }, + contact: { + findall: '#联系人列表', + }, + room: { + findall: '#群列表', + }, + }, + welcomeList: [], + roomWhiteList: [], + contactWhiteList: [], + contactConfig: { + tyutluyc: { + app: 'waiting', + apps: { + qa: { + config: {}, + isOpen: true, + }, + riding: { + config: {}, + isOpen: true, + }, + }, + }, + tyutluyc2: { + app: 'waiting', + apps: { + qa: { + config: {}, + isOpen: true, + }, + riding: { + config: {}, + isOpen: true, + }, + }, + }, + }, + roomConfig: {}, +} + +export { config } diff --git a/src/types/config.ts b/src/types/config.ts new file mode 100644 index 00000000..4f46df36 --- /dev/null +++ b/src/types/config.ts @@ -0,0 +1,222 @@ +type AIType = 'WxOpenai' | string; + +interface SysConfig { + adminRoomTopic: string; + welcomeList: string[]; + roomWhiteList: string[]; + contactWhiteList: string[]; + puppetName: string; + puppetToken: string; + aiType: AIType; + WX_TOKEN: string; + EncodingAESKey: string; + ChatGPTAEmail: string; + ChatGPTAPassword: string; + ChatGPTASessionToken: string; + mqttUsername: string; + mqttPassword: string; + mqttEndpoint: string; + mqttPort: string; + WEB_HOOK: string; + WX_OPENAI_ONOFF: boolean; + AT_AHEAD: boolean; + DIFF_REPLY_ONOFF: boolean; + roomWhiteListOpen: boolean; + contactWhiteListOpen: boolean; + VIKA_ONOFF: boolean; + WEB_HOOK_ONOFF: boolean; + mqtt_PUB_ONOFF: boolean; + mqtt_SUB_ONOFF: boolean; + imOpen: boolean; +} + +interface BotInfo { + alias: string; + avatar: string; + city: string; + friend: boolean; + gender: number; + id: string; + name: string; + phone: any[]; + province: string; + signature: string; + type: number; + weixin: string; +} + +interface AutoQa { + autoReply: boolean; + atReply: boolean; + customReply: boolean; + roomWhitelist: boolean; + contactWhitelist: boolean; +} + +interface Vika { + useVika: boolean; + uploadMessageToVika: boolean; + autoMaticCloud: boolean; +} + +interface WebHook { + webhookMessagePush: boolean; +} + +interface Mqtt { + mqttMessagePush: boolean; + mqttControl: boolean; +} + +interface Im { + imChat: boolean; +} + +interface FunctionOnStatus { + autoQa: AutoQa; + vika: Vika; + webHook: WebHook; + mqtt: Mqtt; + im: Im; +} + +interface WechatyConfig { + puppet: string; + token: string | ''; +} + +interface VikaConfig { + spaceName?: string; + token: string; +} + +interface AdminRoomConfig { + adminRoomId: string; + adminRoomTopic: string; +} + +interface AutoQaConfig { + type?: string; +} + +interface WxOpenAiConfig { + token?: string; + encodingAesKey?: string; +} + +interface ChatGptConfig { + key?: string; + endpoint?: string; +} + +interface MqttConfig { + username?: string; + password?: string; + endpoint?: string; + port: number; +} + +interface WebHookConfig { + url?: string; + token?: string; + username?: string; + password?: string; +} + +interface YuQueConfig { + token?: string; + nameSpace?: string; +} + +interface RidingAppConfig { + config: any; + isOpen: boolean; +} + +interface AppsConfig { + riding: RidingAppConfig; +} + +interface BotCommandConfig { + reboot: string; + selfInfo: string; +} + +interface ContactCommandConfig { + findall: string; +} + +interface RoomCommandConfig { + findall: string; +} + +interface CommandConfig { + bot: BotCommandConfig; + contact: ContactCommandConfig; + room: RoomCommandConfig; +} + +interface BaseConfig { + [key: string]: string | undefined; +} + +interface BotConfig { + base?: BaseConfig; + wechaty: WechatyConfig; + vika: VikaConfig; + adminRoom: AdminRoomConfig; + autoQa: AutoQaConfig; + wxOpenAi: WxOpenAiConfig; + chatGpt: ChatGptConfig; + mqtt: MqttConfig; + webHook: WebHookConfig; + yuQue: YuQueConfig; +} + +interface ContactAppsConfig { + qa: RidingAppConfig; + riding: RidingAppConfig; +} + +interface ContactConfig { + [key: string]: { + app: string; + apps: ContactAppsConfig; + }; +} + +interface RoomAppsConfig { + qa: RidingAppConfig; + riding: RidingAppConfig; + riding2: RidingAppConfig; +} + +interface RoomConfig { + [key: string]: { + app: string; + apps: RoomAppsConfig; + }; +} + +interface Config { + botInfo: BotInfo | {}; + functionOnStatus: FunctionOnStatus; + botConfig: BotConfig; + welcomeList?: string[]; + roomWhiteList: string[]; + contactWhiteList: string[]; + contactConfig: ContactConfig; + roomConfig: RoomConfig; + apps?: AppsConfig; + command?: CommandConfig; +} + +export type { + VikaConfig, + BotInfo, + BotConfig, + ContactConfig, + RoomConfig, + SysConfig, + Config, +} diff --git a/src/types/env.ts b/src/types/env.ts new file mode 100644 index 00000000..f1010551 --- /dev/null +++ b/src/types/env.ts @@ -0,0 +1,37 @@ +export enum EnvironmentVariables { + BASE_WELCOMEMESSAGEFORJOINROOM = 'BASE_WELCOMEMESSAGEFORJOINROOM', + BASE_WELCOMEMESSAGEFORADDFRIEND = 'BASE_WELCOMEMESSAGEFORADDFRIEND', + WECHATY_PUPPET = 'WECHATY_PUPPET', + WECHATY_TOKEN = 'WECHATY_TOKEN', + VIKA_SPACE_NAME = 'VIKA_SPACE_NAME', + VIKA_TOKEN = 'VIKA_TOKEN', + ADMINROOM_ADMINROOMID = 'ADMINROOM_ADMINROOMID', + ADMINROOM_ADMINROOMTOPIC = 'ADMINROOM_ADMINROOMTOPIC', + AUTOQA_TYPE = 'AUTOQA_TYPE', + WXOPENAI_TOKEN = 'WXOPENAI_TOKEN', + WXOPENAI_ENCODINGAESKEY = 'WXOPENAI_ENCODINGAESKEY', + CHATGPT_KEY = 'CHATGPT_KEY', + CHATGPT_ENDPOINT = 'CHATGPT_ENDPOINT', + MQTT_USERNAME = 'MQTT_USERNAME', + MQTT_PASSWORD = 'MQTT_PASSWORD', + MQTT_ENDPOINT = 'MQTT_ENDPOINT', + MQTT_PORT = 'MQTT_PORT', + WEBHOOK_URL = 'WEBHOOK_URL', + WEBHOOK_TOKEN = 'WEBHOOK_TOKEN', + WEBHOOK_USERNAME = 'WEBHOOK_USERNAME', + WEBHOOK_PASSWORD = 'WEBHOOK_PASSWORD', + YUQUE_TOKEN = 'YUQUE_TOKEN', + YUQUE_NAMESPACE = 'YUQUE_NAMESPACE', + AUTOQA_AUTOREPLY = 'AUTOQA_AUTOREPLY', + AUTOQA_ATREPLY = 'AUTOQA_ATREPLY', + AUTOQA_CUSTOMREPLY = 'AUTOQA_CUSTOMREPLY', + AUTOQA_ROOMWHITELIST = 'AUTOQA_ROOMWHITELIST', + AUTOQA_CONTACTWHITELIST = 'AUTOQA_CONTACTWHITELIST', + VIKA_USEVIKA = 'VIKA_USEVIKA', + VIKA_UPLOADMESSAGETOVIKA = 'VIKA_UPLOADMESSAGETOVIKA', + VIKA_AUTOMATICCLOUD = 'VIKA_AUTOMATICCLOUD', + WEBHOOK_WEBHOOKMESSAGEPUSH = 'WEBHOOK_WEBHOOKMESSAGEPUSH', + MQTT_MQTTMESSAGEPUSH = 'MQTT_MQTTMESSAGEPUSH', + MQTT_MQTTCONTROL = 'MQTT_MQTTCONTROL', + IM_IMCHAT = 'IM_IMCHAT', + } diff --git a/src/types/interface.ts b/src/types/interface.ts new file mode 100644 index 00000000..59bef53b --- /dev/null +++ b/src/types/interface.ts @@ -0,0 +1,24 @@ +export interface Plugin { + init(app: any): void; + enable(app: any): void; + disable(app: any): void; + } + +enum PluginType { + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6 +} + +export interface PluginConfig { + name: string; + active: boolean; + type: PluginType; + room?: any; + contact?: any; + params?:any; +} diff --git a/src/schemas/mod.ts b/src/types/mod.ts similarity index 56% rename from src/schemas/mod.ts rename to src/types/mod.ts index 8bb58f9a..46e802e1 100644 --- a/src/schemas/mod.ts +++ b/src/types/mod.ts @@ -1,16 +1,20 @@ import type { - AppConfig, - AppConfigs, BotConfig, + BotInfo, ContactConfig, RoomConfig, Config, SysConfig, + VikaConfig, } from './config.js' +export { EnvironmentVariables } from './env.js' + +export * as configTypes from './config.js' + export { - type AppConfig, - type AppConfigs, + type VikaConfig, + type BotInfo, type BotConfig, type ContactConfig, type RoomConfig, diff --git a/src/util/tool.ts b/src/util/tool.ts deleted file mode 100644 index 2bdc9bd6..00000000 --- a/src/util/tool.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable sort-keys */ -import moment from 'moment' - -import type { - Contact, - Room, - // ScanStatus, - // WechatyBuilder, -} from 'wechaty' - -async function formatSentMessage (userSelf: Contact, text: string, talker: Contact|undefined, room: Room|undefined) { - // console.debug('发送的消息:', text) - const curTime = new Date().getTime() - const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss') - const record = { - fields: { - timeHms, - name: userSelf.name(), - topic: room ? (await room.topic() || '--') : (talker?.name() || '--'), - messagePayload: text, - wxid: room && talker ? (talker.id !== 'null' ? talker.id : '--') : userSelf.id, - roomid: room ? (room.id || '--') : (talker?.id || '--'), - messageType: 'selfSent', - }, - } - return record -} - -// 定义一个延时方法 -const waitForMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) - -export { - waitForMs, - formatSentMessage, -} - -export default waitForMs diff --git a/src/utils/mod.ts b/src/utils/mod.ts new file mode 100644 index 00000000..84814912 --- /dev/null +++ b/src/utils/mod.ts @@ -0,0 +1 @@ +export * from './utils.js' diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 00000000..4d69bb7a --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,110 @@ +/* eslint-disable sort-keys */ +import moment from 'moment' + +import type { + Contact, + Room, + // ScanStatus, + // WechatyBuilder, +} from 'wechaty' + +import type { TaskConfig } from '../plugins/mod.js' + +async function formatSentMessage (userSelf: Contact, text: string, talker: Contact|undefined, room: Room|undefined) { + // console.debug('发送的消息:', text) + const curTime = new Date().getTime() + const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss') + const record = { + fields: { + timeHms, + name: userSelf.name(), + topic: room ? (await room.topic() || '--') : (talker?.name() || '--'), + messagePayload: text, + wxid: room && talker ? (talker.id !== 'null' ? talker.id : '--') : userSelf.id, + roomid: room ? (room.id || '--') : (talker?.id || '--'), + messageType: 'selfSent', + }, + } + return record +} + +// 定义一个延时方法 +const waitForMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +function getNow () { + return new Date().toLocaleString() +} + +const getRule = (task:TaskConfig) => { + const curTimeF = new Date(task.time) + // const curTimeF = new Date(task.time+8*60*60*1000) + let curRule = '* * * * * *' + let dayOfWeek: any = '*' + let month: any = '*' + let dayOfMonth: any = '*' + let hour: any = curTimeF.getHours() + let minute: any = curTimeF.getMinutes() + const second = 0 + const addMonth = [] + switch (task.cycle) { + case '每季度': + month = curTimeF.getMonth() + for (let i = 0; i < 4; i++) { + if (month + 3 <= 11) { + addMonth.push(month) + } else { + addMonth.push(month - 9) + } + month = month + 3 + } + month = addMonth + break + case '每天': + break + case '每周': + dayOfWeek = curTimeF.getDay() + break + case '每月': + month = curTimeF.getMonth() + break + case '每小时': + hour = '*' + break + case '每30分钟': + hour = '*' + minute = [ 0, 30 ] + break + case '每15分钟': + hour = '*' + minute = [ 0, 15, 30, 45 ] + break + case '每10分钟': + hour = '*' + minute = [ 0, 10, 20, 30, 40, 50 ] + break + case '每5分钟': + hour = '*' + minute = [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 ] + break + case '每分钟': + hour = '*' + minute = '*' + break + default: + month = curTimeF.getMonth() + dayOfMonth = curTimeF.getDate() + break + + } + curRule = `${second} ${minute} ${hour} ${dayOfMonth} ${month} ${dayOfWeek}` + return curRule +} + +export { + getNow, + waitForMs, + formatSentMessage, + getRule, +} + +export default waitForMs diff --git a/tests/test.ts b/tests/test.ts new file mode 100644 index 00000000..2f914d9d --- /dev/null +++ b/tests/test.ts @@ -0,0 +1,49 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import 'dotenv/config.js' +// import fs from 'fs' +import { + log, + // Room, + WechatyBuilder, +} from 'wechaty' +import { config } from '../src/services/config.js' + +// import { StoreByVika } from './plugins/basic-data-storage-for-vika.js' +import { ChatFlow } from '../src/chatflow.js' + +// 配置机器人 +function getBotOps (puppet:string, token:string) { + const ops:any = { + name: 'chatflow', + puppet, + puppetOptions: { + token, + }, + } + + if (puppet === 'wechaty-puppet-service') { + process.env['WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT'] = 'true' + } + + if ([ 'wechaty-puppet-wechat4u', 'wechaty-puppet-xp', 'wechaty-puppet-engine' ].includes(puppet)) { + delete ops.puppetOptions.token + } + + if (puppet === 'wechaty-puppet-wechat') { + delete ops.puppetOptions.token + ops.puppetOptions.uos = true + } + + log.info('bot ops:', JSON.stringify(ops)) + return ops +} + +// 构建机器人 +const ops = getBotOps(config.botConfig.wechaty.puppet, config.botConfig.wechaty.token) +const bot = WechatyBuilder.build(ops) + +bot.use(ChatFlow(config)) + +bot.start() + .then(() => log.info('Starter Bot Started.')) + .catch((e: any) => log.error('bot运行异常:', JSON.stringify(e))) diff --git a/tsconfig.json b/tsconfig.json index 7f8219e1..403f0106 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,20 @@ { "extends": "@chatie/tsconfig", "compilerOptions": { - "outDir": "dist", + "outDir": "dist/esm" }, "exclude": [ "node_modules/", - "dist/", - "tests/fixtures/", + "dist/" ], "include": [ "./*.ts", - "app/**/*.ts", - "bin/*.ts", - "bot/**/*.ts", - "examples/**/*.ts", - "scripts/**/*.ts", "src/**/*.ts", - "tests/**/*.spec.ts", - "src/wx-open-ai.js", - "src/plugins/msg-format.js", - "src/db/*.js" + "src/*.ts", + "src/plugins/*.ts", + "src/plugins/*.cjs", + "example/*.ts", + ".eslintrc.cjs", + "tests/*.ts" ] } \ No newline at end of file