backup: 2025-03-05

This commit is contained in:
Li Wei 2025-03-05 14:15:40 +08:00
parent 71b1ef4c19
commit b99b645a6e
9233 changed files with 0 additions and 688048 deletions

View File

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/chatflow/node_modules/gifwrap/templates" />
</list>
</option>
</component>
</module>

View File

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="enabledOnReformat" value="true" />
<option name="enabledOnSave" value="true" />
<option name="sdkUUID" value="6dbc488c-e03a-4668-96c8-77b5e236011d" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
</project>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/WechatBot.iml" filepath="$PROJECT_DIR$/.idea/WechatBot.iml" />
</modules>
</component>
</project>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -1,8 +0,0 @@
const rules = {
}
module.exports = {
extends: '@chatie',
rules,
}

View File

@ -1,37 +0,0 @@
# docker-image.yml
name: Publish Docker image # workflow名称可以在Github项目主页的【Actions】中看到所有的workflow
on: # 配置触发workflow的事件
push:
tags: # tag更新时触发此workflow
- '*'
jobs: # workflow中的job
push_to_registry: # job的名字
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest # job运行的基础环境
steps: # 一个job由一个或多个step组成
- name: Check out the repo
uses: actions/checkout@v2 # 官方的action获取代码
- name: Log in to Docker Hub
uses: docker/login-action@v1 # 三方的action操作 执行docker login
with:
username: ${{ secrets.DOCKERHUB_USERNAME }} # 配置dockerhub的认证在Github项目主页 【Settings】 -> 【Secrets】 添加对应变量
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3 # 抽取项目信息主要是镜像的tag
with:
images: atorber/wechat-openai-qa-bot
- name: Build and push Docker image
uses: docker/build-push-action@v2 # docker build & push
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,126 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
dev.ts*
# config.js*
.DS_Store
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
dev-config.js
dev-index.js
WechatEveryDay.memory-card.json
dev-index.ts
index-dev.ts
config-dev.js
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

View File

@ -1,5 +0,0 @@
{
"cSpell.words": [
"Aibot"
]
}

View File

@ -1,8 +0,0 @@
FROM node:16
WORKDIR /usr/src/app
COPY package.json ./
RUN npm install
COPY . .
CMD [ "npm","run", "init" ]
CMD [ "npm","run", "start" ]

View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,216 +0,0 @@
# wechat-qa-bot
[访问项目语雀文档了解更多信息](https://www.yuque.com/atorber/oegota)
## 简介
本项目使用wechat机器人快速实现一个免费的QA问答系统如果你是一个社群工作者、拼团团长、业务群运营经理使用这个项目可以帮助你解决一些重复性问答。
同时,本项目也具备微信消息收集、定时通知等常用场景功能。
已适配网页版微信linux、mac、Windows均可运行。
### 功能列表
[详细功能查看](https://www.yuque.com/atorber/oegota/aialc7sbyb4ldmg4/edit)
|功能|描述|
|--|--|
|消息存档|群聊天消息存档到表格基于vika维格表免费|
|定时消息|定时消息发送,支持单次定时和周期消息发送给指定好友或群|
|智能问答|可以自定义问答内容,智能匹配答案,支持相似问题匹配,例如“什么时候到货?”“亲,几时到货”“亲,什么时候到货”均能匹配(基于微信对话开放平台,免费)|
|千群千面|多个群相同问题不同回答内容,例如“何时到货?”,A群中回答“今天到”B群中回答“明天到货”|
|群白名单|支持配置群白名单,白名单内群开启机器人问答,未配置问题答案的群不会受到机器人干扰|
|客服后台|简单客服后台,可以把群内消息按发言人列表区分|
|MQTT消息推送|支持配置一个MQTTQ消息队列将消息推送到队列当中|
|远程控制发消息|支持通过MQTT控制机器人向指定好友或群发消息|
|非群主链接检测|支持非群主小程序卡片、网页链接分享检测,自动提醒、警告发送者撤回|
|团购订单转换|支持快团团订货单转换,原始表发送到群即可自动转换为按楼栋统计表|
## 快速开始
[手把手教程](https://www.yuque.com/atorber/oegota/zm4ulnwnqp9whmd6)
1.下载源码并安装依赖
```Shell
git clone <https://github.com/choogoo/wechat-openai-qa-bot.git>
cd ./wechat-openai-qa-bot
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)
4.修改./config.js配置文件
快速开始仅需要修改VIKA_TOKEN、VIKA_SPACENAME配置项,其他配置项暂时无需修改
```javascript
/* eslint-disable sort-keys */
// 配置文件,所有配置必须齐全,补充空白配置项,其他配置项可按需要修改
const configs = {
VIKA_TOKEN: '替换成自己的维格表token', // VIKA维格表token
VIKA_SPACENAME: '替换成你的维格表空间名称', // VIKA维格表空间名称修改为自己的空间名称
}
export default configs
```
> 只有加入到roomWhiteList里的群才会开启只能问答机器人
5.初始化系统表,先运行,系统会自动在维格表中创建好初始化表格
```Shell
npm run sys-init
```
在维格表查看系统表是否创建成功
6.程序默认使用wechaty-puppet-wechat三大系统均可使用
7.启动程序
```Shell
npm start
```
出现二维码之后,扫码二维码登陆微信
8.开启智能问答功能
8.1 设置微信对话平台token填写"环境变量"表中的 【对话平台token】、【对话平台EncodingAESKey】并在"功能开关"表中开启智能问答
添加一个简单问题到微信对话开放平台,测试对应群内智能问答内容
8.2 如果不希望每个群都开启智能问答,需设置群白名单,首先需要将上图中的群白名单开关设置为开启
然后将群加入到问答白名单在“群白名单”表中加入需要开启的群IDroomid群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
```
## 在Docker中部署运行
注意因为wechaty-puppet-xp必须依赖Windows微信客户端所以不能使用Docker但使用wechaty-puppet-padlocal、wechaty-puppet-service则可以用Doker来部署
最新代码已经默认wechaty-puppet-wehcat为初始化puppetmac、linux系统直接拉取镜像即可运行mac M1需要自行打包镜像
### Wechaty-Puppet支持
|puppet名称|支持平台 |需要token |付费| 备注|
|--|--|--|--|--|
|wechaty-puppet-wechat| Windows、Linux、macOS |否| 否 |网页版wechat无法获取真实的微信ID和群ID重启之后ID可能会变|
|wechaty-puppet-xp|Windows| 否| 否 |仅支持windows|
|wechaty-puppet-padlocal👍| Windows、Linux、macOS| 是 |是 |
|wechaty-puppet-service👍| Windows、Linux、macOS| 是 |是 |企业微信|
> 特别注意Wechaty-Puppet是wechaty的概念本项目不涉及机器人开发只是使用wechaty项目进行业务功能实现什么是[Wechaty](https://wechaty.js.org/)请点击链接进行了解学习
### 拉取和运行
- 稳定版本
```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="维格表空间名称"
atorber/wechat-openai-qa-bot:latest
```
## 视频演示及使用教程
到项目官网 [查看视频教程](https://qabot.vlist.cc/)
## 常见问题及解决方案
1. 加入QQ群 583830241 在线交流,添加 ledongmao 微信
2. 到 [项目语雀知识库](https://www.yuque.com/atorber/oegota/ibnui5v8mob11d70) 查看常用问题
3. 提交一个issues <https://github.com/choogoo/wechat-openai-qa-bot/issues>
## 效果展示
去 [效果展示图文](https://www.yuque.com/atorber/oegota/tbsokg3pqu5vk50y) 查看
## 二次开发
此项目只是提供了一个简单的使用微信机器人和智能对话平台实现的QA系统。如果有兴趣可以继续学习微信对话开放平台的高级技能实现诸如连续问答等高级功能欢迎贡献你的创意。
此外要说明的是项目中使用puppet-xp完全是出于免费的考虑如果不考虑这一点的话wechaty还有更好用的puppet对于有能力的开发者来说可以根据实际情况替换。
### TODO LIST
- 消息群发,通知消息同时发布到多个群
- 消息转发,按设定规则转发消息
- 使用VIKA托管配置文件
### 相关依赖
项目用到了一些免费且好用的开源项目和平台
> 如果你是团长可忽略此段内容,开发者可进一步了解
- [Wechaty](https://wechaty.js.org/)
只需几行代码,您就可以拥有一个功能齐全的聊天机器人
- [wechaty-puppet-xp](https://github.com/wechaty/puppet-xp)
可能是目前最好用的免费wechat机器人
- [wechaty-puppet-wechat](https://github.com/wechaty/puppet-wechat)
目前最简单的免费wechat机器人
- [微信对话开放平台](https://openai.weixin.qq.com/)
5分钟零基础免费一键搭建智能对话机器人并应用于微信公众号、小程序、企业网站、APP等
- [vika维格表](https://spcp52tvpjhxm.com.vika.cn/?inviteCode=55152973)
将过去复杂的IT数据库技术做得像表格一样简单(如果要注册,通过这个链接,或者使用邀请码 55152973 )
- [vue-im](https://github.com/polk6/vue-im)
由@polk6开源的客服web项目实现客服后台回复咨询消息

View File

@ -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

View File

View File

@ -1 +0,0 @@
qabot.vlist.cc

View File

@ -1,429 +0,0 @@
## 简介
> 最新文档已迁移至语雀,请移步 https://www.yuque.com/atorber/oegota
本项目使用wechat机器人快速实现一个免费的QA问答系统如果你是一个社群工作者、拼团团长、业务群运营经理使用这个项目可以帮助你解决一些重复性问答。
乐大喜奔已适配网页版微信linux、mac、Windows均可运行。
### 功能列表
|功能|描述|
|--|--|
| 智能问答|可以自定义问答内容,智能匹配答案,支持相似问题匹配,例如“什么时候到货?”“亲,几时到货”“亲,什么时候到货”均能匹配(基于微信对话开放平台,免费)|
|千群千面|多个群相同问题不同回答内容,例如“何时到货?”,A群中回答“今天到”B群中回答“明天到货”|
|免打扰|使用“QA+群ID+回答内容”匹配群,未配置问题答案的群不会受到机器人干扰|
|非群主链接检测|支持非群主小程序卡片、网页链接分享检测,自动提醒、警告发送者撤回|
|团购订单转换|支持快团团订货单转换,原始表发送到群即可自动转换为按楼栋统计表|
|消息存档|群聊天消息存档到表格基于vika维格表免费|
|客服后台|简单客服后台,可以把群内消息按发言人列表区分|
### TODO LIST
- 消息群发,通知消息同时发布到多个群
- 消息转发,按设定规则转发消息
- 使用VIKA托管配置文件
### 相关依赖
项目用到了一些免费且好用的开源项目和平台
> 如果你是团长可忽略此段内容,开发者可进一步了解
- [Wechaty](https://wechaty.js.org/) —— 只需几行代码,您就可以拥有一个功能齐全的聊天机器人
- [wechaty-puppet-xp](https://github.com/wechaty/puppet-xp) —— 可能是目前最好用的免费wechat机器人
- [wechaty-puppet-wechat](https://github.com/wechaty/puppet-wechat) —— 目前最简单的免费wechat机器人
- [微信对话开放平台](https://openai.weixin.qq.com/) —— 5分钟零基础免费一键搭建智能对话机器人并应用于微信公众号、小程序、企业网站、APP等
- [vika维格表](https://spcp52tvpjhxm.com.vika.cn/?inviteCode=55152973) —— 将过去复杂的IT数据库技术做得像表格一样简单(如果要注册,通过这个链接,或者使用邀请码 55152973 )
- [vue-im](https://github.com/polk6/vue-im) —— 由@polk6开源的客服web项目实现客服后台回复咨询消息
### 视频演示
#### 部署运行
<div style="position:relative; padding-bottom:75%; width:100%; height:0">
<iframe src="//player.bilibili.com/player.html?aid=853865811&bvid=BV1Y54y1f7v1&cid=714379422&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position:absolute; height: 100%; width: 100%;"></iframe>
</div>
#### 功能演示
<div style="position:relative; padding-bottom:75%; width:100%; height:0">
<iframe src="//player.bilibili.com/player.html?aid=511574788&bvid=BV1Ju41167Qo&cid=721650219&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position:absolute; height: 100%; width: 100%;"></iframe>
</div>
#### 客服系统
<div style="position:relative; padding-bottom:75%; width:100%; height:0">
<iframe src="//player.bilibili.com/player.html?aid=639343626&bvid=BV1CY4y167YD&cid=726317086&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position:absolute; height: 100%; width: 100%;"></iframe>
</div>
## 快速开始
1. 下载源码并安装依赖
```
git clone https://github.com/choogoo/wechat-openai-qa-bot.git
cd ./wechat-openai-qa-bot
npm install
```
2. 分别登陆[微信对话开放平台](https://openai.weixin.qq.com/)和[vika维格表](https://spcp52tvpjhxm.com.vika.cn/?inviteCode=55152973)官网注册账号并获取token
3. 在电脑上登陆微信,微信版本必须为[WeChatSetup-v3.3.0.115.exe](https://github.com/wechaty/wechaty-puppet-xp/releases/download/v0.5/WeChatSetup-v3.3.0.115.exe)
4. 修改./config.js配置文件
快速开始仅需要修改VIKA_TOKEN、VIKA_SPACENAME配置项,其他配置项暂时无需修改
```
/* eslint-disable sort-keys */
// 配置文件,所有配置必须齐全,补充空白配置项,其他配置项可按需要修改
const configs = {
VIKA_TOKEN: '替换成自己的维格表token', // VIKA维格表token
VIKA_SPACENAME: '替换成你的维格表空间名称', // VIKA维格表空间名称修改为自己的空间名称
}
export default configs
```
> 只有加入到roomWhiteList里的群才会开启只能问答机器人
5. 初始化系统表,先运行,系统会自动在维格表中创建好初始化表格
```
npm run sys-init
```
<img width="817" alt="image" src="https://user-images.githubusercontent.com/104893934/203386340-f2c5cd44-1ecb-4b10-b248-cca84148c0f3.png">
在维格表查看系统表是否创建成功
<img width="1437" alt="image" src="https://user-images.githubusercontent.com/104893934/203386602-a243a23d-6864-4565-8742-c16d06f78ed2.png">
6. 设置使用的puppet程序默认使用wechaty-puppet-xp仅Windows系统下可使用mac、linux系统需切换到wechaty-puppet-xp或wechaty-puppet-padlocal
> 快速启用可使用免费的wechaty-puppet-xp
<img width="1384" alt="image" src="https://user-images.githubusercontent.com/104893934/203387787-46ec974c-3568-4fa6-a8c4-3e569f58aee1.png">
7. 启动程序
```
npm start
```
看到如下界面,说明运行成功了
<img width="786" alt="image" src="https://user-images.githubusercontent.com/104893934/203388629-c8081f57-dfd6-46c8-abb3-3a064e76bbc9.png">
8.开启智能问答功能
8.1 设置微信对话平台token填写"系统配置表"中的 【对话平台token】、【对话平台EncodingAESKey】并开启智能问答
<img width="1310" alt="image" src="https://user-images.githubusercontent.com/104893934/203387234-7ceaee5c-650f-448d-a4f6-59a2153d5de7.png">
8.2 设置群白名单,将群加入到问答白名单在“群白名单”表中加入需要开启的群IDroomid群ID在消息中查看(在群里发一条消息,然后控制台查看或在维格表中查找)
- 获取群ID
<img width="1378" alt="image" src="https://user-images.githubusercontent.com/104893934/203391583-a8c2d3ca-5604-4947-9371-f45b8261fc95.png">
<img width="1139" alt="image" src="https://user-images.githubusercontent.com/104893934/203391251-db34aaa9-c2f1-42dc-8bf2-ed3a2cef707f.png">
- 添加白名单
![image](https://user-images.githubusercontent.com/104893934/203492852-95c083dd-6357-43ec-bba6-6170f1d47cd3.png)
8.3 在微信对话平台中录入问答内容,以群名称建立分类,问答时会优先匹配群名称对应的分类,匹配不到时匹配【通用问题】分类
<img width="1423" alt="image" src="https://user-images.githubusercontent.com/104893934/203390223-9a0ac292-fde9-4114-85dc-9c70a97b917b.png">
8.4 重启程序,在指定群测试问答
## 使用环境变量启动
> 也可以不使用配置文件,通过配置环境变量启动
Mac、Linux操作系统下运行(仅支持使用wechaty-puppet-wechat和wechaty-puppet-padlocal)
```
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
```
set VIKA_TOKEN="替换成自己的维格表token"
set VIKA_SPACENAME="替换成你的维格表空间名称"
npm run sys-init
npm run start
```
## 使用教程
> 提示2022-5-13最新版本里需要在config.js文件中修改自己的微信对话开放平台、VIKA维格表的token维格表token的获取方式请自行浏览官方网站,同时需要在维格表中创建一个名为 mp-chatbot 的空间,关于维格表的操作可以参考[wechaty-vika-link](https://github.com/atorber/wechaty-vika-link)
### 环境准备
1. clone (下载)项目代码,运行以下命令:
```
git clone https://github.com/atorber/wechat-openai-qa-bot.git
```
考虑对git不熟悉的用户可以在页面直接下载项目.zip到电脑上,下载后解压缩即可
<img src="https://user-images.githubusercontent.com/104893934/178886578-a32ac8fc-2efe-4280-be45-59fa918f24a4.png" width="60%">
下载解压缩之后的目录
<img src="https://user-images.githubusercontent.com/104893934/178886647-b1f6193a-58b1-4f35-a82c-0f1c3b6d90e7.png" width="60%">
2. 安装nodejs项目的tools目录下有相应的安装包node-v16.15.0-x64.zip解压缩并安装
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)
> 特别注意目前支持的微信客户端版本为 WeChatSetup-v3.6.0.18,如果电脑上已经安装了其他版本的微信,需要卸载之后安装项目中的版本
### 安装依赖
1. 假设当前系统为win10在系统搜索栏中输入 powershell ,选择第一个结果
<img src="https://user-images.githubusercontent.com/104893934/178886715-8370286a-8bfe-49d4-b7aa-270b396c7d82.png" width="60%">
2. 打开Windows PoweShell
<img src="https://user-images.githubusercontent.com/104893934/178886782-66c60bde-71a5-45a5-84be-aabb199104c4.png" width="60%">
3. 到项目目录下用鼠标点击地址栏复制文件路径,例如当前的路径为 C:\Users\wechaty\Documents\GitHub\wechaty-wx-openai-link
> 一定要查看自己的路径不要直接copy
<img src="https://user-images.githubusercontent.com/104893934/178886794-8e3be7d6-a64b-4810-b1fd-acdc934f2807.png" width="60%">
4. 在复制如下命令在Windows PoweShell中执行
```
cd C:\Users\wechaty\Documents\GitHub\wechat-openai-qa-bot
npm install
```
### 问答平台注册
1. 微信对话开放平台注册,访问[https://openai.weixin.qq.com/](https://openai.weixin.qq.com/)导入示例数据及获取token
> 示例问答中的 xxx@chatroom 为你需要引入QA的群此处特别注意必须在回答中以 **QA+xxx@chatroom+回答内容** 才能达到在不同的群内有不同回答的效果
2. 扫码登陆
<img src="https://user-images.githubusercontent.com/104893934/178886931-19fb0d67-3682-4dea-8978-dc53acb59bcd.png" width="60%">
3. 填写机器人信息
<img src="https://user-images.githubusercontent.com/104893934/178886955-f2aed93a-0667-44ac-b657-a7dc6b901bc0.jpg" width="60%">
4. 添加问答
在微信对话开放平台中添加问题,创建【通用问题】分类,所有群和好友可匹配,使用群名称创建分类,仅匹配对应群
5. 选择项目中tools目录下的示例问答
<img src="https://user-images.githubusercontent.com/104893934/178887001-53c16428-77da-4eba-8044-c9a9a2714174.png" width="60%">
6. 上线发布
<img src="https://user-images.githubusercontent.com/104893934/178887066-d26a9c74-96c6-463a-8ac7-117b72e13dc4.png" width="60%">
7. 发布成功
<img src="https://user-images.githubusercontent.com/104893934/178887079-5b17ab07-8e7f-4c8f-87bf-5ed7da546ad8.png" width="60%">
8. 应用绑定获取token
<img src="https://user-images.githubusercontent.com/104893934/178887115-6a2b14cb-6ccc-43c6-961d-d5c1bbcd58b6.png" width="60%">
9. 填写申请信息,提交后马上就会审核通过
<img src="https://user-images.githubusercontent.com/104893934/178887131-db6248c0-302d-418f-bfa6-0ca5952d3f40.png" width="60%">
10. 开通成功复制token备用
<img src="https://user-images.githubusercontent.com/104893934/178887148-88e681c2-db4e-4051-a94f-215a1231eabb.png" width="60%">
### 维格表注册
维格表token的获取方式请自行浏览[vika维格表](https://spcp52tvpjhxm.com.vika.cn/?inviteCode=55152973)官方网站,同时需要在维格表中创建一个名为 mp-chatbot 的空间,关于维格表的操作可以参考[wechaty-vika-link](https://github.com/atorber/wechaty-vika-link/tree/anti-epidemic)
### 修改配置
在获取token之后更新token到配置文件中准备启动系统
```
// 配置文件,所有配置必须齐全,补充空白配置项,其他配置项可按需要修改
const configs = {
VIKA_TOKEN: '替换成自己的维格表token', // VIKA维格表token
VIKA_SPACENAME: '替换成你的维格表空间名称', // VIKA维格表空间名称修改为自己的空间名称
}
export default configs
```
### 启动程序
执行如下命令
```
npm run start
```
顺利的话恭喜你已经拥有一个QA机器人接下来你需要在简单问答中继续导入你需要的问答内容
- 程序运行成功
<img src="https://user-images.githubusercontent.com/104893934/178887182-bb72a4f0-f2ff-4b52-b67e-d8832844c180.png" width="60%">
### 客服系统
1. 修改“系统配置”表中的为`IM对话`为开启
2. 启动vue-im再启动主程序
```
cd ./vue-im
npm install
npm run dev
```
3. 启动后浏览器中访问 http://localhost:8080/#/imServer 即可打开客服管理后台
4. 到根目录运行`npm run start`启动主程序
## 效果展示
### 群消息存档
<img src="https://user-images.githubusercontent.com/19552906/167827644-a4cad573-b26f-4701-a27f-1ada1d2ffb47.png" width="60%">
### 自动问答回复
<img src="https://user-images.githubusercontent.com/104893934/167547910-4550f388-ee15-478c-8345-560b98367d88.png" width="60%">
### 问题管理
<img src="https://user-images.githubusercontent.com/104893934/167548122-e97bd126-4df9-410c-b87c-876df3f7aacf.png" width="60%">
### 编辑问题
<img src="https://user-images.githubusercontent.com/104893934/167548070-31c847ae-b876-4051-bccf-ed81baad56b9.png" width="60%">
### 非本群链接检测
<img src="https://user-images.githubusercontent.com/104893934/167547463-0b943e27-4667-4266-bed4-1fd020637902.png" width="60%">
### 客服后台系统
<img src="https://user-images.githubusercontent.com/104893934/169646853-b635e1ad-92fd-4fd4-b62a-c165e5ba4796.png" width="60%">
### 快团团订单自动汇总
- 发送原始订单表到群内自动生成按楼栋汇总好的表格
<img src="https://user-images.githubusercontent.com/104893934/167663152-94127586-5429-4689-bba8-379127606a56.png" width="60%">
- 快团团后台导出的全部字段原始表
<img src="https://user-images.githubusercontent.com/104893934/168030413-f13c2107-d54f-4921-b361-948ac28a0841.png" width="60%">
- 生成汇总表
<img src="https://user-images.githubusercontent.com/104893934/168030570-b88991f4-be4b-4479-94e7-0041d0508fc1.png" width="60%">
## DEMO体验
如果你对以上操作感觉困难而不能使用添加ledongmao微信提供你需要的问答清单我们可以提供一个免费的机器人供体验
当然,最好的反馈方式是在这里 https://github.com/choogoo/wechat-openai-qa-bot/issues 提交一个issues
## 在线交流
QQ群 583830241
## 二次开发
此项目只是提供了一个简单的使用微信机器人和智能对话平台实现的QA系统。如果有兴趣可以继续学习微信对话开放平台的高级技能实现诸如连续问答等高级功能欢迎贡献你的创意。
此外要说明的是项目中使用puppet-xp完全是出于免费的考虑如果不考虑这一点的话wechaty还有更好用的puppet对于有能力的开发者来说可以根据实际情况替换。
## 常见问题
**遇到任何报错,一定记得第一时间查看报错信息,善用翻译工具,即使看不懂,起码复制或截图,否则没有人能仅凭几句简单描述帮你解决问题**
### 1. 环境依赖
- nodejs > 16 且 npm > 7
使用wechaty-puppet-xp时需使用Windows > 10操作
### 2. 切换网页版微信
在”系统配置“表中修改”puppet“并重启程序
### 3. 安装依赖时提示需要Visual Studio 2017+
去微软官网下载[Visual Studio 2022](https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=Community&channel=Release&version=VS2022&source=VSLandingPage&cid=2030&passive=false)并安装
<img src="https://user-images.githubusercontent.com/104893934/167300714-49a0dc40-8857-4e81-a780-80e63af74d97.png" width="60%">
### 4. ubuntu系统下使用wechaty-puppet-wechat缺少依赖解决方法
根据报错信息参考 https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md
尝试运行如下命令
```
sudo apt install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget libgbm1
```
安装单个依赖
```
sudo apt install libgbm1
```
### 5. mac M1环境下运行报错
报错信息
```
09:24:09 INFO Starter Bot Started.
09:24:26 ERR PuppetWeChatBridge start() exception: TimeoutError: Timed out after 30000 ms while trying to connect to the browser! Only Chrome at revision r982053 is guaranteed to work.
09:24:26 ERR PuppetWeChat initBridge() exception: Timed out after 30000 ms while trying to connect to the browser! Only Chrome at revision r982053 is guaranteed to work.
09:24:26 ERR PuppetWeChat initBridge() this.bridge.stop() rejection: Error: no page
```
解决方案,设置环境变量 `export PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM=RAM`
参考 https://www.npmjs.com/package/puppeteer?activeTab=readme
### 6. 如果折腾半天也没有搞定,可以联系远程协助指导安装
提前下载好[向日葵](https://sunlogin.oray.com/download)软件并注册号账号,登陆后发控制码
<img src="https://user-images.githubusercontent.com/104893934/167300700-19c6283b-584c-48f4-bc10-7418cc7528f3.png" width="60%">

View File

@ -1,21 +0,0 @@
大家好,今天给大家演示一下微信智能问答机器人,如果你是一个社群工作者、拼团团长、业务群运营经理,使用这个项目可以帮助你解决一些重复性问答。
QA系统由微信开放对话平台提供由后台系统可以操作数据存储和配置使用维格表实现。
下面为大家演示几个核心功能:
一 到货信息看板和自动问答
二 通知公告自动问答
先看一下效果,群成员在群内发送问题内容,机器会将包含链接的回答内容自动回复发送,点击链接可以查看详情
实现以上功能很简单,下面我来实际操作演示一下
首先,我们需要在维格表后台发布一条公告,我们以核酸检测通知为例,选择发布日期、填写发布内容和发布者,提交之后,通知内容可以在列表中显示
然后,我们再继续添加一个商品,输入商品名称苹果,提交,苹果被添加到商品列表
接着,我们继续添加一条到货信息,选择日期,选择商品,商品可以选择多个,设置状态为待发货,提交之后,我们在到货信息列表中可以看到相关信息,可以以不同的视图模式显示
以上信息创建完毕后,我们在问答系统中配置相应问题,这样机器人就可以在群内自动回复了
复制一下到货信息列表的分享链接地址,在问答系统中创建一个问题 问题的内容是 ”什么时候到货“,把刚刚复制的链接替换到问题的回复内容中
选择相似问题,系统推荐,系统会自动推荐一些相似的问题,按需要进行勾选,保存一下
继续添加一个社区通知问答,到问题表中复制通知公告的分享链接地址,编辑社区通知问题,将链接替换到问题答案中,保存问题
以上,我们已经添加了 ”什么时候到货“和”社区通知“ 两个问题,
问题保存后需要发布上线一下,点击左侧的发布管理 上线发布 发布,提示发布成功后,我们在微信群众看一下效果
发送消息内容 社区公告,机器人会自动返回回答内容;
发送 什么时候到货,机器人自动回复到货信息
点击回复中的链接地址,就可以查看相关内容了,在电脑网页中的显示是这样,卡片形式,很清晰明了
我们切换到手机模式看一下,卡片视图,也不错
只需要以上简单几步就完成了两个自动通知场景的构建欢迎大家进一步关注Wechat Openai QA Bot项目如果觉得可以给一个star

View File

@ -1,244 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>微信智能问答系统</title>
<link rel="icon" href="_media/favicon.ico" />
<meta
name="google-site-verification"
content="6t0LoIeFksrjF4c9sqUEsVXiQNxLp2hgoqo0KryT-sE"
/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="keywords"
content="doc,docs,documentation,gitbook,creator,generator,github,jekyll,github-pages"
/>
<meta name="description" content="A magical documentation generator." />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0"
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css"
title="vue"
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/dark.css"
title="dark"
disabled
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/buble.css"
title="buble"
disabled
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/pure.css"
title="pure"
disabled
/>
<style>
nav.app-nav li ul {
min-width: 100px;
}
#carbonads {
box-shadow: none !important;
width: auto !important;
}
</style>
</head>
<body>
<div id="app">Loading ...</div>
<script src="//cdn.jsdelivr.net/npm/docsify-plugin-carbon@1/index.js"></script>
<script>
// Set html "lang" attribute based on URL
var lang = location.hash.match(/#\/(de-de|es|ru-ru|zh-cn)\//);
if (lang) {
document.documentElement.setAttribute('lang', lang[1]);
}
// Docsify configuration
window.$docsify = {
alias: {
'.*?/awesome':
'https://github.com/choogoo/wechat-openai-qa-bot/tree/main/docs/README.md',
'.*?/changelog':
'https://raw.githubusercontent.com/docsifyjs/docsify/master/CHANGELOG.md',
'/.*/_navbar.md': '/_navbar.md',
'/es/(.*)':
'https://raw.githubusercontent.com/docsifyjs/docs-es/master/$1',
'/de-de/(.*)':
'https://raw.githubusercontent.com/docsifyjs/docs-de/master/$1',
'/ru-ru/(.*)':
'https://raw.githubusercontent.com/docsifyjs/docs-ru/master/$1',
'/zh-cn/(.*)':
'https://cdn.jsdelivr.net/gh/docsifyjs/docs-zh@master/$1',
},
auto2top: true,
coverpage: true,
executeScript: true,
loadSidebar: true,
loadNavbar: true,
mergeNavbar: true,
maxLevel: 4,
subMaxLevel: 2,
ga: 'UA-106147152-1',
matomo: {
host: '//matomo.thunderwave.de',
id: 6,
},
name: '微信智能问答系统',
repo: 'https://github.com/choogoo/wechat-openai-qa-bot',
nameLink: {
'/es/': '#/es/',
'/de-de/': '#/de-de/',
'/ru-ru/': '#/ru-ru/',
'/zh-cn/': '#/zh-cn/',
'/': '#/',
},
search: {
noData: {
'/es/': '¡No hay resultados!',
'/de-de/': 'Keine Ergebnisse!',
'/ru-ru/': 'Никаких результатов!',
'/zh-cn/': '没有结果!',
'/': 'No results!',
},
paths: 'auto',
placeholder: {
'/es/': 'Buscar',
'/de-de/': 'Suche',
'/ru-ru/': 'Поиск',
'/zh-cn/': '搜索',
'/': 'Search',
},
pathNamespaces: ['/es', '/de-de', '/ru-ru', '/zh-cn']
},
vueComponents: {
'button-counter': {
template:
'<button @click="count += 1">You clicked me {{ count }} times</button>',
data: function() {
return {
count: 0,
};
},
},
},
vueGlobalOptions: {
data: function() {
return {
count: 0,
message: 'Hello, World!',
// Fake API response
images: [
{ title: 'Image 1', url: 'https://picsum.photos/150?random=1' },
{ title: 'Image 2', url: 'https://picsum.photos/150?random=2' },
{ title: 'Image 3', url: 'https://picsum.photos/150?random=3' },
],
};
},
computed: {
timeOfDay: function() {
const date = new Date();
const hours = date.getHours();
if (hours < 12) {
return 'morning';
} else if (hours < 18) {
return 'afternoon';
} else {
return 'evening';
}
},
},
methods: {
hello: function() {
alert(this.message);
},
},
},
vueMounts: {
'#counter': {
data: function() {
return {
count: 0,
};
},
},
},
plugins: [
// DocsifyCarbon.create('CEBI6KQE', 'docsifyjsorg'),
function(hook, vm) {
hook.beforeEach(function(html) {
url =
'https://github.com/choogoo/wechat-openai-qa-bot/tree/main/docs/' +
vm.route.file;
// if (/githubusercontent\.com/.test(vm.route.file)) {
// url = vm.route.file
// .replace('raw.githubusercontent.com', 'github.com')
// .replace(/\/master/, '/blob/master');
// } else if (/jsdelivr\.net/.test(vm.route.file)) {
// url = vm.route.file
// .replace('cdn.jsdelivr.net/gh', 'github.com')
// .replace('@master', '/blob/master');
// } else {
// url =
// 'https://github.com/choogoo/wechat-openai-qa-bot/tree/main/docs/' +
// vm.route.file;
// }
var editHtml = '[:memo: Edit Document](' + url + ')\n';
return (
editHtml +
html +
'\n\n----\n\n' +
'<a href="https://docsify.js.org" target="_blank" style="color: inherit; font-weight: normal; text-decoration: none;">Powered by atorber</a>'
);
}),
hook.afterEach(function(html) {
if (vm.route.path === '/') {
return html;
}
return html;
// return (
// html +
// '<br/> <i>Vercel</i> has given us a Pro account <br/> <a href="https://vercel.com/?utm_source=docsifyjsdocs" target="_blank"><img src="https://cdn.jsdelivr.net/gh/docsifyjs/docsify/docs/_media/vercel_logo.svg" alt="Vercel" width="100" height="64"></a>'
// );
});
},
],
};
</script>
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/docsify.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-markdown.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-nginx.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-php.min.js"></script>
<script>
(function() {
function loadJS(src, attrs) {
document.write(
'<script src="' + src + '" ' + (attrs || '') + '><\/script>'
);
}
// Public site only
if (/docsify/.test(location.host)) {
loadJS('//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/ga.min.js');
loadJS('//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/matomo.min.js');
}
})();
</script>
<script src="//cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
<!-- <script src="//cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script> -->
</body>
</html>

View File

@ -1,43 +0,0 @@
@echo off
REM 检查 Node.js 是否已经安装
where node >nul 2>nul
if %errorlevel% equ 0 (
goto run_program
) else (
goto install_node
)
:install_node
REM 下载 Node.js 安装程序
set NODE_VERSION=16.13.2
set NODE_FILENAME=node-v%NODE_VERSION%-x64.msi
set NODE_URL=https://nodejs.org/dist/v%NODE_VERSION%/%NODE_FILENAME%
REM 检查文件是否已经存在
if exist %NODE_FILENAME% (
goto install_node
) else (
goto download_node
)
:download_node
REM 下载 Node.js 安装程序
powershell -Command "& {Invoke-WebRequest -Uri '%NODE_URL%' -OutFile '%NODE_FILENAME%'}"
goto install_node
:install_node
REM 安装 Node.js
msiexec /i %NODE_FILENAME% /qn
:run_program
REM 安装依赖并运行程序
call npm install
call npm run sys-init
call npm start
echo "bot was started successfully."
pause
exit /b

View File

@ -1,57 +0,0 @@
#!/bin/bash
# 检查 Node.js 是否已经安装
function check_node {
if command -v node > /dev/null 2>&1; then
# 如果已经安装,则直接运行程序
run_program
else
# 如果未安装,则下载并安装 Node.js
install_node
fi
}
install_node() {
# 下载 Node.js 安装程序
NODE_VERSION=16.13.2
NODE_FILENAME=node-v${NODE_VERSION}-darwin-x64.tar.gz
NODE_URL=https://nodejs.org/dist/v${NODE_VERSION}/${NODE_FILENAME}
# 检查文件是否已经存在
if [ -f "${NODE_FILENAME}" ]; then
# 如果已经存在,则直接安装 Node.js
install_node_from_file
else
# 如果不存在,则先下载 Node.js 安装程序,再安装 Node.js
download_node
install_node_from_file
fi
}
download_node() {
# 下载 Node.js 安装程序
curl -o "${NODE_FILENAME}" "${NODE_URL}"
}
install_node_from_file() {
# 解压 Node.js 安装程序,并将可执行文件添加到 PATH 环境变量中
tar -xzf "${NODE_FILENAME}"
export PATH="$PWD/node-v${NODE_VERSION}-darwin-x64/bin:$PATH"
}
run_program() {
SCRIPT_RELATIVE_DIR=$(dirname "${BASH_SOURCE}")
cd $SCRIPT_RELATIVE_DIR
# 运行程序
npm install
npm run sys-init
npm start
echo "bot was started successfully."
}
# 调用检查 Node.js 的函数
check_node
# 等待用户按下回车键后退出脚本
read -p "Press [Enter] key to exit."
exit 0

View File

@ -1,57 +0,0 @@
#!/bin/bash
# 检查 Node.js 是否已经安装
function check_node {
if command -v node > /dev/null 2>&1; then
# 如果已经安装,则直接运行程序
run_program
else
# 如果未安装,则下载并安装 Node.js
install_node
fi
}
install_node() {
# 下载 Node.js 安装程序
NODE_VERSION=16.13.2
NODE_FILENAME=node-v${NODE_VERSION}-darwin-x64.tar.gz
NODE_URL=https://nodejs.org/dist/v${NODE_VERSION}/${NODE_FILENAME}
# 检查文件是否已经存在
if [ -f "${NODE_FILENAME}" ]; then
# 如果已经存在,则直接安装 Node.js
install_node_from_file
else
# 如果不存在,则先下载 Node.js 安装程序,再安装 Node.js
download_node
install_node_from_file
fi
}
download_node() {
# 下载 Node.js 安装程序
curl -o "${NODE_FILENAME}" "${NODE_URL}"
}
install_node_from_file() {
# 解压 Node.js 安装程序,并将可执行文件添加到 PATH 环境变量中
tar -xzf "${NODE_FILENAME}"
export PATH="$PWD/node-v${NODE_VERSION}-darwin-x64/bin:$PATH"
}
run_program() {
SCRIPT_RELATIVE_DIR=$(dirname "${BASH_SOURCE}")
cd $SCRIPT_RELATIVE_DIR
# 运行程序
npm install
npm run sys-init
npm start
echo "bot was started successfully."
}
# 调用检查 Node.js 的函数
check_node
# 等待用户按下回车键后退出脚本
read -p "Press [Enter] key to exit."
exit 0

View File

@ -1,91 +0,0 @@
{
"name": "chatflow",
"version": "1.11.3",
"description": "openai-qa-bot",
"type": "module",
"engines": {
"node": ">=16",
"npm": ">=7"
},
"scripts": {
"start": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/bot.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\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/atorber/wechaty-wx-openai-link.git"
},
"keywords": [],
"author": "atorber <atorber@163.com>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/atorber/wechaty-wx-openai-link/issues"
},
"homepage": "https://github.com/atorber/wechaty-wx-openai-link#readme",
"dependencies": {
"@vikadata/vika": "^1.0.5",
"axios": "^1.4.0",
"chatgpt": "^1.1.3",
"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",
"moment": "^2.29.1",
"mqtt": "^4.3.7",
"nedb-promises": "^6.2.1",
"njwt": "^1.2.0",
"node-schedule": "^2.1.0",
"node-xlsx": "^0.21.0",
"npm": "^9.6.4",
"openai-sdk": "^1.0.1",
"qrcode-terminal": "^0.12.0",
"request": "^2.88.0",
"request-promise": "^4.2.6",
"socket.io": "^2.1.0",
"socket.io-client": "^2.4.0",
"uuid": "^9.0.0",
"wechaty": "^1.20.2",
"wechaty-plugin-contrib": "^1.0.18",
"wechaty-puppet-engine": "^1.0.20",
"wechaty-puppet-padlocal": "^1.11.13",
"wechaty-puppet-service": "^1.19.8",
"wechaty-puppet-wechat4u": "^1.13.15",
"wechaty-puppet-xp": "^1.12.7",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@chatie/eslint-config": "^1.0.4",
"@chatie/git-scripts": "^0.6.2",
"@chatie/tsconfig": "^4.6.3",
"@types/jest": "^29.2.3",
"@types/node": "^18.11.9",
"@types/qrcode-terminal": "^0.12.0",
"@types/request-promise": "^4.1.48",
"check-node-version": "^4.2.1",
"cross-env": "^7.0.3",
"is-pr": "^2.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.3"
},
"git": {
"scripts": {
"pre-push": "npx git-scripts-pre-push"
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
declare module 'qrcode-terminal'

View File

@ -1,90 +0,0 @@
import nedb from '../db/nedb.js'
const cdb = nedb()
/**
* 添加配置文件
* @param {*} config
*/
async function addConfig(info) {
try {
let doc = await cdb.insert(info)
return doc
} catch (error) {
console.log('插入数据错误', error)
}
}
/**
* 更新配置文件
* @param {*} config
*/
async function updateConfig(config) {
try {
let res = await allConfig()
if (res) {
let up = await cdb.update({ id: config.id }, config)
return up
} else {
let add = await addConfig(config)
return add
}
} catch (error) {
console.log('配置文件更新失败', error)
}
}
/**
* 获取所有配置
*/
async function allConfig() {
try {
let search = await cdb.find()
return search[0]
} catch (error) {
console.log('查询数据错误', error)
}
}
/**
* 每日任务
*/
async function dayTaskSchedule() {
try {
let res = await cdb.find({})
return res[0].dayTaskSchedule
} catch (error) {
console.log('获取每日任务', error)
}
}
/**
* 群资讯
*/
async function roomNewsSchedule() {
try {
let res = await cdb.find.find({})
return res[0].roomNewsSchedule
} catch (error) {
console.log('获取每日任务', error)
}
}
/**
* 群任务
*/
async function roomTaskSchedule() {
try {
let res = await cdb.find.find({})
return res[0].roomTaskSchedule
} catch (error) {
console.log('获取每日任务', error)
}
}
export { addConfig }
export { updateConfig }
export { allConfig }
export { dayTaskSchedule }
export { roomNewsSchedule }
export { roomTaskSchedule }
export default {
addConfig,
updateConfig,
allConfig,
dayTaskSchedule,
roomNewsSchedule,
roomTaskSchedule,
}

View File

@ -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,
}

View File

@ -1,77 +0,0 @@
import nedb from '../db/nedb.js'
import path from "path";
import os from "os";
const baseDir = path.join(
os.homedir(),
path.sep,
".wechaty",
"wechaty-panel-cache",
path.sep,
);
const dbpath = baseDir + 'room.db'
const rdb = nedb(dbpath)
/**
* 记录群聊天记录 记录格式
* { roomName: '群名', roomId: '', content: '内容', contact: '用户名', wxid: '', time: '时间' }
* @param info
* @returns {Promise<unknown>}
*/
export async function addRoomRecord(info) {
try {
let doc = await rdb.insert(info)
return doc
} catch (error) {
console.log('插入数据错误', error)
}
}
/**
* 获取指定群的聊天记录
* @param room
* @returns {Promise<*>}
*/
export async function getRoomRecord(roomName) {
try {
let search = await rdb.find({roomName})
return search
} catch (error) {
console.log('查询数据错误', error)
}
}
/**
* 清楚指定群的聊天记录
* @param roomName
* @returns {Promise<void>}
*/
export async function removeRecord(roomName) {
try {
let search = await rdb.remove({roomName}, {multi: true})
return search
} catch (e) {
console.log("error", e);
}
}
/**
* 获取指定群聊的所有聊天内容
* @param rooName
* @param day 取的天数
* @returns {Promise<*>}
*/
export async function getRoomRecordContent(rooName, day) {
try {
let list = await getRoomRecord(rooName)
list = list.filter(item=> {
return item.time >= new Date().getTime() - day * 24 * 60 * 60 * 1000
})
let word = ''
list.forEach((item)=> {
word = word + item.content
})
return word
} catch (e) {
console.log("error", e);
}
}

View File

@ -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')
}

View File

@ -1,103 +0,0 @@
{
"baseConfig": {
"VIKA_TOKEN": "usk3EsO4fN56sEJrb4tuamp",
"VIKA_SPACENAME": "WeChat",
"puppetName": "wechaty-puppet-xp",
"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
}
}
}
}
}

View File

@ -1,88 +0,0 @@
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 || {})
}
stmt.exec((err: any, docs: unknown) => {
if (err) {
return reject(err)
}
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)
}
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)
}
resolve(newDoc)
})
})
}
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)
}
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)
}
resolve(numAffected)
})
})
}
export default (database: any) => {
return DB(database)
}

View File

@ -1,27 +0,0 @@
import Datastore from 'nedb-promises'
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 {
db,
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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<void> {
await vika.init()
}
// async function getFields (datasheetId: string): Promise<void> {
// await vika.getSheetFields(datasheetId)
// }
// void getFields('dstKiDu2sEAXJGvsJR')
void init()

View File

@ -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

View File

@ -1 +0,0 @@
export * as types from './types.js'

View File

@ -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,
}

View File

@ -1,468 +0,0 @@
/* eslint-disable no-empty */
/* eslint-disable sort-keys */
import mqtt from 'mqtt'
import { v4 } from 'uuid'
import { FileBox } from 'file-box'
import {
// Contact,
log,
Contact,
// Message,
// ScanStatus,
Wechaty,
// UrlLink,
// MiniProgram,
} from 'wechaty'
import { wechaty2chatdev, propertyMessage, eventMessage } from './msg-format.js'
import {
// waitForMs as wait,
formatSentMessage,
} from '../util/tool.js'
class ChatDevice {
chatbot!:any
chatdevice!:any
bot!: Wechaty
mqttclient:any
isConnected:any
propertyApi:string
eventApi:string
commandApi:string
constructor (username:string, password:string, endpoint:string, port:string|number, botId:string) {
this.mqttclient = mqtt.connect(`mqtt://${endpoint}:${port || 1883}`, {
username,
password,
clientId: v4(),
})
this.isConnected = false
this.propertyApi = `thing/chatbot/${botId}/property/post`
this.eventApi = `thing/chatbot/${botId}/event/post`
this.commandApi = `thing/chatbot/${botId}/command/invoke`
}
init (bot:Wechaty) {
this.chatbot = bot
this.chatdevice = this
this.bot = bot
const that = this
this.mqttclient.on('connect', function () {
that.isConnected = true
log.info('================================================\n\nMQTT连接成功~\n\n================================================\n')
})
this.mqttclient.on('reconnect', function (e:any) {
log.info('subscriber on reconnect')
})
this.mqttclient.on('disconnect', function (e:any) {
log.info('disconnect--------', e)
that.isConnected = false
})
this.mqttclient.on('error', function (e:any) {
log.info('error----------', e)
})
this.mqttclient.on('message', this.onMessage)
this.sub_command()
}
sub_command () {
this.mqttclient.subscribe(this.commandApi, function (err:any) {
if (err) {
log.info(err)
}
})
}
pub_property (msg:any) {
this.mqttclient.publish(this.propertyApi, msg)
}
pub_event (msg:any) {
this.mqttclient.publish(this.eventApi, msg)
}
async pub_message (msg:any) {
try {
const payload = await wechaty2chatdev(msg)
this.mqttclient.publish(this.eventApi, payload)
} catch (err) {
console.error(err)
}
}
getBot () {
return this.bot
}
async onMessage (topic:string, message:any) {
log.info('mqtt onMessage:', topic, message.toString())
// const content = JSON.parse(message.toString())
message = JSON.parse(message)
const name = message.name
const params = message.params
if (name === 'start') {
}
if (name === 'stop') {
}
if (name === 'logout') {
}
if (name === 'logonoff') {
}
if (name === 'userSelf') {
}
if (name === 'say') {
}
if (name === 'send') {
await send(params, this.chatbot)
}
if (name === 'sendAt') {
await sendAt(params, this.chatbot)
}
if (name === 'aliasGet') {
}
if (name === 'aliasSet') {
}
if (name === 'roomCreate') {
await createRoom(params, this.chatbot)
}
if (name === 'roomAdd') {
}
if (name === 'roomDel') {
}
if (name === 'roomAnnounceGet') {
}
if (name === 'roomAnnounceSet') {
}
if (name === 'roomQuit') {
}
if (name === 'roomTopicGet') {
}
if (name === 'roomTopicSet') {
}
if (name === 'roomQrcodeGet') {
await getQrcod(params, this.chatbot, this.chatdevice)
}
if (name === 'memberAllGet') {
}
if (name === 'contactAdd') {
}
if (name === 'contactAliasSet') {
}
if (name === 'contactFindAll') {
await getAllContact(this.chatdevice, this.chatbot)
}
if (name === 'contactFind') {
}
if (name === 'roomFindAll') {
await getAllRoom(this.chatdevice, this.chatbot)
}
if (name === 'roomFind') {
}
if (name === 'config') {
}
}
}
async function getAllContact (chatdevice:any, bot:Wechaty) {
const contactList:Contact[] = await bot.Contact.findAll()
let friends = []
for (const i in contactList) {
const contact = contactList[i]
let avatar = ''
try {
avatar = JSON.parse(JSON.stringify(await contact?.avatar())).url
} catch (err) {
}
const contactInfo = {
id: contact?.id,
gender: contact?.gender() || '',
name: contact?.name() || '',
alias: await contact?.alias() || '',
avatar,
}
friends.push(contactInfo)
if (friends.length === 100) {
const msg = propertyMessage('contactList', friends)
chatdevice.pub_property(msg)
friends = []
}
}
const msg = propertyMessage('contactList', friends)
chatdevice.pub_property(msg)
}
async function getAllRoom (chatdevice:any, bot:Wechaty) {
const roomList = await bot.Room.findAll()
for (const i in roomList) {
const room = roomList[i]
const roomInfo:any = {}
roomInfo.id = room?.id
const avatar = await room?.avatar()
roomInfo.avatar = JSON.parse(JSON.stringify(avatar)).url
roomInfo.ownerId = room?.owner()?.id
try {
roomInfo.topic = await room?.topic()
} catch (err) {
roomInfo.topic = room?.id
}
roomList[i] = roomInfo
}
const msg = propertyMessage('roomList', roomList)
chatdevice.pub_property(msg)
}
async function send (params:any, bot:Wechaty) {
log.info('params:', params)
let msg:any = ''
if (params.messageType === 'Text') {
/* {
"reqId":"442c1da4-9d3a-4f9b-a6e9-bfe858e4ac43",
"method":"thing.command.invoke",
"version":"1.0",
"timestamp":1610430718000,
"name":"send",
"params":{
"toContacts":[
"tyutluyc",
"5550027590@chatroom"
],
"messageType":"Text",
"messagePayload":"welcome to wechaty!"
}
} */
msg = params.messagePayload
} else if (params.messageType === 'Contact') {
/* {
"reqId":"442c1da4-9d3a-4f9b-a6e9-bfe858e4ac43",
"method":"thing.command.invoke",
"version":"1.0",
"timestamp":1610430718000,
"name":"send",
"params":{
"toContacts":[
"tyutluyc",
"5550027590@chatroom"
],
"messageType":"Contact",
"messagePayload":"tyutluyc"
}
} */
const contactCard = await bot.Contact.find({ id: params.messagePayload })
if (!contactCard) {
log.info('not found')
return {
msg: '无此联系人',
}
} else {
msg = contactCard
}
} else if (params.messageType === 'Attachment') {
/* {
"reqId":"442c1da4-9d3a-4f9b-a6e9-bfe858e4ac43",
"method":"thing.command.invoke",
"version":"1.0",
"timestamp":1610430718000,
"name":"send",
"params":{
"toContacts":[
"tyutluyc",
"5550027590@chatroom"
],
"messageType":"Attachment",
"messagePayload":"/tmp/text.txt"
}
} */
if (params.messagePayload.indexOf('http') !== -1 || params.messagePayload.indexOf('https') !== -1) {
msg = FileBox.fromUrl(params.messagePayload)
} else {
msg = FileBox.fromFile(params.messagePayload)
}
} else if (params.messageType === 'Image') {
/* {
"reqId":"442c1da4-9d3a-4f9b-a6e9-bfe858e4ac43",
"method":"thing.command.invoke",
"version":"1.0",
"timestamp":1610430718000,
"name":"send",
"params":{
"toContacts":[
"tyutluyc",
"5550027590@chatroom"
],
"messageType":"Image",
"messagePayload":"https://wechaty.github.io/wechaty/images/bot-qr-code.png"
}
} */
// msg = FileBox.fromUrl(params.messagePayload)
if (params.messagePayload.indexOf('http') !== -1 || params.messagePayload.indexOf('https') !== -1) {
log.info('图片http地址', params.messagePayload)
msg = FileBox.fromUrl(params.messagePayload)
} else {
log.info('图片本地地址:', params.messagePayload)
msg = FileBox.fromFile(params.messagePayload)
}
} else if (params.messageType === 'Url') {
/* {
"reqId":"442c1da4-9d3a-4f9b-a6e9-bfe858e4ac43",
"method":"thing.command.invoke",
"version":"1.0",
"timestamp":1610430718000,
"name":"send",
"params":{
"toContacts":[
"tyutluyc",
"5550027590@chatroom"
],
"messageType":"Url",
"messagePayload":{
"description":"WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love",
"thumbnailUrl":"https://avatars0.githubusercontent.com/u/25162437?s=200&v=4",
"title":"Welcome to Wechaty",
"url":"https://github.com/wechaty/wechaty"
}
}
} */
msg = params.messagePayload
} else if (params.messageType === 'MiniProgram') {
/* {
"reqId":"442c1da4-9d3a-4f9b-a6e9-bfe858e4ac43",
"method":"thing.command.invoke",
"version":"1.0",
"timestamp":1610430718000,
"name":"send",
"params":{
"toContacts":[
"tyutluyc",
"5550027590@chatroom"
],
"messageType":"MiniProgram",
"messagePayload":{
"appid":"wx36027ed8c62f675e",
"description":"群组大师群管理工具",
"title":"群组大师",
"pagePath":"pages/start/relatedlist/index.html",
"thumbKey":"",
"thumbUrl":"http://mmbiz.qpic.cn/mmbiz_jpg/mLJaHznUd7O4HCW51IPGVarcVwAAAuofgAibUYIct2DBPERYIlibbuwthASJHPBfT9jpSJX4wfhGEBnqDvFHHQww/0",
"username":"gh_6c52e2baeb2d@app"
}
}
} */
msg = params.messagePayload
} else {
return {
msg: '不支持的消息类型',
}
}
log.info('msg:', msg)
const toContacts = params.toContacts
for (let i = 0; i < toContacts.length; i++) {
if (toContacts[i].split('@').length === 2 || toContacts[i].split(':').length === 2) {
log.info(`向群${toContacts[i]}发消息`)
const room = await bot.Room.find({ id: toContacts[i] })
if (room) {
try {
await room.say(msg)
await formatSentMessage(bot.currentUser, msg, undefined, room)
} catch (err) {
console.error(err)
}
}
} else {
log.info(`好友${toContacts[i]}发消息`)
// log.info(bot)
const contact = await bot.Contact.find({ id: toContacts[i] })
if (contact) {
try {
await contact.say(msg)
await formatSentMessage(bot.currentUser, msg, contact, undefined)
} catch (err) {
console.error(err)
}
}
}
}
}
async function sendAt (params:any, bot:Wechaty) {
const atUserIdList = params.toContacts
const room = await bot.Room.find({ id: params.room })
const atUserList = []
for (const userId of atUserIdList) {
const curContact = await bot.Contact.find({ id:userId })
atUserList.push(curContact)
}
await room?.say(params.messagePayload, ...atUserList)
await formatSentMessage(bot.currentUser, params.messagePayload, undefined, room)
}
async function createRoom (params:any, bot:Wechaty) {
const contactList:Contact[] = []
for (const i in params.contactList) {
const c = await bot.Contact.find({ name: params.contactList[i] })
if (c) {
contactList.push(c)
}
}
const room = await bot.Room.create(contactList, params.topic)
// log.info('Bot', 'createDingRoom() new ding room created: %s', room)
// await room.topic(params.topic)
await room.say('你的专属群创建完成')
await formatSentMessage(bot.currentUser, '你的专属群创建完成', undefined, room)
}
async function getQrcod (params:any, bot:Wechaty, chatdevice:any) {
const roomId = params.roomId
const room = await bot.Room.find({ id: roomId })
const qr = await room?.qrCode()
const msg = eventMessage('qrcode', qr)
chatdevice.pub_event(msg)
}
export { ChatDevice }
export default ChatDevice

View File

@ -1,315 +0,0 @@
/* eslint-disable no-console */
/* eslint-disable sort-keys */
import {
init,
// chat,
chatAibot,
// nlp,
// QueryData,
genToken,
} from '../sdk/openai/index.js'
import { FileBox } from 'file-box'
// import excel2order from '../excel.js'
import {
Contact,
Room,
Message,
// ScanStatus,
// WechatyBuilder,
log,
types,
Wechaty,
} from 'wechaty'
import path from 'path'
// import os from 'os'
import {
waitForMs as wait,
formatSentMessage,
} from '../util/tool.js'
import { ChatGPTAPI } from 'chatgpt'
const botTpyes = [ 'WxOpenai', 'ChatGPT' ]
const useBot = 0
const callBot = botTpyes[useBot]
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\\`
async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | undefined, message: Message) {
// const talker = message.talker()
// const roomid = room ? room.id : ''
let text = message.text()
const keyWord = bot.currentUser.name()
if (text.indexOf(keyWord) !== -1 && text.length > 4) {
const index = text.lastIndexOf(keyWord) + keyWord.length - 1
text = text.substring(index + 1, text.length)
}
let answer: any = {}
if (message.type() === types.Message.Text && room) {
answer = await aibot(sysConfig, talker, room, text)
}
if (message.type() === types.Message.Text && !room) {
answer = await aibot(sysConfig, talker, undefined, text)
}
if (room && message.type() === types.Message.MiniProgram && !sysConfig.linkWhiteList.includes(talker.id)) {
const miniProgram = await message.toMiniProgram()
text = `${miniProgram.title()?.slice(0, 5)}是由群主或管理员所发布的小程序卡片消息吗?`
answer = await aibot(sysConfig, talker, room, text)
}
if (room && message.type() === types.Message.Url && !sysConfig.linkWhiteList.includes(talker.id)) {
const urllink = await message.toUrlLink()
text = `${urllink.title().slice(0, 5)}是由群主或管理员所发布的小程序卡片消息吗?`
answer = await aibot(sysConfig, talker, room, text)
}
// log.info(JSON.stringify(answer))
console.debug('回复消息:', JSON.stringify(answer))
if (answer.messageType) {
switch (answer.messageType) {
case types.Message.Text: {
log.info(`${talker.name()} 发送消息...`)
if (room) {
// answer = text.length > 20 ? (answer.text + '\n------------------------------\n' + talker.name() + ':' + text.slice(0, 10) + '...') : (answer.text + '\n------------------------------\n' + talker.name() + ':' + text)
answer = answer.text + '\n'
// console.debug(answer)
await room.say(answer, ...[ talker ])
formatSentMessage(bot.currentUser, answer, undefined, room)
} else {
answer = answer.text + '\n'
await message.say(answer)
formatSentMessage(bot.currentUser, answer, message.talker(), undefined)
}
break
}
case types.Message.Image: {
const fileBox = FileBox.fromUrl(answer.text.url)
if (room) {
await room.say(fileBox)
formatSentMessage(bot.currentUser, fileBox.toString(), undefined, room)
} else {
await message.say(fileBox)
formatSentMessage(bot.currentUser, fileBox.toString(), message.talker(), undefined)
}
break
}
case types.Message.MiniProgram: {
const miniProgram = new bot.MiniProgram({
appid: answer.text.appid,
pagePath: answer.text.pagepath,
// thumbUrl: answer.text.thumb_url,
thumbKey: '42f8609e62817ae45cf7d8fefb532e83',
thumbUrl: 'https://openai-75050.gzc.vod.tencent-cloud.com/openaiassets_afffe2516dac42406e06eddc19303a8d.jpg',
title: answer.text.title,
})
if (room) {
await room.say(miniProgram)
formatSentMessage(bot.currentUser, miniProgram.toString(), undefined, message.room())
} else {
await message.say(miniProgram)
formatSentMessage(bot.currentUser, miniProgram.toString(), message.talker(), undefined)
}
break
}
default: {
break
}
}
}
if (message.type() === types.Message.Attachment) {
try {
const file = await message.toFileBox()
const fileName = file.name
// text = `${urllink.title().slice(0, 5)}是由群主或管理员所发布的小程序卡片消息吗?`
// answer = await aibot(talker, room, text)
if (fileName.split('.')[1] === 'xlsx') {
// log.info('file=============', file)
const filePath = __dirname + `\\cache\\${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)
log.info('fileName=====', filePath)
// await excel2order(filePath, message)
}
} catch (err) {
log.error('转换失败', err)
}
}
// if (message.type() === types.Message.Image) {
// await wait(1000)
// try {
// const file = await message.toFileBox()
// log.info('image=====', file)
// } catch (err) {
// log.error('image=====', err)
// }
// }
};
async function aibot (sysConfig: any, talker: any, room: any, query: any) {
let answer = {}
const roomid = room?.id
const wxid = talker.id
const nickName = talker.name()
const topic = await room?.topic()
const content = query
// log.info(opt)
let answerJson
switch (callBot) {
case 'WxOpenai':
// log.info('开始请求微信对话平台...')
init({
EncodingAESKey: sysConfig.EncodingAESKey,
TOKEN: sysConfig.WX_TOKEN,
})
try {
const username = room ? (nickName + '/' + topic) : nickName
const userid = room ? (wxid + '/' + roomid) : wxid
const signature = genToken({
userid,
username,
})
let queryData
if (sysConfig.DIFF_REPLY_ONOFF && room) {
queryData = {
first_priority_skills: [ topic || '' ],
query,
second_priority_skills: [ '通用问题' ],
signature,
}
} else {
queryData = {
first_priority_skills: [ '通用问题' ],
query,
signature,
}
}
const resMsg = await chatAibot(queryData)
// console.debug(resMsg)
log.info('对话返回原始:', resMsg)
// log.info('对话返回:', JSON.stringify(resMsg).replace(/[\r\n]/g, "").replace(/\ +/g, ""))
log.info('回答内容:', resMsg.msgtype, resMsg.query, resMsg.answer)
// console.debug(resMsg.query)
// console.debug(resMsg.answer)
if (resMsg.msgtype && resMsg.confidence > 0.8) {
switch (resMsg.msgtype) {
case 'text':
answer = {
messageType: types.Message.Text,
text: resMsg.answer || resMsg.msg[0].content,
}
break
case 'miniprogrampage':
answerJson = JSON.parse(resMsg.answer)
answer = {
messageType: types.Message.MiniProgram,
text: answerJson.miniprogrampage,
}
break
case 'image':
answerJson = JSON.parse(resMsg.answer)
answer = {
messageType: types.Message.Image,
text: answerJson.image,
}
break
case 'callback':
if (resMsg.answer_type === 'text') {
answer = {
messageType: types.Message.Text,
text: resMsg.answer,
}
}
break
default:
log.info(JSON.stringify({ msg: '没有命中关键字', nickName, query, roomid, topic }))
break
}
if (sysConfig.DIFF_REPLY_ONOFF) {
if (room && (resMsg.skill_name !== topic && resMsg.skill_name !== '通用问题')) {
answer = {}
}
}
}
} catch (err) {
log.error(JSON.stringify(err))
}
break
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
answer = {
messageType: types.Message.Text,
text: response,
}
} catch (err) {
console.error(err)
}
break
default:
console.debug('没有匹配')
break
}
return answer
}
export {
wxai,
aibot,
}
export default wxai

View File

@ -1,9 +0,0 @@
import {
wxai,
aibot,
} from './wx-openai.js'
export {
wxai,
aibot,
}

View File

@ -1,314 +0,0 @@
/* eslint-disable no-console */
/* eslint-disable sort-keys */
import {
init,
// chat,
chatAibot,
// nlp,
// QueryData,
genToken,
} from '../sdk/openai/index.js'
import { FileBox } from 'file-box'
// import excel2order from '../excel.js'
import {
Contact,
Room,
Message,
// ScanStatus,
// WechatyBuilder,
log,
types,
Wechaty,
} from 'wechaty'
import path from 'path'
// import os from 'os'
import {
waitForMs as wait,
formatSentMessage,
} from '../util/tool.js'
import { ChatGPTAPI } from 'chatgpt'
const botTpyes = [ 'WxOpenai', 'ChatGPT' ]
const useBot = 0
const callBot = botTpyes[useBot]
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\\`
async function wxai (sysConfig: any, bot: Wechaty, talker: Contact, room: Room | undefined, message: Message) {
// const talker = message.talker()
// const roomid = room ? room.id : ''
let text = message.text()
const keyWord = bot.currentUser.name()
if (text.indexOf(keyWord) !== -1 && text.length > 4) {
const index = text.lastIndexOf(keyWord) + keyWord.length - 1
text = text.substring(index + 1, text.length)
}
let answer: any = {}
if (message.type() === types.Message.Text && room) {
answer = await aibot(sysConfig, talker, room, text)
}
if (message.type() === types.Message.Text && !room) {
answer = await aibot(sysConfig, talker, undefined, text)
}
if (room && message.type() === types.Message.MiniProgram && !sysConfig.linkWhiteList.includes(talker.id)) {
const miniProgram = await message.toMiniProgram()
text = `${miniProgram.title()?.slice(0, 5)}是由群主或管理员所发布的小程序卡片消息吗?`
answer = await aibot(sysConfig, talker, room, text)
}
if (room && message.type() === types.Message.Url && !sysConfig.linkWhiteList.includes(talker.id)) {
const urllink = await message.toUrlLink()
text = `${urllink.title().slice(0, 5)}是由群主或管理员所发布的小程序卡片消息吗?`
answer = await aibot(sysConfig, talker, room, text)
}
// log.info(JSON.stringify(answer))
console.debug('回复消息:', JSON.stringify(answer))
if (answer.messageType) {
switch (answer.messageType) {
case types.Message.Text: {
log.info(`${talker.name()} 发送消息...`)
if (room) {
// answer = text.length > 20 ? (answer.text + '\n------------------------------\n' + talker.name() + ':' + text.slice(0, 10) + '...') : (answer.text + '\n------------------------------\n' + talker.name() + ':' + text)
answer = answer.text + '\n'
// console.debug(answer)
await room.say(answer, ...[ talker ])
formatSentMessage(bot.currentUser, answer, undefined, room)
} else {
answer = answer.text + '\n'
await message.say(answer)
formatSentMessage(bot.currentUser, answer, message.talker(), undefined)
}
break
}
case types.Message.Image: {
const fileBox = FileBox.fromUrl(answer.text.url)
if (room) {
await room.say(fileBox)
formatSentMessage(bot.currentUser, fileBox.toString(), undefined, room)
} else {
await message.say(fileBox)
formatSentMessage(bot.currentUser, fileBox.toString(), message.talker(), undefined)
}
break
}
case types.Message.MiniProgram: {
const miniProgram = new bot.MiniProgram({
appid: answer.text.appid,
pagePath: answer.text.pagepath,
// thumbUrl: answer.text.thumb_url,
thumbKey: '42f8609e62817ae45cf7d8fefb532e83',
thumbUrl: 'https://openai-75050.gzc.vod.tencent-cloud.com/openaiassets_afffe2516dac42406e06eddc19303a8d.jpg',
title: answer.text.title,
})
if (room) {
await room.say(miniProgram)
formatSentMessage(bot.currentUser, miniProgram.toString(), undefined, message.room())
} else {
await message.say(miniProgram)
formatSentMessage(bot.currentUser, miniProgram.toString(), message.talker(), undefined)
}
break
}
default: {
break
}
}
}
if (message.type() === types.Message.Attachment) {
try {
const file = await message.toFileBox()
const fileName = file.name
// text = `${urllink.title().slice(0, 5)}是由群主或管理员所发布的小程序卡片消息吗?`
// answer = await aibot(talker, room, text)
if (fileName.split('.')[1] === 'xlsx') {
// log.info('file=============', file)
const filePath = __dirname + `\\cache\\${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)
log.info('fileName=====', filePath)
// await excel2order(filePath, message)
}
} catch (err) {
log.error('转换失败', err)
}
}
// if (message.type() === types.Message.Image) {
// await wait(1000)
// try {
// const file = await message.toFileBox()
// log.info('image=====', file)
// } catch (err) {
// log.error('image=====', err)
// }
// }
};
async function aibot (sysConfig: any, talker: any, room: any, query: any) {
let answer = {}
const roomid = room?.id
const wxid = talker.id
const nickName = talker.name()
const topic = await room?.topic()
// log.info(opt)
const content = query
let answerJson
switch (callBot) {
case 'WxOpenai':
// log.info('开始请求微信对话平台...')
init({
EncodingAESKey: sysConfig.EncodingAESKey,
TOKEN: sysConfig.WX_TOKEN,
})
try {
const username = room ? (nickName + '/' + topic) : nickName
const userid = room ? (wxid + '/' + roomid) : wxid
const signature = genToken({
userid,
username,
})
let queryData
if (sysConfig.DIFF_REPLY_ONOFF && room) {
queryData = {
first_priority_skills: [ topic || '' ],
query,
second_priority_skills: [ '通用问题' ],
signature,
}
} else {
queryData = {
first_priority_skills: [ '通用问题' ],
query,
signature,
}
}
const resMsg = await chatAibot(queryData)
// console.debug(resMsg)
log.info('对话返回原始:', resMsg)
// log.info('对话返回:', JSON.stringify(resMsg).replace(/[\r\n]/g, "").replace(/\ +/g, ""))
log.info('回答内容:', resMsg.msgtype, resMsg.query, resMsg.answer)
// console.debug(resMsg.query)
// console.debug(resMsg.answer)
if (resMsg.msgtype && resMsg.confidence > 0.8) {
switch (resMsg.msgtype) {
case 'text':
answer = {
messageType: types.Message.Text,
text: resMsg.answer || resMsg.msg[0].content,
}
break
case 'miniprogrampage':
answerJson = JSON.parse(resMsg.answer)
answer = {
messageType: types.Message.MiniProgram,
text: answerJson.miniprogrampage,
}
break
case 'image':
answerJson = JSON.parse(resMsg.answer)
answer = {
messageType: types.Message.Image,
text: answerJson.image,
}
break
case 'callback':
if (resMsg.answer_type === 'text') {
answer = {
messageType: types.Message.Text,
text: resMsg.answer,
}
}
break
default:
log.info(JSON.stringify({ msg: '没有命中关键字', nickName, query, roomid, topic }))
break
}
if (sysConfig.DIFF_REPLY_ONOFF) {
if (room && (resMsg.skill_name !== topic && resMsg.skill_name !== '通用问题')) {
answer = {}
}
}
}
} catch (err) {
log.error(JSON.stringify(err))
}
break
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
answer = {
messageType: types.Message.Text,
text: response,
}
} catch (err) {
console.error(err)
}
break
default:
console.debug('没有匹配')
break
}
return answer
}
export {
wxai,
aibot,
}
export default wxai

View File

@ -1,2 +0,0 @@
declare module '*'
declare module './excel.js'

View File

@ -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

View File

@ -1,93 +0,0 @@
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
import 'dotenv/config.js'
import { Contact, Message, types, log, Wechaty } from 'wechaty'
import { FileBox } from 'file-box'
import XLSX from 'xlsx'
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
async function sendTextMessage (contact: Contact, text: string): Promise<boolean> {
try {
await contact.say(`${contact.name()}${text}`)
return true
} catch (error) {
log.error('StarterBot', 'Error sending message: %s', error)
return false
}
}
async function sendNotice (bot:Wechaty, msg: Message) {
log.info('sendNotice:', msg.talker().id)
await delay(3000)
// 检测群消息
if (msg.type() === types.Message.Attachment) {
const file = await msg.toFileBox()
const fileType = file.name.split('.').pop()
if (fileType === 'xlsx' || fileType === 'xls') {
const buffer = await file.toBuffer()
const workbook = XLSX.read(buffer, { type: 'buffer' })
const sheetName = workbook.SheetNames[0] || 'null'
const sheet = workbook.Sheets[sheetName]
if (sheet !== undefined) {
const data:any = XLSX.utils.sheet_to_json(sheet, { header: 1 })
if (data[0].includes('wxid') && data[0].includes('text') && data[0].includes('state')) {
const wxidIndex = data[0].indexOf('wxid')
const textIndex = data[0].indexOf('text')
const stateIndex = data[0].indexOf('state')
let successCount = 0
let failureCount = 0
const failedWxids:string[] = []
for (let i = 1; i < data.length; i++) {
const wxid = data[i][wxidIndex]
const text = data[i][textIndex]
log.info('通知内容:', wxid, text)
try {
const contact:Contact|undefined = await bot.Contact.find({ id: wxid })
log.info('contact:', JSON.stringify(contact))
if (contact && contact.friend()) {
const isSuccess = await sendTextMessage(contact, text)
await delay(100)
data[i][stateIndex] = isSuccess ? '成功' : '失败'
isSuccess ? successCount++ : failureCount++
if (!isSuccess) {
failedWxids.push(wxid)
}
} else {
data[i][stateIndex] = '失败'
failedWxids.push(wxid)
failureCount++
}
} catch (err) {
log.info('wxid不存在', err)
}
}
const updatedSheet = XLSX.utils.aoa_to_sheet(data)
workbook.Sheets[sheetName] = updatedSheet
const updatedBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' })
const updatedFile = FileBox.fromBuffer(updatedBuffer, 'res_' + file.name)
log.info('updatedFile:', updatedFile)
await msg.say(`通知发送完成,成功${successCount}人,失败${failureCount}详情查看excel文件`)
await msg.say(updatedFile)
// if (failedWxids.length > 0) {
// await msg.say(`发送失败的wxid\n${failedWxids.join('\n')}`)
// }
}
}
}
}
}
export { sendNotice }
export default sendNotice

View File

@ -1,171 +0,0 @@
/* eslint-disable sort-keys */
import {
log,
Room,
Wechaty,
} from 'wechaty'
import * as io from 'socket.io-client'
import {
formatSentMessage,
} from '../util/tool.js'
// IM相关配置
const configData = {
chatInfoEn: {
chatState: 'agent', // chat状态robot 机器人、agent 客服
inputContent: '', // 输入框内容
lastMsgShowTime: new Date(), // 最后一个消息的显示时间
msgList: [], // 消息列表
state: 'on', // 连接状态;on 在线off离线
}, // 会话信息,包括聊天记录、状态
socket: {} as any,
clientChatEn: {
clientChatId: 'ledongmao',
clientChatName: '客服机器人',
avatarUrl: 'static/image/im_client_avatar.png',
}, // 当前账号的信息
serverChatEn: {
serverChatId: 'xiaop',
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
}
/**
* 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: { role: any; contentType: any; content?: string; createTime?: any }, successCallback: { (): void; (): any }) {
// 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
// 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()
}
/**
*
* @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 }

View File

@ -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

View File

@ -1,858 +0,0 @@
/* eslint-disable sort-keys */
enum FieldType {
SingleText = 'SingleText',
SingleSelect = 'SingleSelect',
Text = 'Text',
Attachment = 'Attachment'
}
export type Field = {
name: string,
type: string,
property?: any,
desc?: string
};
export type FieldSingleText = Field & { type: FieldType.SingleText };
export type FieldSingleSelect = Field & { type: FieldType.SingleSelect };
export type FieldText = Field & { type: FieldType.Text };
export type Record = {
fields: {
[key: string]: string
}
}
export type Sheet = {
fields: Field[],
name: string,
defaultRecords: Record[]
}
// export type CommandSchema = {
// '指令名称': FieldSingleText,
// '说明': FieldText,
// '管理员微信号': FieldSingleText,
// '类型': FieldSingleSelect,
// };
// export type Command = Sheet & {
// fields: CommandSchema,
// name: '指令列表',
// defaultRecords: Record[]
// }
const commandSheet: Sheet = {
fields: [ {
name: '指令名称',
type: FieldType.SingleText,
property: {
defaultValue: '',
},
},
{
name: '说明',
type: FieldType.Text,
},
{
name: '管理员微信号',
type: FieldType.SingleText,
property: {
},
},
{
name: '类型',
type: FieldType.SingleSelect,
property: {
options: [
{
name: '系统指令',
color: 'deepPurple_0',
},
{
name: '群指令',
color: 'indigo_0',
},
],
defaultValue: '系统指令',
},
},
],
name: '指令列表',
defaultRecords: [
{
fields: {
: '更新配置',
: '更新系统配置,更改配置后需主动更新一次配置配置才会生效',
: '系统指令',
},
},
{
fields: {
: '更新白名单',
: '更新群白名单,白名单变动时需主动更新白名单',
: '系统指令',
},
},
{
fields: {
: '更新问答',
: '更新微信对话平台中的问答列表',
: '系统指令',
},
},
{
fields: {
: '更新机器人',
: '更新机器人的群列表和好友列表',
: '系统指令',
},
},
{
fields: {
: '启用问答',
: '当前群启用智能问答',
: '群指令',
},
},
{
fields: {
: '关闭问答',
: '当前群关闭智能问答',
: '群指令',
},
},
],
}
// export type ConfigSchema = {
// '机器人名称': FieldSingleText,
// 'AT回复': FieldSingleSelect,
// '智能问答': FieldSingleSelect,
// '对话平台token': FieldSingleText,
// };
// export type Config = Sheet & {
// fields: ConfigSchema,
// name: '系统配置',
// defaultRecords: Record[]
// }
const configSheet: Sheet = {
fields: [
{
name: '机器人名称',
type: 'SingleText',
property: {
defaultValue: '',
},
},
{
name: 'AT回复',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
defaultValue: '关闭',
},
},
{
name: '智能问答',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
defaultValue: '关闭',
},
desc: '开启后可以使用微信对话平台只能问答',
},
{
name: '对话平台token',
type: 'SingleText',
property: {
defaultValue: '',
},
desc: '微信开放对话平台token启用智能问答时必须填写否则无效',
},
{
name: '对话平台EncodingAESKey',
type: 'SingleText',
property: {
defaultValue: '',
},
desc: '微信开放对话平台EncodingAESKey启用智能问答时必须填写否则无效',
},
{
name: '不同群个性回复',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
defaultValue: '开启',
},
desc: '开启后不同群相同问题可以得到不同答案',
},
{
name: '群白名单',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
defaultValue: '开启',
},
desc: '开启后只有白名单内的群会自动问答',
},
{
name: '好友白名单',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
defaultValue: '开启',
},
desc: '开启后只有白名单内的好友自动问答',
},
{
name: '消息上传到维格表',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
defaultValue: '开启',
},
desc: '开启后消息记录会自动上传到维格表的【消息记录】表中',
},
{
name: 'IM对话',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
defaultValue: '关闭',
},
desc: '开启后可以使用客服对话系统',
},
{
name: 'puppet',
type: 'SingleSelect',
property: {
options: [
{
name: 'wechaty-puppet-wechat',
color: 'deepPurple_0',
},
{
name: 'wechaty-puppet-xp',
color: 'indigo_0',
},
{
name: 'wechaty-puppet-padlocal',
color: 'blue_0',
},
],
defaultValue: 'wechaty-puppet-wechat',
},
desc: 'puppet名称目前支持3中puppet',
},
{
name: 'wechaty-token',
type: 'SingleText',
property: {
defaultValue: '',
},
desc: 'puppet的token仅当使用padlocal时需要填写',
},
{
name: 'MQTT控制',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
defaultValue: '关闭',
},
desc: '开启可以通过MQTT控制微信',
},
{
name: 'MQTT推送',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
defaultValue: '关闭',
},
desc: '开启后消息会发送到MQTT队列',
},
{
name: 'MQTT用户名',
type: 'SingleText',
property: {
defaultValue: '',
},
desc: 'MQTT用户名',
},
{
name: 'MQTT密码',
type: 'SingleText',
property: {
defaultValue: '',
},
desc: 'MQTT接入地址',
},
{
name: 'MQTT接入地址',
type: 'SingleText',
property: {
defaultValue: '',
},
desc: 'MQTT接入地址',
},
{
name: 'MQTT端口号',
type: 'SingleText',
property: {
defaultValue: '1883',
},
desc: 'MQTT端口号',
},
],
name: '系统配置',
defaultRecords: [ {
fields: {
: '未设置',
},
} ],
}
const switchSheet: Sheet = {
fields: [
{
name: '功能项',
type: 'SingleText',
property: {
defaultValue: '',
},
},
{
name: '启用状态',
type: 'SingleSelect',
property: {
options: [
{
name: '开启',
color: 'deepPurple_0',
},
{
name: '关闭',
color: 'indigo_0',
},
],
},
},
{
name: '说明',
type: FieldType.Text,
},
],
name: '功能开关',
defaultRecords: [
{
fields: {
: '智能问答',
: '关闭',
: '开启后可以使用微信对话平台只能问答',
},
},
{
fields: {
: 'AT回复',
: '关闭',
: '开启后只有@好友才会回复问答',
},
},
{
fields: {
: '不同群个性回复',
: '开启',
: '开启后不同群相同问题可以得到不同答案',
},
},
{
fields: {
: '群白名单',
: '开启',
: '开启后只有白名单内的群会自动问答',
},
},
{
fields: {
: '好友白名单',
: '开启',
: '开启后只有白名单内的好友自动问答',
},
},
{
fields: {
: '消息上传到维格表',
: '开启',
: '开启后消息记录会自动上传到维格表的【消息记录】表',
},
},
{
fields: {
: 'IM对话',
: '开启',
: '开启后可以使用客服对话系统',
},
},
{
fields: {
: 'MQTT控制',
: '关闭',
: '开启可以通过MQTT控制微信',
},
},
{
fields: {
: 'MQTT推送',
: '关闭',
: '开启后消息会发送到MQTT队列',
},
},
],
}
// const recordsConfig = [{
// fields: {
// 智能问答: '关闭',
// 对话平台token: '',
// 不同群个性回复: '关闭',
// 群白名单: '关闭',
// 好友白名单: '关闭',
// 消息上传到维格表: '开启',
// IM对话: '关闭',
// puppet: 'wechaty-puppet-xp',
// 'wechaty-token': '',
// MQTT控制: '关闭',
// MQTT推送: '关闭',
// MQTT用户名: '',
// MQTT密码: '',
// MQTT接入地址: '',
// MQTT端口号: '1883',
// },
// }]
const contactSheet: Sheet = {
fields: [
{
name: 'id',
type: 'SingleText',
property: {
},
},
{
name: 'name',
type: 'SingleText',
property: {
defaultValue: '',
},
},
{
name: 'alias',
type: 'SingleText',
property: {
defaultValue: '',
},
},
{
name: 'gender',
type: 'SingleText',
property: {
defaultValue: '',
},
},
{
name: 'friend',
type: 'Checkbox',
property: {
icon: 'white_check_mark',
},
},
{
name: 'type',
type: 'SingleText',
property: {
defaultValue: '',
},
},
{
name: 'avatar',
type: 'Text',
},
{
name: 'phone',
type: 'SingleText',
property: {
defaultValue: '',
},
},
{
name: 'file',
type: 'Attachment',
},
],
name: '好友列表',
defaultRecords: [],
}
const qaSheet = {
fields: [
{
name: '分类(必填)',
type: 'Text',
},
{
name: '问题(必填)',
type: 'Text',
},
{
name: '问题阈值(选填-默认0.9)',
type: 'Text',
},
{
name: '相似问题(多个用##分隔)',
type: 'Text',
},
{
name: '机器人回答(多个用##分隔)',
type: 'Text',
},
{
name: '是否停用(选填-默认FALSE)',
type: 'Text',
},
],
name: '智能问答列表',
defaultRecords: [
{
fields: {
'分类(必填)': '社区通知',
'问题(必填)': '社区通知',
'问题阈值(选填-默认0.9)': '0.7',
'相似问题(多个用##分隔)': '社区状态通知##社区里的通知##社区通知,急##看社区通知##社区服务通知##社区公示##社区公告',
'机器人回答(多个用##分隔)': '{"multimsg":["Easy Chatbot Show25108313781@chatroom北辰香麓欣麓园社区公告点击链接查看详情https://spcp52tvpjhxm.com.vika.cn/share/shrsf3Sf0BHitZlU62C0N"]}',
'是否停用(选填-默认FALSE)': 'false',
},
},
{
fields: {
'分类(必填)': '基础问答',
'问题(必填)': 'What is Wechaty',
'问题阈值(选填-默认0.9)': '0.7',
'相似问题(多个用##分隔)': "what'swechaty",
'机器人回答(多个用##分隔)': '{"multimsg":["Wechaty is an Open Source software application for building chatbots.LINE_BREAKGo to the https://wechaty.js.org/docs/wechaty for more information."]}',
'是否停用(选填-默认FALSE)': 'false',
},
},
],
}
const roomListSheet = {
fields: [
{
name: 'id',
type: 'SingleText',
property: {
},
},
{
id: 'fldgEKH5CjXu7',
name: 'topic',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldu000ieNIL3',
name: 'ownerId',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldP5pXik9Tw0',
name: 'avatar',
type: 'Text',
editable: true,
},
{
id: 'fldWB1gC5mrrg',
name: 'adminIdList',
type: 'Text',
editable: true,
},
{
id: 'fld95m7IYPajP',
name: 'memberIdList',
type: 'Text',
editable: true,
},
{
id: 'fldYg3WRl6auV',
name: 'external',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldarlC9hzslN',
name: 'file',
type: 'Attachment',
editable: true,
},
],
name: '群列表',
defaultRecords: [],
}
const roomWhiteListSheet = {
fields: [
{
id: 'fldxEzzn8r5ox',
name: '群ID',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
isPrimary: true,
},
{
id: 'fld9s9Sz7kmo3',
name: '群名称',
type: 'SingleText',
property: {
},
editable: true,
},
{
id: 'fldaic9DJDnZG',
name: '群主昵称',
type: 'SingleText',
property: {
},
editable: true,
},
{
id: 'fldSujdkqvifr',
name: '群主微信号',
type: 'SingleText',
property: {
},
editable: true,
},
{
id: 'fldKKH4aUXsWd',
name: '备注',
type: 'Text',
editable: true,
},
],
name: '群白名单',
defaultRecords: [],
}
const contactWhiteListSheet = {
fields: [
{
id: 'fldxEzzn8r5ox',
name: '好友ID',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
isPrimary: true,
},
{
id: 'fld9s9Sz7kmo3',
name: '昵称',
type: 'SingleText',
property: {
},
editable: true,
},
{
id: 'fldKKH4aUXsWd',
name: '备注',
type: 'Text',
editable: true,
},
],
name: '好友白名单',
defaultRecords: [],
}
const messageSheet = {
fields: [
{
name: 'timeHms',
property: {
defaultValue: '',
},
type: 'SingleText',
},
{
name: 'name',
property: {
defaultValue: '',
},
type: 'SingleText',
},
{
name: 'topic',
property: {
defaultValue: '',
},
type: 'SingleText',
},
{
name: 'messagePayload',
type: 'Text',
},
{
name: 'wxid',
property: {
defaultValue: '',
},
type: 'SingleText',
},
{
name: 'roomid',
property: {
defaultValue: '',
},
type: 'SingleText',
},
{
name: 'messageType',
property: {
defaultValue: '',
},
type: 'SingleText',
},
{
name: 'file',
type: 'Attachment',
},
],
name: '消息记录',
defaultRecords: [],
}
type Sheets = {
[key: string]: Sheet
}
const sheets: Sheets = {
configSheet,
contactSheet,
roomListSheet,
commandSheet,
messageSheet,
qaSheet,
roomWhiteListSheet,
switchSheet,
contactWhiteListSheet,
}
export {
sheets,
type Sheets,
}
export default sheets

View File

@ -1,164 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const recordRes = {
code: 200,
success: true,
data: {
total: 7,
records: [
{
recordId: 'recUqpQPkMrQO',
createdAt: 1670218148000,
updatedAt: 1670218148000,
fields: {
: '更新配置',
: '系统指令',
: '更新系统配置,更改配置后需主动更新一次配置配置才会生效',
},
},
{
recordId: 'reck2x7TobP0D',
createdAt: 1670218148000,
updatedAt: 1671812102000,
fields: {
: '更新白名单',
: '系统指令',
: 'TBD更新群白名单白名单变动时需主动更新白名单',
},
},
{
recordId: 'recuczAHqUTOv',
createdAt: 1670218148000,
updatedAt: 1671812106000,
fields: {
: '更新问答',
: '系统指令',
: 'TBD更新微信对话平台中的问答列表',
},
},
{
recordId: 'recZi3MqRfoLP',
createdAt: 1670218148000,
updatedAt: 1671812111000,
fields: {
: '更新机器人',
: '系统指令',
: 'TBD更新机器人的群列表和好友列表',
},
},
{
recordId: 'recRr9P8QmRyA',
createdAt: 1670218148000,
updatedAt: 1671812116000,
fields: {
: '启用问答',
: '群指令',
: 'TBD当前群启用智能问答',
},
},
{
recordId: 'rec0Ya8vDiV86',
createdAt: 1670218148000,
updatedAt: 1671812120000,
fields: {
: '关闭问答',
: '群指令',
: 'TBD当前群关闭智能问答',
},
},
{
recordId: 'receuIOdNUz8T',
createdAt: 1671682513000,
updatedAt: 1671682529000,
fields: {
: '更新提醒',
: '系统指令',
: '更新通知提醒任务',
},
},
],
pageNum: 1,
pageSize: 7,
},
message: 'SUCCESS',
}
const defaultRecords: any[] = recordRes.data.records
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldCSCcQI7iEm',
name: '指令名称',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
isPrimary: true,
},
{
id: 'fldplyGKKgxME',
name: '说明',
type: 'Text',
editable: true,
},
{
id: 'fldE1R4eb6E8S',
name: '管理员微信号',
type: 'SingleText',
property: {},
editable: true,
},
{
id: 'fldopkUTne42Y',
name: '类型',
type: 'SingleSelect',
property: {
options: [
{
id: 'optYAC1AnMzGf',
name: '系统指令',
color: {
name: 'deepPurple_0',
value: '#E5E1FC',
},
},
{
id: 'opt7qitH0LOpj',
name: '群指令',
color: {
name: 'indigo_0',
value: '#DDE7FF',
},
},
],
},
editable: true,
},
],
},
message: 'SUCCESS',
}
const fields: Field[] = vikaRes.data.fields
const commandSheet: Sheet = {
fields,
name: '指令列表',
defaultRecords,
}
export {
commandSheet,
}
export default commandSheet

View File

@ -1,106 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldDJcm8sDIt0',
name: 'id',
type: 'SingleText',
property: {},
editable: true,
isPrimary: true,
},
{
id: 'fld39evAmfVMb',
name: 'name',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldY9rUAthDEm',
name: 'alias',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldCbZKiBXklM',
name: 'gender',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldK2fgxvTDcs',
name: 'friend',
type: 'Checkbox',
property: {
icon: '✅',
},
editable: true,
},
{
id: 'fld96axLmYqKU',
name: 'type',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldfwvJnJIS4i',
name: 'avatar',
type: 'Text',
editable: true,
},
{
id: 'fldZveatV4H1Z',
name: 'phone',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldpm0jUT1Ch6',
name: 'file',
type: 'Attachment',
editable: true,
},
],
},
message: 'SUCCESS',
}
const defaultRecords: any[] = []
const fields: Field[] = vikaRes.data.fields
const contactSheet: Sheet = {
fields,
name: '好友列表',
defaultRecords,
}
export {
contactSheet,
}
export default contactSheet

View File

@ -1,59 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldgPYseCnwqz',
name: '分组名称',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
isPrimary: true,
},
{
id: 'fld77XnB5JGWf',
name: '联系人',
type: 'MagicLink',
property: {
foreignDatasheetId: 'dstbutP3T8WorWLlbq',
brotherFieldId: 'fldsnqqpYglrI',
},
editable: true,
desc: '好友列表',
},
{
id: 'fldTE6BPvtD7h',
name: '备注',
type: 'Text',
editable: true,
},
],
},
message: 'SUCCESS',
}
const defaultRecords: any[] = []
const fields: Field[] = vikaRes.data.fields
const groupSheet: Sheet = {
fields,
name: '好友分组',
defaultRecords,
}
export {
groupSheet,
}
export default groupSheet

View File

@ -1,55 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldVWq6zteuyr',
name: '好友ID',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
isPrimary: true,
},
{
id: 'fldh6BOfEyKn0',
name: '昵称',
type: 'SingleText',
property: {},
editable: true,
},
{
id: 'fldFv7pl3973t',
name: '备注',
type: 'Text',
editable: true,
},
],
},
message: 'SUCCESS',
}
const defaultRecords: any[] = []
const fields: Field[] = vikaRes.data.fields
const contactWhiteListSheet: Sheet = {
fields,
name: '好友白名单',
defaultRecords,
}
export {
contactWhiteListSheet,
}
export default contactWhiteListSheet

View File

@ -1,248 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const recordRes = {
code: 200,
success: true,
data: {
total: 13,
records: [
{
recordId: 'recm3YUoiY3lX',
fields: {
: '基本配置',
: 'base',
: '管理群',
: 'adminRoomTopic',
'值(只修改此列)': '大师是群主',
: '管理群名称,需尽量保持名称复杂,避免重名群干扰',
},
},
{
recordId: 'recrEIHXFV14w',
createdAt: 1671304478000,
updatedAt: 1671308763000,
fields: {
: 'WechatyPuppet',
: 'puppetName',
: 'Wechaty',
: 'wechaty',
: '可选值:\nwechaty-puppet-wechat4u\nwechaty-puppet-wechat\nwechaty-puppet-xp\nwechaty-puppet-padlocal\nwechaty-puppet-service',
'值(只修改此列)': 'wechaty-puppet-wechat4u',
},
},
{
recordId: 'rec99fo7LJIXP',
createdAt: 1671304478000,
updatedAt: 1671308940000,
fields: {
: 'WechatyToken',
: 'puppetToken',
: 'Wechaty',
: 'wechaty',
: '使用wechaty-puppet-padlocal、wechaty-puppet-service时需配置此token',
},
},
{
recordId: 'recinVcKkDT4g',
createdAt: 1671304478000,
updatedAt: 1671306617000,
fields: {
: 'AI对话平台Type',
: 'aiType',
: '自动问答',
: 'auto-qa',
: 'TODO-可选值:\nWxOpenai\nChatGPT',
'值(只修改此列)': 'WxOpenai',
},
},
{
recordId: 'reca02j4zeJJO',
createdAt: 1671304478000,
updatedAt: 1671304478000,
fields: {
: '微信对话开放平台Token',
: 'WX_TOKEN',
: '微信开放对话平台',
: 'wx-open-ai',
: '微信对话开放平台中获取',
},
},
{
recordId: 'recDs5CswG6Y2',
createdAt: 1671304478000,
updatedAt: 1671304478000,
fields: {
: '微信对话开放平台EncodingAESKey',
: 'EncodingAESKey',
: '智能问答',
: '微信对话开放平台中获取',
},
},
{
recordId: 'rec5Mjc4E6GjK',
createdAt: 1671304478000,
updatedAt: 1671304478000,
fields: {
: 'ChatGPTAEmail',
: 'ChatGPTAEmail',
: '智能问答',
: 'TODO',
},
},
{
recordId: 'recN4gbSUoWIa',
createdAt: 1671304478000,
updatedAt: 1671304478000,
fields: {
: 'ChatGPTAPassword',
: 'ChatGPTAPassword',
: '智能问答',
: 'TODO',
},
},
{
recordId: 'rechhkGPqXzo6',
createdAt: 1671304478000,
updatedAt: 1671304478000,
fields: {
: 'ChatGPTASessionToken',
: 'ChatGPTASessionToken',
: '智能问答',
: 'TODO',
},
},
{
recordId: 'recos1u8VvHuQ',
createdAt: 1671304478000,
updatedAt: 1671304478000,
fields: {
: 'MQTT用户名',
: 'mqttUsername',
: 'MQTT连接',
: 'MQTT连接配置信息推荐使用百度云的物联网核心套件',
},
},
{
recordId: 'rechxZI6WS5Uq',
createdAt: 1671304478000,
updatedAt: 1671304478000,
fields: {
: 'MQTT密码',
: 'mqttPassword',
: 'MQTT连接',
: 'MQTT连接配置信息推荐使用百度云的物联网核心套件',
},
},
{
recordId: 'recB2MNTLz9zM',
createdAt: 1671304480000,
updatedAt: 1671304480000,
fields: {
: 'MQTT接入地址',
: 'mqttEndpoint',
: 'MQTT连接',
: 'MQTT连接配置信息推荐使用百度云的物联网核心套件',
},
},
{
recordId: 'recqXfHERfj3b',
createdAt: 1671304480000,
updatedAt: 1671304480000,
fields: {
: 'MQTT端口号',
: 'mqttPort',
: 'MQTT连接',
: 'MQTT连接配置信息推荐使用百度云的物联网核心套件',
'值(只修改此列)': '1883',
},
},
{
recordId: 'rec8prGUMpMiw',
createdAt: 1671304480000,
updatedAt: 1671304480000,
fields: {
: 'WebHook地址',
: 'WEB_HOOK',
: '消息推送',
: 'TODO-格式 http://baidu.com/abc,多个地址使用英文逗号隔开使用post请求推送',
},
},
],
pageNum: 1,
pageSize: 13,
},
message: 'SUCCESS',
}
const defaultRecords: any[] = recordRes.data.records
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldswxMTbHJwr',
name: '配置组',
type: 'SingleText',
property: {},
editable: true,
isPrimary: true,
},
{
id: 'fldswxMTbHJwr',
name: '配置组标识',
type: 'SingleText',
property: {},
editable: true,
isPrimary: true,
},
{
id: 'fldlCUQ2Aju1Y',
name: '配置项',
type: 'SingleText',
property: {},
editable: true,
},
{
id: 'fldDrMTuWCuCM',
name: '标识',
type: 'SingleText',
property: {},
editable: true,
},
{
id: 'fld6GYkhQCQ7m',
name: '值(只修改此列)',
type: 'Text',
editable: true,
},
{
id: 'fldpD6BA5xeZf',
name: '说明',
type: 'Text',
editable: true,
},
],
},
message: 'SUCCESS',
}
const fields: Field[] = vikaRes.data.fields
const configSheet: Sheet = {
fields,
name: '环境变量',
defaultRecords,
}
export {
configSheet,
}
export default configSheet

View File

@ -1,99 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldr8wtGTGr4o',
name: 'timeHms',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
isPrimary: true,
},
{
id: 'fldIDa0zPtgYo',
name: 'name',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldCbOzc2qfVn',
name: 'topic',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldQYW3U9dvKm',
name: 'messagePayload',
type: 'Text',
editable: true,
},
{
id: 'fldJ9S09Ib9ZT',
name: 'wxid',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldH7x4REKsrD',
name: 'roomid',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldiRwFyYEIYX',
name: 'messageType',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldh1g0q0rx9M',
name: 'file',
type: 'Attachment',
editable: true,
},
],
},
message: 'SUCCESS',
}
const defaultRecords: any[] = []
const fields: Field[] = vikaRes.data.fields
const messageSheet: Sheet = {
fields,
name: '消息记录',
defaultRecords,
}
export {
messageSheet,
}
export default messageSheet

View File

@ -1,47 +0,0 @@
enum FieldType {
SingleText = 'SingleText',
SingleSelect = 'SingleSelect',
Text = 'Text',
Attachment = 'Attachment'
}
type Field = {
id?:string,
name: string,
type: string,
property?: any,
desc?: string,
editable?: boolean,
isPrimary?: boolean,
};
type FieldSingleText = Field & { type: FieldType.SingleText };
type FieldSingleSelect = Field & { type: FieldType.SingleSelect };
type FieldText = Field & { type: FieldType.Text };
type Record = {
fields: {
[key: string]: string
}
}
type Sheet = {
fields: Field[],
name: string,
defaultRecords: Record[]
}
type Sheets = {
[key: string]: Sheet
}
export {
FieldType,
type Field,
type FieldSingleText,
type FieldSingleSelect,
type FieldText,
type Record,
type Sheet,
type Sheets,
}

View File

@ -1,205 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldoYSvRnvc1f',
name: '内容',
type: 'Text',
editable: true,
isPrimary: true,
},
{
id: 'fldfqyrfkWBBy',
name: '接收好友',
type: 'MagicLink',
property: {
foreignDatasheetId: 'dstbutP3T8WorWLlbq',
brotherFieldId: 'fld0reGOMyTGV',
},
editable: true,
desc: '好友列表',
},
// {
// "id": "fldXq00SDGvS9",
// "name": "接收分组",
// "type": "MagicLink",
// "property": {
// "foreignDatasheetId": "dsttzJxMEqxZ0m5UHZ",
// "brotherFieldId": "fldNqvOAzEkxC"
// },
// "editable": true,
// "desc": "好友分组"
// },
{
id: 'fldJ1TvTV1T8c',
name: '接收群',
type: 'MagicLink',
property: {
foreignDatasheetId: 'dstRVUymHGd1e4mrWU',
brotherFieldId: 'flduYxMKg3ERW',
},
editable: true,
desc: '群列表',
},
{
id: 'fldoCm0thVXmq',
name: '时间',
type: 'DateTime',
property: {
format: 'YYYY/MM/DD HH:mm',
includeTime: true,
},
editable: true,
},
{
id: 'fldTnEtGqFIt6',
name: '周期',
type: 'SingleSelect',
property: {
options: [
{
id: 'optrcyujqFycE',
name: '无重复',
color: {
name: 'deepPurple_0',
value: '#E5E1FC',
},
},
{
id: 'optpUa6oOH7mb',
name: '每天',
color: {
name: 'indigo_0',
value: '#DDE7FF',
},
},
{
id: 'opt1PXrPyuWwu',
name: '每周',
color: {
name: 'blue_0',
value: '#DDF5FF',
},
},
{
id: 'optiiAF9BNYKj',
name: '每月',
color: {
name: 'yellow_0',
value: '#FFF6D8',
},
},
{
id: 'optnWPpccOnnb',
name: '每小时',
color: {
name: 'teal_0',
value: '#D6F3E8',
},
},
{
id: 'optrcSxCfZzyR',
name: '每分钟',
color: {
name: 'green_0',
value: '#DCF3D1',
},
},
{
id: 'optt9JWn7cSbF',
name: '每5分钟',
color: {
name: 'orange_0',
value: '#FFEECC',
},
},
{
id: 'optkEeIO3oiGP',
name: '每10分钟',
color: {
name: 'tangerine_0',
value: '#FFE4CC',
},
},
{
id: 'opt6FET9p070f',
name: '每15分钟',
color: {
name: 'pink_0',
value: '#FFE2E8',
},
},
{
id: 'optWUcO5sbqGN',
name: '每30分钟',
color: {
name: 'red_0',
value: '#F9D8D7',
},
},
{
id: 'optQuO5UYFHrZ',
name: '每季度',
color: {
name: 'deepPurple_0',
value: '#E5E1FC',
},
},
],
},
editable: true,
},
{
id: 'fldiC33Rgidk5',
name: '启用状态',
type: 'SingleSelect',
property: {
options: [
{
id: 'optJAukD9h9vd',
name: '开启',
color: {
name: 'deepPurple_0',
value: '#E5E1FC',
},
},
{
id: 'optXdfUlXCcYG',
name: '关闭',
color: {
name: 'indigo_0',
value: '#DDE7FF',
},
},
],
},
editable: true,
},
],
},
message: 'SUCCESS',
}
const defaultRecords: any[] = []
const fields: Field[] = vikaRes.data.fields
const noticeSheet: Sheet = {
fields,
name: '通知提醒',
defaultRecords,
}
export {
noticeSheet,
}
export default noticeSheet

View File

@ -1,108 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const recordRes = {
code: 200,
success: true,
data: {
total: 2,
records: [
{
recordId: 'rec0HdqkdrCsy',
createdAt: 1670218155000,
updatedAt: 1670218155000,
fields: {
'是否停用(选填-默认FALSE)': 'false',
'相似问题(多个用##分隔)': '社区状态通知##社区里的通知##社区通知,急##看社区通知##社区服务通知##社区公示##社区公告',
'问题(必填)': '社区通知',
'分类(必填)': '社区通知',
'机器人回答(多个用##分隔)': '{"multimsg":["Easy Chatbot Show25108313781@chatroom北辰香麓欣麓园社区公告点击链接查看详情https://spcp52tvpjhxm.com.vika.cn/share/shrsf3Sf0BHitZlU62C0N"]}',
'问题阈值(选填-默认0.9)': '0.7',
},
},
{
recordId: 'recnsUhNejZb8',
createdAt: 1670218155000,
updatedAt: 1670218155000,
fields: {
'是否停用(选填-默认FALSE)': 'false',
'相似问题(多个用##分隔)': "what'swechaty",
'问题(必填)': 'What is Wechaty',
'分类(必填)': '基础问答',
'机器人回答(多个用##分隔)': '{"multimsg":["Wechaty is an Open Source software application for building chatbots.LINE_BREAKGo to the https://wechaty.js.org/docs/wechaty for more information."]}',
'问题阈值(选填-默认0.9)': '0.7',
},
},
],
pageNum: 1,
pageSize: 2,
},
message: 'SUCCESS',
}
const defaultRecords: any[] = recordRes.data.records
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldho5KU1iGhU',
name: '分类(必填)',
type: 'Text',
editable: true,
isPrimary: true,
},
{
id: 'fldawxa55XjvG',
name: '问题(必填)',
type: 'Text',
editable: true,
},
{
id: 'fldzhaT6r2uTU',
name: '问题阈值(选填-默认0.9)',
type: 'Text',
editable: true,
},
{
id: 'fldREsAJJLzzT',
name: '相似问题(多个用##分隔)',
type: 'Text',
editable: true,
},
{
id: 'fldn2DNCN15xy',
name: '机器人回答(多个用##分隔)',
type: 'Text',
editable: true,
},
{
id: 'fldCNMdtG6kcO',
name: '是否停用(选填-默认FALSE)',
type: 'Text',
editable: true,
},
],
},
message: 'SUCCESS',
}
const fields: Field[] = vikaRes.data.fields
const qaSheet: Sheet = {
fields,
name: '智能问答列表',
defaultRecords,
}
export {
qaSheet,
}
export default qaSheet

View File

@ -1,91 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fld0DNOa8mCoC',
name: 'id',
type: 'SingleText',
property: {},
editable: true,
isPrimary: true,
},
{
id: 'fldr1dFmmZd0y',
name: 'topic',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fld36Of6QwGZy',
name: 'ownerId',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fld96Wo2Jn0tW',
name: 'avatar',
type: 'Text',
editable: true,
},
{
id: 'fldyW6b6RLGNC',
name: 'adminIdList',
type: 'Text',
editable: true,
},
{
id: 'fldLSc8IyEw7t',
name: 'memberIdList',
type: 'Text',
editable: true,
},
{
id: 'fldrULE0yzHXN',
name: 'external',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
},
{
id: 'fldZ0MvElgzQX',
name: 'file',
type: 'Attachment',
editable: true,
},
],
},
message: 'SUCCESS',
}
const defaultRecords: any[] = []
const fields: Field[] = vikaRes.data.fields
const roomListSheet: Sheet = {
fields,
name: '群列表',
defaultRecords,
}
export {
roomListSheet,
}
export default roomListSheet

View File

@ -1,55 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldgii4niM8aw',
name: '群ID',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
isPrimary: true,
},
{
id: 'flddQopavELXi',
name: '群名称',
type: 'SingleText',
property: {},
editable: true,
},
{
id: 'fldQlUE6uw9HV',
name: '备注',
type: 'Text',
editable: true,
},
],
},
message: 'SUCCESS',
}
const defaultRecords: any[] = []
const fields: Field[] = vikaRes.data.fields
const roomWhiteListSheet: Sheet = {
fields,
name: '群白名单',
defaultRecords,
}
export {
roomWhiteListSheet,
}
export default roomWhiteListSheet

View File

@ -1,221 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheet,
Field,
} from './Model'
const recordRes = {
code: 200,
success: true,
data: {
total: 10,
records: [
{
recordId: 'recf6KOk48YKn',
createdAt: 1671302940000,
updatedAt: 1671303805000,
fields: {
: '开启后可以使用微信对话平台只能问答',
: '智能问答',
: 'WX_OPENAI_ONOFF',
'启用状态(只修改此列)': '关闭',
: '智能问答',
},
},
{
recordId: 'recYniqLy8b8D',
createdAt: 1671302940000,
updatedAt: 1671303814000,
fields: {
: '开启后只有@机器人时才会回复问答',
: 'AT回复',
: 'AT_AHEAD',
'启用状态(只修改此列)': '开启',
: '智能问答',
},
},
{
recordId: 'recn53pOPa3Fu',
createdAt: 1671302940000,
updatedAt: 1671303822000,
fields: {
: '开启后不同群相同问题可以设置不同的回答',
: '不同群个性回复',
: 'DIFF_REPLY_ONOFF',
'启用状态(只修改此列)': '开启',
: '智能问答',
},
},
{
recordId: 'recPdt5BOLiXq',
createdAt: 1671302940000,
updatedAt: 1671303837000,
fields: {
: '开启后只对白名单内的群消息进行自动问答',
: '群白名单',
: 'roomWhiteListOpen',
'启用状态(只修改此列)': '开启',
: '智能问答',
},
},
{
recordId: 'recHv8B2IaofP',
createdAt: 1671302940000,
updatedAt: 1671303853000,
fields: {
: '开启后只对白名单内的好友消息进行自动问答',
: '好友白名单',
: 'contactWhiteListOpen',
'启用状态(只修改此列)': '开启',
: '智能问答',
},
},
{
recordId: 'recXEwHZSAATR',
createdAt: 1671302940000,
updatedAt: 1671303860000,
fields: {
: '开启后消息记录会自动上传到维格表的【消息记录】表',
: '消息上传到维格表',
: 'VIKA_ONOFF',
'启用状态(只修改此列)': '开启',
: '消息推送',
},
},
{
recordId: 'rec1WdnWXsyPo',
createdAt: 1671302940000,
updatedAt: 1671303946000,
fields: {
: 'TODO-开启后系统将机器人事件消息推送到指定的地址',
: 'WebHook推送',
: 'WEB_HOOK_ONOFF',
'启用状态(只修改此列)': '关闭',
: '消息推送',
},
},
{
recordId: 'recjMAPK1OZbT',
createdAt: 1671302940000,
updatedAt: 1671303880000,
fields: {
: '开启后消息会发送到MQTT队列需要先配置MQTT配置项',
: 'MQTT推送',
: 'mqtt_PUB_ONOFF',
'启用状态(只修改此列)': '关闭',
: '消息推送',
},
},
{
recordId: 'recESlHvyEPcj',
createdAt: 1671302940000,
updatedAt: 1671303873000,
fields: {
: '开启可以通过MQTT控制微信需要先配置MQTT配置项',
: 'MQTT控制',
: 'mqtt_SUB_ONOFF',
'启用状态(只修改此列)': '关闭',
: '远程控制',
},
},
{
recordId: 'recumi1YTrUAq',
createdAt: 1671302940000,
updatedAt: 1671303906000,
fields: {
: '开启后可以使用客服对话系统需先手动启用IM服务',
: 'IM对话',
: 'imOpen',
'启用状态(只修改此列)': '关闭',
: '客服系统',
},
},
],
pageNum: 1,
pageSize: 10,
},
message: 'SUCCESS',
}
const defaultRecords: any[] = recordRes.data.records
const vikaRes = {
code: 200,
success: true,
data: {
fields: [
{
id: 'fldq84eKS9Cyq',
name: '配置组',
type: 'SingleText',
property: {
defaultValue: '',
},
editable: true,
isPrimary: true,
},
{
id: 'fldPp0bwSk84x',
name: '功能项',
type: 'SingleText',
property: {},
editable: true,
},
{
id: 'fldZ0kmh0WQTh',
name: '标识',
type: 'SingleText',
property: {},
editable: true,
},
{
id: 'fldmndbxeLd37',
name: '启用状态(只修改此列)',
type: 'SingleSelect',
property: {
options: [
{
id: 'opt4DXQURFJQf',
name: '开启',
color: {
name: 'deepPurple_0',
value: '#E5E1FC',
},
},
{
id: 'optvpgML9rza6',
name: '关闭',
color: {
name: 'indigo_0',
value: '#DDE7FF',
},
},
],
},
editable: true,
},
{
id: 'fldAKw21lBTlb',
name: '说明',
type: 'Text',
editable: true,
},
],
},
message: 'SUCCESS',
}
const fields: Field[] = vikaRes.data.fields
const switchSheet: Sheet = {
fields,
name: '功能开关',
defaultRecords,
}
export {
switchSheet,
}
export default switchSheet

View File

@ -1,35 +0,0 @@
/* eslint-disable sort-keys */
import type {
Sheets,
} from './Model'
import messageSheet from './Message.js'
import commandSheet from './CommandList.js'
import configSheet from './EnvConfig.js'
import switchSheet from './Switch.js'
import contactSheet from './Contact.js'
// import qaSheet from './QaList.js'
import roomListSheet from './Room.js'
import roomWhiteListSheet from './RoomWhiteList.js'
import contactWhiteListSheet from './ContactWhiteList.js'
import noticeSheet from './Notice.js'
// import groupSheet from './ContactGroup.js'
const sheets: Sheets = {
configSheet,
switchSheet,
commandSheet,
contactSheet,
roomListSheet,
// qaSheet,
roomWhiteListSheet,
contactWhiteListSheet,
// groupSheet,
noticeSheet,
messageSheet,
}
export {
sheets,
}
export default sheets

View File

@ -1,248 +0,0 @@
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
// 导入Wechaty相关模块
import {
Contact,
Message,
Room,
ScanStatus,
WechatyBuilder,
log,
} from 'wechaty'
import { FileBox } from 'file-box'
// 导入html-to-docx模块
import htmlToDocx from 'html-to-docx'
import qrcodeTerminal from 'qrcode-terminal'
// 导入语雀相关模块
import Yuque from '@yuque/sdk'
// 创建一个语雀客户端对象使用环境变量中的token和repoId需要提前设置
const yuqueClient = new Yuque({
token:process.env['YUQUE_TOKEN'] || 'xxx',
})
const repoId = process.env['YUQUE_REPO_ID'] || 'xxx'
// 创建一个Wechaty实例
// const bot = WechatyBuilder.build({
// name: 'meeting-bot',
// })
const bot = WechatyBuilder.build({
name: 'ding-dong-bot',
puppet: 'wechaty-puppet-xp',
})
type Meeting = {
isMeeting:boolean
meetingLog:string
meetingLogDoc:string
title:string
room:Room
}
type Meetings = {
[key:string]:Meeting
}
let meetings:Meetings
// 定义一个函数处理扫码登录事件
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('MeetingBot', 'onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl)
// 在终端显示二维码图片
qrcodeTerminal.generate(qrcode, { small: true })
} else {
log.info('MeetingBot', 'onScan: %s(%s)', ScanStatus[status], status)
}
}
// 定义一个函数处理登录事件
function onLogin (user: Contact) {
log.info('MeetingBot', '%s login', user)
}
// 定义一个函数处理登出事件
function onLogout (user: Contact) {
log.info('MeetingBot', '%s logout', user)
}
// 定义一个异步函数处理消息事件
async function onMessage (msg: Message) {
// 获取消息发送者和内容
const sender = msg.talker()
const text = msg.text()
// 如果消息来自群聊,并且内容是 #开会 ,则开始记录会议信息,并回复“开始记录”
if (msg.room() && msg.room() !== undefined && text === '#开会') {
// 判断当前群是否在会议中
const meetingRoom = msg.room()
if (meetingRoom) {
if (meetings[meetingRoom.id] && meetings[meetingRoom.id]?.isMeeting) {
// 回复“已经在会议中”
await msg.say('会议进行中')
} else {
// 设置当前状态为在会议中,并保存当前群聊对象和时间戳为会议标题
const time = new Date().toLocaleString()
const meetingLog = `## 开始时间\n ${time}\n\n## 内容记录\n`
const meetingLogDoc = `<h2>开始时间</h2><br> ${time}<br><h2>内容记录</h2><br>`
const title = await meetingRoom.topic() + '-' + time
const meeting:Meeting = {
isMeeting:true,
meetingLog,
meetingLogDoc,
room:meetingRoom,
title,
}
meetings[meetingRoom.id] = meeting
// 回复“开始记录”
await msg.say('开始记录')
}
}
}
// 如果消息来自群聊,并且内容是 #结束 ,则结束记录会议信息,并回复“结束记录”
if (msg.room() && msg.room() !== undefined && text === '#结束') {
// 设置当前状态为不在会议中,并清空当前群聊对象和时间戳为会议标题
const meetingRoom = msg.room()
if (meetingRoom) {
if (meetings[meetingRoom.id] && meetings[meetingRoom.id]?.isMeeting) {
// 设置当前状态为在会议中,并保存当前群聊对象和时间戳为会议标题
meetings[meetingRoom.id]!.isMeeting = false
// 回复“结束记录”
await msg.say('结束记录')
} else {
// 回复“未在会议中,发送 #开会 开始会议”
await msg.say('未在会议中,发送 #开会 开始会议')
}
}
}
// 如果消息来自群聊,并且内容是 #会议纪要 ,则导出会议期间的聊天记录到语雀文档中,并回复文档链接或错误信息。
if (msg.room() && msg.room() !== undefined && text === '#会议纪要') {
const meetingRoom = msg.room()
if (meetingRoom) {
// 判断当前群是否在会议中
if (meetings[meetingRoom.id] && meetings[meetingRoom.id]?.isMeeting) {
// 回复“已经在会议中”
await msg.say('先发送 #结束 结束会议之后再导出会议纪要')
} else if (!meetings[meetingRoom.id]) {
// 回复“没有可导出的会议纪要”
await msg.say('没有可导出的会议纪要')
} else {
// 导出会议纪要
const meeting = meetings[meetingRoom.id]
const slug = `unittest_create_${Date.now()}`
const data = {
body: meeting?.meetingLog,
public: 1,
slug,
title:meeting?.title,
}
try {
// 调用语雀API创建一
// 调用语雀API创建一个文档使用会议标题和聊天记录作为参数
const doc = await yuqueClient.docs.create({
data,
namespace: repoId,
})
// 获取文档的链接地址,并回复给群聊
const docUrl = `https://www.yuque.com/${repoId}/${doc.slug}`
await msg.say(`会议纪要已导出到语雀文档:${docUrl}`)
delete meetings[meetingRoom.id]
} catch (err) {
// 如果出现错误,打印错误信息,并回复给群聊
log.error('MeetingBot', err)
await msg.say('导出会议纪要失败,请重试~')
}
}
}
}
// 如果消息来自群聊,并且内容是 #会议纪要 ,则导出会议期间的聊天记录到语雀文档中,并回复文档链接或错误信息。
if (msg.room() && msg.room() !== undefined && text === '#会议纪要文档') {
const meetingRoom = msg.room()
if (meetingRoom) {
// 判断当前群是否在会议中
if (meetings[meetingRoom.id] && meetings[meetingRoom.id]?.isMeeting) {
// 回复“已经在会议中”
await msg.say('先发送 #结束 结束会议之后再导出会议纪要')
} else if (!meetings[meetingRoom.id]) {
// 回复“没有可导出的会议纪要”
await msg.say('没有可导出的会议纪要')
} else {
// 导出会议纪要
const meeting:Meeting|undefined = meetings[meetingRoom.id]
try {
// 使用html-to-docx库将会议聊天信息转换为word文档的buffer对象
const buffer = await htmlToDocx(meeting?.meetingLogDoc)
const reg = /[^a-zA-Z0-9]/g
// 将buffer对象转换为FileBox对象用于发送文件
const fileBox = FileBox.fromBuffer(buffer, `${meeting?.title.replace(reg, '')}.docx`)
// 发送文件给群聊
await msg.say(fileBox)
delete meetings[meetingRoom.id]
} catch (err) {
// 如果出现错误,打印错误信息,并回复给群聊
log.error('MeetingBot', err)
await msg.say('导出会议纪要失败,请重试~')
}
}
}
}
// 如果消息来自群聊,并且当前状态是在会议中,则将消息内容追加到聊天记录中
if (msg.room() && msg.room() !== undefined) {
const meetingRoom = msg.room()
if (meetingRoom && meetings[meetingRoom.id] && meetings[meetingRoom.id]?.isMeeting) {
const meeting = meetings[meetingRoom.id]
// 获取消息发送者的昵称和内容,拼接成一行记录,并追加到聊天记录中
const name = sender.name() || '未知用户'
meeting!.meetingLog += `- ${new Date().toLocaleString()} ${name}: ${text}\n`
meeting!.meetingLogDoc += `${new Date().toLocaleString()} <br>${name}: ${text}<br>`
}
}
}
// 绑定事件处理函数到Wechaty实例上
bot.on('scan', onScan)
bot.on('login', onLogin)
bot.on('logout', onLogout)
bot.on('message', onMessage)
// 启动Wechaty实例
bot.start()
.then(() => log.info('MeetingBot', 'Meeting Bot Started.'))
.catch(e => log.error('MeetingBot', e))

View File

@ -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

View File

@ -1,2 +0,0 @@
declare module './chat-device.js'
declare module './msg-format.js'

View File

@ -1,30 +0,0 @@
import { Message, log } from 'wechaty'
import axios from 'axios'
// 获取格式化后的顺风车信息
async function getFormattedRideInfo (message:Message) {
let text: string = message.text()
const name:string = message.talker().name()
const apiUrl = 'https://openai.api2d.net/v1/chat/completions'
const headers = {
Authorization: 'Bearer xxxx', // <-- 把 fkxxxxx 替换成你自己的 Forward Key注意前面的 Bearer 要保留,并且和 Key 中间有一个空格。
'Content-Type': 'application/json',
}
text = `从"发布人:${name}\n信息${text}"中提取出:类型(人找车、车找人)、出发地、目的地、出发日期、出发时间、联系电话、发布人、车费、途经路线,不要输出任何其他的描述`
const payload = {
messages: [ { content: text, role: 'user' } ],
model: 'gpt-3.5-turbo',
}
try {
const response = await axios.post(apiUrl, payload, { headers })
log.info('顺风车信息检测结果:', JSON.stringify(response.data))
return response.data
} catch (error) {
console.error(error)
return undefined
}
}
export { getFormattedRideInfo }

View File

@ -1,109 +0,0 @@
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
import 'dotenv/config.js'
import {
WechatyBuilder,
// types,
} from 'wechaty'
import * as PUPPET from 'wechaty-puppet'
// 导入wechaty-puppet-wechat模块用于连接网页版微信
import { PuppetWeChat } from 'wechaty-puppet-wechat'
// 导入qrcode-terminal模块用于在终端显示二维码
import qrcodeTerminal from 'qrcode-terminal'
// 引入fs模块用于文件操作
import fs from 'fs'
// 定义一个csv文件路径用于存储消息
const csvFile = './messages.csv'
// 定义一个文件夹路径,用于存储图片、视频、文件、语音消息
const mediaFolder = './media'
// 定义一个数组,用于缓存消息
let messages: string[] = []
// 定义一个定时器用于每10秒批量写入消息到csv文件
let timer:any
const wait = (ms: number | undefined) => new Promise(resolve => setTimeout(resolve, ms))
// 使用WechatyBuilder构建一个Wechaty实例并指定使用wechaty-puppet-wechat作为puppet
// const bot = WechatyBuilder.build({
// name: 'ding-dong-bot',
// puppet: 'wechaty-puppet-wechat',
// puppetOptions: {
// uos: true
// }
// })
const bot = WechatyBuilder.build({
name: 'ding-dong-bot',
puppet: 'wechaty-puppet-xp',
})
// 监听扫码事件
bot.on('scan', (qrcode: string, status: number) => {
// 在终端显示二维码,并提示用户扫码登录
qrcodeTerminal.generate(qrcode, { small: true })
// eslint-disable-next-line no-console
console.log(`Scan QR Code to login: ${status}`)
})
// 监听登录事件
bot.on('login', user => {
// eslint-disable-next-line no-console
console.log(`User ${user} logged in`)
})
// 监听消息事件
bot.on('message', async message => {
// 获取消息的发送者、接收者、内容、类型和时间戳
const from = message.talker()
const to = message.listener()
const content = message.text()
const type = message.type()
const timestamp = message.date().getTime()
// 如果消息类型是图片、视频、文件或语音,将其保存到本地文件夹,并获取其存储路径
let path = ''
if (type === PUPPET.types.Message.Image || type === PUPPET.types.Message.Video || type === PUPPET.types.Message.Attachment || type === PUPPET.types.Message.Audio) {
await wait(3000)
try {
// 创建文件夹,如果已存在则忽略错误
fs.mkdirSync(mediaFolder, { recursive: true })
// 获取文件名格式为类型_发送者_时间戳.后缀
const filename = `${type}_${from.name}_${timestamp}.${(await message.toFileBox()).mediaType}`
// 获取文件路径,格式为:文件夹/文件名
path = `${mediaFolder}/${filename}`
// 将消息保存到本地文件夹
await (await message.toFileBox()).toFile(path)
} catch (err) {
console.error(err)
}
}
// 将消息的相关信息添加到缓存数组中,格式为:发送者,接收者,内容,类型,时间戳,路径\n
messages.push(`${from},${to},${content},${type},${timestamp},${path}\n`)
// 如果定时器不存在创建一个定时器每10秒执行一次写入操作
if (!timer) {
timer = setInterval(() => {
// 如果缓存数组不为空将其内容写入到csv文件中并清空缓存数组
if (messages.length > 0) {
// 创建csv文件如果已存在则忽略错误
fs.openSync(csvFile, 'a')
// 将缓存数组的内容写入到csv文件中并处理错误
fs.appendFile(csvFile, messages.join(''), err => {
if (err) {
// eslint-disable-next-line no-console
console.error(err)
} else {
// eslint-disable-next-line no-console
console.log('Messages written to csv file')
}
})
// 清空缓存数组
messages = []
}
}, 10000)
}
})
// 启动Wechaty实例
void bot.start()

View File

@ -1,969 +0,0 @@
/* eslint-disable promise/always-return */
/* eslint-disable no-console */
/* eslint-disable sort-keys */
import { ICreateRecordsReqParams, Vika } from '@vikadata/vika'
import type { ReadStream } from 'fs'
import moment from 'moment'
// import { type } from 'os'
// import { v4 } from 'uuid'
// import rp from 'request-promise'
// import schedule from 'node-schedule'
import fs from 'fs'
import console from 'console'
import {
Contact,
log,
Message,
Room,
ScanStatus,
types,
Wechaty,
} from 'wechaty'
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 { sheets } from './lib/dataModel.js'
type VikaBotConfigTypes = {
spaceName: string,
token: string,
}
class VikaBot {
token!: string
spaceName!: string
vika!: Vika
spaceId!: string
messageSheet: any
commandSheet: any
contactSheet: any
qaSheet: any
roomListSheet: any
configSheet!: string
switchSheet!: string
roomWhiteListSheet!: string
contactWhiteListSheet!: string
noticeSheet!: string
msgStore!: any[]
constructor (config: VikaBotConfigTypes) {
if (!config.token) {
console.error('未配置token请在config.ts中配置')
} else if (!config.spaceName) {
console.error('未配置空间名称请在config.ts中配置')
} else {
this.token = config.token
this.spaceName = config.spaceName
this.vika = new Vika({ token: this.token })
this.spaceId = ''
this.msgStore = []
// this.checkInit()
}
}
async getAllSpaces () {
// 获取当前用户的空间站列表
const spaceListResp = await this.vika.spaces.list()
if (spaceListResp.success) {
// console.log(spaceListResp.data.spaces)
return spaceListResp.data.spaces
} else {
console.error(spaceListResp)
return spaceListResp
}
}
async getSpaceId () {
const spaceList: any = await this.getAllSpaces()
for (const i in spaceList) {
if (spaceList[i].name === this.spaceName) {
this.spaceId = spaceList[i].id
break
}
}
if (this.spaceId) {
return this.spaceId
} else {
return ''
}
}
async getNodesList () {
// 获取指定空间站的一级文件目录
const nodeListResp = await this.vika.nodes.list({ spaceId: this.spaceId })
const tables: any = {}
if (nodeListResp.success) {
// console.log(nodeListResp.data.nodes);
const nodes = nodeListResp.data.nodes
nodes.forEach((node: any) => {
// 当节点是文件夹时,可以执行下列代码获取文件夹下的文件信息
if (node.type === 'Datasheet') {
tables[node.name] = node.id
}
})
} else {
console.error(nodeListResp)
}
return tables
}
async getSheetFields (datasheetId: string) {
const datasheet = await this.vika.datasheet(datasheetId)
const fieldsResp = await datasheet.fields.list()
let fields: any = []
if (fieldsResp.success) {
console.log(JSON.stringify(fieldsResp.data.fields))
fields = fieldsResp.data.fields
} else {
console.error(fieldsResp)
}
return fields
}
async createDataSheet (key: string, name: string, fields: { name: string; type: string }[]) {
const datasheetRo = {
fields,
name,
}
try {
const res: any = await this.vika.space(this.spaceId).datasheets.create(datasheetRo)
console.log(`系统表【${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)
return res.data
} catch (error) {
console.error(name, error)
return error
// TODO: handle error
}
}
async createRecord (datasheetId: string, records: ICreateRecordsReqParams) {
log.info('createRecord:', records)
const datasheet = await this.vika.datasheet(datasheetId)
try {
const res = await datasheet.records.create(records)
if (res.success) {
// console.log(res.data.records)
} else {
console.error('记录写入维格表失败:', res)
}
} catch (err) {
console.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))
const talker = msg.talker()
// console.debug(talker)
// const to = msg.to()
// const type = msg.type()
text = text || msg.text()
const room = msg.room()
let topic = ''
if (room) {
topic = await room.topic()
}
const curTime = this.getCurTime()
// const reqId = v4()
// const ID = msg.id
// 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,
},
}
// log.info('addChatRecord:', JSON.stringify(record))
this.msgStore.push(record)
log.info('最新消息池长度:', this.msgStore.length)
}
addRecord (record:any) {
log.info('addRecord:', JSON.stringify(record))
if (record.fields) {
this.msgStore.push(record)
log.info('最新消息池长度:', this.msgStore.length)
}
}
async addScanRecord (uploadedAttachments: string, text: string) {
const curTime = this.getCurTime()
const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss')
const files = []
if (uploadedAttachments) {
files.push(uploadedAttachments)
}
const records = [
{
fields: {
timeHms,
name: 'system',
topic: '--',
messagePayload: text,
wxid: 'system',
roomid: '--',
messageType: 'qrcode',
file: files,
},
},
]
log.info('addScanRecord:', records)
const datasheet = this.vika.datasheet(this.messageSheet)
datasheet.records.create(records).then((response) => {
if (response.success) {
console.log('写入vika成功', response.code)
} else {
console.error('调用vika写入接口成功写入vika失败', response)
}
return response
}).catch(err => { console.error('调用vika写入接口失败', err) })
}
async addHeartbeatRecord (text: string) {
const curTime = this.getCurTime()
const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss')
const files: any = []
const record = {
fields: {
timeHms,
name: 'system',
topic: '--',
messagePayload: text,
wxid: 'system',
roomid: '--',
messageType: 'heartbeat',
file: files,
},
}
log.info('addHeartbeatRecord:', JSON.stringify(record))
this.msgStore.push(record)
}
async upload (file: ReadStream) {
const datasheet = this.vika.datasheet(this.messageSheet)
try {
const resp = await datasheet.upload(file)
if (resp.success) {
const uploadedAttachments = resp.data
console.debug('上传成功', uploadedAttachments)
return uploadedAttachments
}
} catch (error: any) {
console.error(error.message)
return error
}
}
async deleteRecords (datasheetId: string, recordsIds: string | any[]) {
// console.debug('操作数据表ID', datasheetId)
// console.debug('待删除记录IDs', recordsIds)
const datasheet = this.vika.datasheet(datasheetId)
const response = await datasheet.records.delete(recordsIds)
if (response.success) {
console.log(`删除${recordsIds.length}条记录`)
} else {
console.error('删除记录失败:', response)
}
}
async getRecords (datasheetId: string, query = {}) {
let records: any = []
const datasheet = await this.vika.datasheet(datasheetId)
// 分页获取记录,默认返回第一页
const response = await datasheet.records.query(query)
if (response.success) {
records = response.data.records
// console.log(records)
} else {
console.error(response)
records = response
}
return records
}
async getAllRecords (datasheetId: string) {
let records = []
const datasheet = await this.vika.datasheet(datasheetId)
const response: any = await datasheet.records.queryAll()
// console.debug('原始返回:',response)
if (response.next) {
for await (const eachPageRecords of response) {
// console.debug('eachPageRecords:',eachPageRecords.length)
records.push(...eachPageRecords)
}
// console.debug('records:',records.length)
} else {
console.error(response)
records = response
}
return records
}
async clearBlankLines (datasheetId: any) {
const records = await this.getRecords(datasheetId, {})
// console.debug(records)
const recordsIds = []
for (const i in records) {
recordsIds.push(records[i].recordId)
}
// console.debug(recordsIds)
await this.deleteRecords(datasheetId, recordsIds)
}
async getConfig () {
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,
// }
const sysConfig: any = {}
sysConfig.roomWhiteList = []
sysConfig.contactWhiteList = []
for (let i = 0; i < configRecords.length; i++) {
if (configRecords[i].fields['标识']) {
sysConfig[configRecords[i].fields['标识']] = configRecords[i].fields['值(只修改此列)'] || ''
}
}
for (let i = 0; i < switchRecords.length; i++) {
if (switchRecords[i].fields['标识']) {
sysConfig[switchRecords[i].fields['标识']] = switchRecords[i].fields['启用状态(只修改此列)'] === '开启'
}
}
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'])
}
}
const contactWhiteListRecords = await this.getRecords(this.contactWhiteListSheet, {})
for (let i = 0; i < contactWhiteListRecords.length; i++) {
if (contactWhiteListRecords[i].fields['好友ID']) {
sysConfig.contactWhiteList.push(contactWhiteListRecords[i].fields['好友ID'])
}
}
sysConfig.welcomeList = []
log.info('sysConfig:', JSON.stringify(sysConfig))
return sysConfig
}
async onMessage (message:Message) {
try {
let uploadedAttachments = ''
const msgType = types.Message[message.type()]
let file:any = ''
let filePath = ''
let text = ''
let urlLink
let miniProgram
switch (message.type()) {
// 文本消息
case types.Message.Text:
text = message.text()
break
// 图片消息
case 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 types.Message.Url:
urlLink = await message.toUrlLink()
text = JSON.stringify(JSON.parse(JSON.stringify(urlLink)).payload)
// file = await message.toFileBox();
break
// 小程序卡片消息
case 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 types.Message.Audio:
try {
file = await message.toFileBox()
} catch (e) {
console.error('Audio解析失败', e)
file = ''
}
break
// 视频消息
case types.Message.Video:
try {
file = await message.toFileBox()
} catch (e) {
console.error('Video解析失败', e)
file = ''
}
break
// 动图表情消息
case types.Message.Emoticon:
try {
file = await message.toFileBox()
} catch (e) {
console.error('Emoticon解析失败', e)
file = ''
}
break
// 文件消息
case types.Message.Attachment:
try {
file = await message.toFileBox()
} catch (e) {
console.error('Attachment解析失败', e)
file = ''
}
break
// 文件消息
case types.Message.Location:
// const location = await message.toLocation()
// text = JSON.stringify(JSON.parse(JSON.stringify(location)).payload)
break
case types.Message.Unknown:
// 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 this.upload(readerStream)
fs.unlink(filePath, (err) => {
console.debug('上传vika完成删除文件', filePath, err)
})
} catch {
console.debug('上传失败:', filePath)
fs.unlink(filePath, (err) => {
console.debug('上传vika失败删除文件', filePath, err)
})
}
}
if (message.type() !== types.Message.Unknown) {
await this.addChatRecord(message, uploadedAttachments, msgType, text)
}
} catch (e) {
console.log('vika 写入失败:', e)
}
}
async onScan (qrcode:string, status:ScanStatus) {
if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) {
const qrcodeUrl = encodeURIComponent(qrcode)
const qrcodeImageUrl = [
'https://wechaty.js.org/qrcode/',
qrcodeUrl,
].join('')
// log.info('StarterBot', 'vika onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl)
let uploadedAttachments = ''
let file: FileBox
let filePath = 'qrcode.png'
try {
file = FileBox.fromQRCode(qrcode)
filePath = './' + file.name
try {
const writeStream = fs.createWriteStream(filePath)
await file.pipe(writeStream)
await wait(200)
const readerStream = fs.createReadStream(filePath)
uploadedAttachments = await this.upload(readerStream)
const text = qrcodeImageUrl
await this.addScanRecord(uploadedAttachments, text)
fs.unlink(filePath, (err) => {
log.info('二维码上传vika完成删除文件', filePath, err)
})
} catch {
log.info('二维码上传失败:', filePath)
fs.unlink(filePath, (err) => {
log.info('二维码上传vika失败删除文件', filePath, err)
})
}
} catch (e) {
log.info('二维码vika 写入失败:', e)
}
} else {
log.info('StarterBot', 'vika onScan: %s(%s)', ScanStatus[status], status)
}
}
async updateContacts (bot:Wechaty) {
let updateCount = 0
try {
const contacts: Contact[] = await bot.Contact.findAll()
log.info('当前微信最新联系人数量:', contacts.length)
const recordsAll: any = []
const recordExisting = await this.getAllRecords(this.contactSheet)
log.info('云端好友数量:', recordExisting.length)
const wxids: string[] = []
if (recordExisting.length) {
recordExisting.forEach((record: { fields: any, id: any }) => {
wxids.push(record.fields.id)
})
}
for (let i = 0; i < contacts.length; i++) {
const item = contacts[i]
if (item && item.friend() && !wxids.includes(item.id)) {
let avatar = ''
try {
avatar = String(await item.avatar())
} catch (err) {
}
const fields = {
alias: String(await item.alias() || ''),
avatar,
friend: item.friend(),
gender: String(item.gender() || ''),
id: item.id,
name: item.name(),
phone: String(await item.phone()),
type: String(item.type()),
}
const record = {
fields,
}
recordsAll.push(record)
}
}
for (let i = 0; i < recordsAll.length; i = i + 10) {
const records = recordsAll.slice(i, i + 10)
await this.createRecord(this.contactSheet, records)
log.info('好友列表同步中...', i + 10)
updateCount = updateCount + 10
void await wait(250)
}
log.info('同步好友列表完成,更新好友数量:', updateCount)
} catch (err) {
log.error('更新好友列表失败:', err)
}
}
async updateRooms (bot: Wechaty) {
let updateCount = 0
try {
const rooms: Room[] = await bot.Room.findAll()
log.info('当前最新微信群数量:', rooms.length)
const recordsAll: any = []
const recordExisting = await this.getAllRecords(this.roomListSheet)
log.info('云端群数量:', recordExisting.length)
const wxids: string[] = []
if (recordExisting.length) {
recordExisting.forEach((record: { fields: any, id: any }) => {
wxids.push(record.fields.id)
})
}
for (let i = 0; i < rooms.length; i++) {
const item = rooms[i]
if (item && !wxids.includes(item.id)) {
let avatar:any = 'null'
try {
avatar = String(await item.avatar())
} catch (err) {
log.error('获取群头像失败:', err)
}
const fields = {
avatar,
id: item.id,
ownerId: String(item.owner()?.id || ''),
topic: await item.topic() || '',
}
const record = {
fields,
}
recordsAll.push(record)
}
}
for (let i = 0; i < recordsAll.length; i = i + 10) {
const records = recordsAll.slice(i, i + 10)
await this.createRecord(this.roomListSheet, records)
log.info('群列表同步中...', i + 10)
updateCount = updateCount + 10
void await wait(250)
}
log.info('同步群列表完成,更新群数量:', updateCount)
} catch (err) {
log.error('更新群列表失败:', err)
}
}
async getTimedTask () {
const taskRecords = await this.getRecords(this.noticeSheet, {})
// console.debug(taskRecords)
const timedTasks: any = []
const taskFields: Field[] = sheets['noticeSheet']?.fields || []
const taskFieldDic: any = {}
for (let i = 0; i < taskFields.length; i++) {
const taskField: Field | undefined = taskFields[i]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (taskFields && taskField !== undefined && taskFields[i]?.desc) {
taskFieldDic[taskField.name] = taskField.desc
}
}
for (let i = 0; i < taskRecords.length; i++) {
const task = taskRecords[i]
const taskConfig: any = {
id: task.recordId,
msg: task.fields['内容'],
time: task.fields['时间'],
cycle: task.fields['周期'],
contacts: [],
rooms: [],
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)
}
}
// console.debug(2, timedTasks)
return timedTasks
}
async checkInit (msg: string) {
this.spaceId = await this.getSpaceId()
// console.log('空间ID:', this.spaceId)
let sheetCount = 0
if (this.spaceId) {
const tables = await this.getNodesList()
// console.debug(tables)
for (const k in sheets) {
const sheet = sheets[k as keyof Sheets]
// console.log(k, sheet)
if (sheet) {
if (!tables[sheet.name]) {
sheetCount = sheetCount + 1
console.error(`缺少【${sheet.name}】表,请运行 npm run sys-init 自动创建系统表,然后再运行 npm start`)
} else {
this[k as keyof VikaBot] = tables[sheet.name]
}
}
}
if (sheetCount === 0) {
console.log(`================================================\n\n${msg}\n\n================================================\n`)
} else {
return false
}
} else {
console.error('指定空间不存在请先创建空间并在config.ts中配置VIKA_SPACENAME')
return false
}
const that = this
setInterval(() => {
// log.info('待处理消息池长度:', that.msgStore.length||0);
// that.msgStore = that.msgStore.concat(global.sentMessage)
// global.sentMessage = []
if (that.msgStore.length && that.messageSheet) {
const end = that.msgStore.length < 10 ? that.msgStore.length : 10
const records = that.msgStore.splice(0, end)
const messageSheet = that.messageSheet
const datasheet = that.vika.datasheet(messageSheet)
// log.info('写入vika的消息', JSON.stringify(records))
try {
datasheet.records.create(records).then((response) => {
if (response.success) {
console.log('写入vika成功', end, JSON.stringify(response.code))
} else {
console.error('调用vika写入接口成功写入vika失败', response)
}
}).catch(err => { console.error('调用vika写入接口失败', err) })
} catch (err) {
console.error('调用datasheet.records.create失败', err)
}
}
}, 250)
return true
}
async init () {
this.spaceId = await this.getSpaceId()
// console.log('空间ID:', this.spaceId)
if (this.spaceId) {
const tables = await this.getNodesList()
// console.log(tables)
await wait(1000)
for (const k in sheets) {
// console.debug(this)
const sheet = sheets[k as keyof Sheets]
// console.log(k, sheet)
if (sheet && !tables[sheet.name]) {
const fields = sheet.fields
const newFields: Field[] = []
for (let j = 0; j < fields.length; j++) {
const field = fields[j]
const newField: Field = {
type: field?.type || '',
name: field?.name || '',
desc: field?.desc || '',
}
console.debug(field)
let options
switch (field?.type) {
case 'SingleText':
newField.property = field.property || {}
newFields.push(newField)
break
case 'SingleSelect':
options = field.property.options
newField.property = {}
newField.property.defaultValue = field.property.defaultValue || options[0].name
newField.property.options = []
for (let z = 0; z < options.length; z++) {
const option = {
name: options[z].name,
color: options[z].color.value,
}
newField.property.options.push(option)
}
newFields.push(newField)
break
case 'Text':
newFields.push(newField)
break
case 'DateTime':
newField.property = {}
newField.property.dateFormat = 'YYYY-MM-DD'
newField.property.includeTime = true
newField.property.timeFormat = 'HH:mm'
newField.property.autoFill = true
newFields.push(newField)
break
case 'Checkbox':
newField.property = {
icon: 'white_check_mark',
}
newFields.push(newField)
break
case 'MagicLink':
newField.property = {}
newField.property.foreignDatasheetId = this[field.desc as keyof VikaBot]
if (field.desc) {
newFields.push(newField)
}
break
case 'Attachment':
newFields.push(newField)
break
default:
newFields.push(newField)
break
}
// if (field?.type !== 'MagicLink' || (field.type === 'MagicLink' && field.desc)) {
// newFields.push(newField)
// }
}
// console.debug(newFields)
await this.createDataSheet(k, sheet.name, newFields)
await wait(200)
const defaultRecords = sheet.defaultRecords
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (defaultRecords) {
// console.debug(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)
await this.createRecord(this[k as keyof VikaBot], records)
await wait(200)
}
console.log(sheet.name + '初始化数据写入完成...')
}
console.log(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')
// const tasks = await this.getTimedTask()
return true
} else {
console.error('指定空间不存在请先创建空间并在config.ts中配置VIKA_SPACENAME')
return false
}
}
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
}
}
export { VikaBot }
export default VikaBot

View File

@ -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"]
}

View File

@ -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

View File

@ -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

View File

@ -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": {}
}
}

View File

@ -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.

View File

@ -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服务端
<img src="https://user-images.githubusercontent.com/104893934/169646853-b635e1ad-92fd-4fd4-b62a-c165e5ba4796.png" width="60%">
## Usage
```
npm install
npm run dev
```
启动后使用谷歌浏览器访问http://localhost:8080/#/imServer
## Express-server
./build/webpack.dev.conf.js 内置了一个Express服务后台接口都在此处

View File

@ -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'
))
})
})

View File

@ -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)
}
}

View File

@ -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')
})
}
}

View File

@ -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'
}
}

View File

@ -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'
}
}

View File

@ -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
// <input type="file" name="uploadFile" />
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);

View File

@ -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

View File

@ -1,7 +0,0 @@
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

View File

@ -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
}
}

View File

@ -1,4 +0,0 @@
'use strict'
module.exports = {
NODE_ENV: '"production"'
}

View File

@ -1,7 +0,0 @@
'use strict'
const merge = require('webpack-merge')
const devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>wechat客服系统</title>
<link rel="stylesheet" href="/static/css/reset.css">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -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"
]
}

View File

@ -1,22 +0,0 @@
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
};
</script>
<style>
#app {
width: 100%;
height: 100%;
position: absolute;
overflow-y: hidden;
-webkit-tap-highlight-color: transparent;
-webkit-font-smoothing: antialiased;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -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 '载入中'提示框titlee.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 '载入中'提示框titlee.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;

View File

@ -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';
}

View File

@ -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;

View File

@ -1,952 +0,0 @@
<!-- 聊天记录 -->
<template>
<div class="common_chat-wrapper">
<div class="common_chat-inner">
<!-- 聊天记录 -->
<div v-if="chatLoaded" class="common_chat-main" id="common_chat_main" ref="common_chat_main">
<div class="common_chat-main-content">
<div class="inner">
<div v-for="(item ,index) in chatInfoEn.msgList" :key="index">
<!-- 系统消息 -->
<div v-if="item.role=='sys'" class="item sys">
<!-- 1)文本类型 -->
<div v-if="item.contentType=='text'" class="text-content">
<p>{{item.content}}</p>
</div>
</div>
<!-- 客户客服 -->
<div v-else class="item" :class="{ sender: item.role == oprRoleName, receiver: item.role != oprRoleName }">
<div class="info-wrapper" :class="item.state">
<!-- 头像 -->
<div class="avatar-wrapper">
<img class="kf-img" :src="item.avatarUrl" />
</div>
<!-- 1)文本类型 -->
<div v-if="item.contentType=='text'" class="item-content common_chat_emoji-wrapper-global">
<p class="text" v-html="getqqemojiEmoji(item.content)"></p>
</div>
<!-- 2)图片类型 -->
<div v-else-if="item.contentType=='image'" class="item-content">
<img class="img" :src="item.fileUrl" @click="imgViewDialog_show(item)" />
</div>
<!-- 3)文件类型 -->
<div v-else-if="item.contentType=='file'" class="item-content">
<div class="file">
<i class="file-icon iconfont fa fa-file"></i>
<div class="file-info">
<p class="file-name">{{getFileName(item.fileName)}}</p>
<div class="file-opr">
<div v-show="item.state=='success'">
<a class="file-download" :href="item.fileUrl" target="_blank" :download="item.fileUrl">下载</a>
</div>
</div>
</div>
</div>
</div>
<!-- 4)文本类型 -->
<div v-if="item.contentType=='transformServer'" class="item-content common_chat_emoji-wrapper-global">
<p class="text">
当前没有配置机器人
<el-button type="text" @click="chatCallback('transformServer')">转接客服</el-button>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部区域 -->
<div class="common_chat-footer">
<div>
<!-- 表情文件选择等操作 -->
<div class="opr-wrapper">
<common-chat-emoji class="item" ref="qqemoji" @select="qqemoji_selectFace"></common-chat-emoji>
<a class="item" href="javascript:void(0)" @click="fileUpload_click('file')">
<i class="iconfont fa fa-file-o"></i>
</a>
<form method="post" enctype="multipart/form-data">
<input type="file" name="uploadFile" id="common_chat_opr_fileUpload" style="display:none;position:absolute;left:0;top:0;width:0%;height:0%;opacity:0;" />
</form>
</div>
<!-- 聊天输入框 -->
<div class="input-wrapper">
<div
maxlength="500"
class="inputContent common_chat_emoji-wrapper-global"
id="common_chat_input"
contenteditable="true"
@paste.stop="inputContent_paste"
@drop="inputContent_drop"
@keydown="inputContent_keydown"
@mouseup="inputContent_mouseup"
@mouseleave="inputContent_mouseup"
></div>
</div>
<!-- 发送按钮 -->
<el-button type="primary" size="small" class="send-btn" :class="chatInfoEn.state" @click="sendText()" :disabled="chatInfoEn.inputContent.length==0">发送</el-button>
</div>
<!-- 离线 -->
<div v-show="chatInfoEn.state=='off' || chatInfoEn.state=='end'" class="off-wrapper">
<span class="content">会话已经结束</span>
</div>
</div>
</div>
<!-- 图片查看dialog -->
<el-dialog title :visible.sync="imgViewDialogVisible" class="imgView-dialog" :modal="false">
<div class="header">
<i class="iconfont fa fa-remove" @click="imgViewDialog_close"></i>
</div>
<div class="main">
<img class="img" :src="imgViewDialog_imgSrc" />
</div>
</el-dialog>
</div>
</template>
<script>
import common_chat_emoji from './common_chat_emoji.vue';
export default {
components: {
commonChatEmoji: common_chat_emoji,
},
props: {
chatInfoEn: {
required: true,
type: Object,
default: {
inputContent: '', //
msgList: [], //
},
},
oprRoleName: {
required: true,
type: String,
default: '',
}, // e.g. server:client:
},
data() {
return {
inputContent_setTimeout: null, //
selectionRange: null, //
shortcutMsgList: [], //
imgViewDialogVisible: false, // dialog
imgViewDialog_imgSrc: '', // dialog
chatLoaded: false, // chat
};
},
computed: {},
watch: {},
mounted() {
this.$nextTick(function () {
this.$data.chatLoaded = true;
this.init();
});
},
methods: {
/**
* 初始化
* @param {Object} opts 可选对象
*/
init: function (opts) {
var self = this;
//
document.getElementById('common_chat_input').innerHTML = '';
self.$refs.qqemoji.$data.faceHidden = true;
// 线
if (this.chatInfoEn.state == 'on') {
// 1.
setTimeout(function () {
//
document.getElementById('common_chat_input').focus();
self.setInputContentSelectRange();
//
if (self.chatInfoEn.inputContent) {
self.setInputDiv(self.chatInfoEn.inputContent);
}
}, 200);
} else {
document.getElementById('common_chat_input').blur();
}
// 2.
this.$nextTick(function () {
self.$refs.common_chat_main.scrollTop = self.$refs.common_chat_main.scrollHeight;
document.getElementById('common_chat_input').focus();
});
},
/**
* 发送文本
*/
sendText: function () {
var self = this;
if (self.chatInfoEn.inputContent.length == '') {
return;
}
var msgContent = self.chatInfoEn.inputContent;
document.getElementById('common_chat_input').innerHTML = '';
self.setInputContentByDiv();
this.sendMsg({
contentType: 'text',
content: msgContent,
});
},
/**
* 设置输入内容
* 根据input输入框innerHTML转换为纯文本
*/
setInputContentByDiv: function () {
var self = this;
var htmlStr = document.getElementById('common_chat_input').innerHTML;
// 1.<img textanme="[]"/> => []
var tmpInputContent = htmlStr.replace(/<img .+?text=\"(.+?)\".+?>/g, '[$1]').replace(/<.+?>/g, '');
// 2.
if (tmpInputContent.length > 500) {
document.getElementById('common_chat_input').innerHTML = '';
var value = tmpInputContent.substr(0, 499).replace(/\[(.+?)\]/g, function (item, value) {
return self.$refs.qqemoji.getImgByFaceName(value);
});
this.setInputDiv(value);
}
// 3.store
this.chatInfoEn.inputContent = tmpInputContent;
},
/**
* 设置input输入框内容
* @param {String} vlaue 设置的值
*/
setInputDiv: function (value) {
if (this.$data.selectionRange == null) {
document.getElementById('common_chat_input').focus();
return;
}
// 1.selectionRange
if (window.getSelection) {
window.getSelection().removeAllRanges();
window.getSelection().addRange(this.$data.selectionRange);
} else {
this.$data.selectionRange && this.$data.selectionRange.select();
}
// 2.img
value = this.getqqemojiEmoji(value);
// 3.
if (window.getSelection) {
var sel, range;
// IE9 and non-IE
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
// 1)()
range = sel.getRangeAt(0); //
range.deleteContents(); //
// 2)DocumentFragment
var elemnet;
if (range.createContextualFragment) {
elemnet = range.createContextualFragment(value);
} else {
// createContextualFragment
// DocumentFragment
elemnet = document.createDocumentFragment();
var divEl = document.createElement('div');
divEl.innerHTML = value;
// divElDocumentFragment
for (let i = 0, len = divEl.children.length; i < len; i++) {
elemnet.appendChild(divEl.firstChild);
}
}
// 3)
var lastNode = elemnet.lastChild;
range.insertNode(elemnet);
range.setStartAfter(lastNode);
sel.removeAllRanges();
sel.addRange(range);
}
} else if (document.selection && document.selection.type != 'Control') {
// IE < 9
document.selection.createRange().pasteHTML(imgStr);
}
// 4.inputContent
this.setInputContentByDiv();
},
/**
* 转换为QQ表情
*/
getqqemojiEmoji: function (value) {
if (value == undefined) {
return;
}
var self = this;
let rs = value.replace(/\[(.+?)\]/g, function (item, value) {
return self.$refs.qqemoji.getImgByFaceName(value);
});
return rs;
},
/**
* 设置input输入框的选择焦点
*/
setInputContentSelectRange: function () {
if (window.getSelection && window.getSelection().rangeCount > 0) {
var selectRange = window.getSelection().getRangeAt(0);
if (
selectRange.commonAncestorContainer.nodeName == '#text' &&
selectRange.commonAncestorContainer.parentElement &&
selectRange.commonAncestorContainer.parentElement.id == 'common_chat_input'
) {
//
this.$data.selectionRange = selectRange;
} else if (selectRange.commonAncestorContainer.id == 'common_chat_input') {
//
this.$data.selectionRange = selectRange;
}
}
},
/**
* 输入框的mouseup
*/
inputContent_mouseup: function (e) {
this.setInputContentSelectRange();
},
/**
* 输入框的keydown
*/
inputContent_keydown: function (e) {
// keyup访
clearTimeout(this.$data.inputContent_setTimeout);
this.$data.inputContent_setTimeout = setTimeout(() => {
this.setInputContentByDiv();
//
if (e.keyCode == 13) {
this.sendText();
}
this.setInputContentSelectRange();
}, 1);
},
/**
* 输入框的粘贴
*/
inputContent_paste: function (e) {
var self = this;
var isImage = false;
if (e.clipboardData && e.clipboardData.items.length > 0) {
// 1.
for (var i = 0; i < e.clipboardData.items.length; i++) {
var item = e.clipboardData.items[i];
if (item.kind == 'file' && item.type.indexOf('image') >= 0) {
//
var file = item.getAsFile();
let formData = new FormData();
formData.append('uploadFile', file);
this.$http.uploadFile({
url: '/upload',
params: formData,
successCallback: (rs) => {
document.getElementById('common_chat_opr_fileUpload').value = '';
this.sendMsg({
contentType: 'image',
fileName: rs.fileName,
fileUrl: rs.fileUrl,
state: 'success',
});
},
});
isImage = true;
}
}
// 2.
if (!isImage) {
var str = e.clipboardData.getData('text/plain');
//
var span = document.createElement('span');
span.innerHTML = str;
var txt = span.innerText;
this.setInputDiv(txt.replace(/\n/g, '').replace(/\r/g, '').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
}
}
e.stopPropagation();
e.preventDefault();
},
/**
* 文件上传_点击
*/
fileUpload_click: function (fileType) {
document.getElementById('common_chat_opr_fileUpload').onchange = this.fileUpload_change;
document.getElementById('common_chat_opr_fileUpload').click();
},
/**
* 文件上传_选中文件
*/
fileUpload_change: function (e) {
var fileNameIndex = document.getElementById('common_chat_opr_fileUpload').value.lastIndexOf('\\') + 1;
var fileName = document.getElementById('common_chat_opr_fileUpload').value.substr(fileNameIndex);
var extend = fileName.substring(fileName.lastIndexOf('.') + 1);
// 1.
// 1)
if (document.getElementById('common_chat_opr_fileUpload').files[0].size >= 1000 * 1000 * 10) {
this.$ak.Msg.toast('文件大小不能超过10M', 'error');
document.getElementById('common_chat_opr_fileUpload').value = '';
return false;
}
// 2.
let formData = new FormData();
formData.append('uploadFile', document.getElementById('common_chat_opr_fileUpload').files[0]);
this.$http.uploadFile({
url: '/upload',
params: formData,
successCallback: (rs) => {
document.getElementById('common_chat_opr_fileUpload').value = '';
this.sendMsg({
contentType: ['png', 'jpg', 'jpeg', 'gif', 'bmp'].indexOf(extend) >= 0 ? 'image' : 'file',
fileName: fileName,
fileUrl: rs.fileUrl,
state: 'success',
});
},
});
},
/**
* qqemoji选中表情
*/
qqemoji_selectFace: function (rs) {
var imgStr = rs.imgStr;
this.setInputDiv(imgStr);
},
/**
* 转换文件名若文件名称超过9个字符将进行截取处理
* @param {String} fileName 文件名称
*/
getFileName: function (fileName) {
if (!fileName) {
return;
}
var name = fileName.substring(0, fileName.lastIndexOf('.'));
var extend = fileName.substring(fileName.lastIndexOf('.') + 1);
if (name.length > 9) {
name = name.substring(0, 3) + '...' + name.substring(name.length - 3);
}
return name + '.' + extend;
},
/**
* 图片查看dialog_显示
*/
imgViewDialog_show: function (item) {
this.$data.imgViewDialogVisible = true;
this.$data.imgViewDialog_imgSrc = item.fileUrl;
},
/**
* 图片查看dialog_显示
*/
imgViewDialog_close: function () {
this.$data.imgViewDialogVisible = false;
var self = this;
setTimeout(function () {
self.$data.imgViewDialog_imgSrc = '';
}, 100);
},
/**
* 输入框的拖拽
*/
inputContent_drop: function (e) {
var self = this;
setTimeout(function () {
self.setInputContentByDiv();
}, 100);
},
/**
* 发送消息e.g. 文本图片文件
* @param {Object} msg 消息对象
*/
sendMsg: function (msg) {
var self = this;
// 1.
this.$emit('sendMsg', {
msg: msg,
successCallbcak: function () {
document.getElementById('common_chat_input').focus();
self.goEnd();
},
});
},
/**
* 传递回调
*/
chatCallback: function (emitType, data) {
this.$emit('chatCallback', {
eventType: emitType,
data: data,
});
},
/**
* 聊天记录滚动到底部
*/
goEnd: function () {
this.$nextTick(() => {
setTimeout(() => {
this.$refs.common_chat_main.scrollTop = this.$refs.common_chat_main.scrollHeight;
}, 100);
});
},
},
};
</script>
<style lang="less">
.common_chat-wrapper {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
font-size: 12px;
float: left;
border: 0px;
.common_chat-inner {
width: 100%;
height: 100%;
.common_chat-main {
position: relative;
height: calc(~'100% - 190px');
overflow-y: auto;
overflow-x: hidden;
.common_chat-main-header {
padding-top: 14px;
text-align: center;
.el-button {
padding: 0px;
font-size: 12px;
color: #8d8d8d;
}
}
.common_chat-main-content {
position: absolute;
width: 100%;
height: 100%;
& > .inner {
padding-bottom: 20px;
.item {
clear: both;
overflow: hidden;
}
.sys {
color: #b0b0b0;
font-size: 12px;
text-align: center;
.text-content {
padding-top: 20px;
}
.myd-content {
.desc {
margin-top: 18px;
}
.text {
color: #3e3e3e;
margin-top: 12px;
}
.remark {
margin-top: 10px;
}
}
}
.receiver,
.sender {
margin-top: 18px;
font-size: 12px;
.avatar-wrapper {
float: left;
.kf-img {
width: 40px;
height: 40px;
}
}
.info-wrapper {
position: relative;
text-align: left;
font-size: 12px;
.item-content {
position: relative;
max-width: 330px;
color: #3e3e3e;
font-size: 13px;
border-radius: 3px;
.text {
line-height: 1.8;
white-space: normal;
word-wrap: break-word;
word-break: break-all;
padding: 10px 12px;
}
.qqemoji {
width: 24px;
height: 24px;
}
.img {
max-width: 320px;
max-height: 240px;
white-space: normal;
word-wrap: break-word;
word-break: break-all;
padding: 5px;
cursor: pointer;
}
.file {
width: 220px;
padding: 10px 8px;
margin: 3px;
overflow: hidden;
background: #fff;
border-radius: 5px;
.el-button {
padding: 0px;
font-size: 12px;
}
.file-info {
float: left;
padding: 0px 8px;
.file-name {
width: 160px;
display: inline-block;
color: #333333;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
line-height: 1.3;
}
}
.file-opr {
margin-top: 8px;
}
.file-icon {
float: left;
color: #663399;
font-size: 40px;
}
.file-download {
color: #00a8d7;
cursor: pointer;
text-decoration: blink;
}
}
.preInput {
position: relative;
color: #8d8d8d;
img {
height: 15px;
position: relative;
top: 3px;
}
}
.issueList {
width: 250px;
padding: 10px;
.title {
position: relative;
.content {
position: absolute;
margin-top: -1px;
margin-left: 6px;
}
}
.el-collapse-item__wrap {
background: transparent;
}
.el-collapse {
border: 0px;
margin-top: 8px;
margin-bottom: -8px;
.el-collapse-item__header {
font-size: 13px;
background: transparent;
color: #f7455d;
padding-left: 5px;
}
.el-collapse-item__wrap {
.el-collapse-item__content {
font-size: 12px;
color: #3e3e3e;
padding-left: 5px;
}
}
}
}
.issueExtend {
width: 250px;
padding: 10px 10px 0px;
.main {
border-top: 1px solid #eeeff0;
margin-top: 10px;
padding-top: 10px;
p {
margin-bottom: 5px;
}
.el-button {
font-size: 12px;
color: #f7455d;
}
}
}
.issueResult {
width: 250px;
.main {
padding: 10px;
}
.footer {
border-top: 1px solid #eeeff0;
height: 30px;
.btn {
width: 60px;
margin: 0px 30px;
padding: 6px 0px;
display: inline-block;
text-align: center;
font-size: 10px;
color: #8d8d8d;
cursor: pointer;
position: relative;
&:first-child::after {
top: 4px;
right: -30px;
width: 1px;
height: 80%;
content: '';
position: absolute;
background-color: #eeeff0;
z-index: 0;
}
}
.iconfont {
font-size: 10px;
margin-right: 5px;
}
}
}
}
}
}
.item.receiver {
margin-left: 5px;
.avatar-wrapper {
margin-right: 15px;
}
.info-wrapper {
.item-content {
float: left;
color: #000000;
background-color: #f9fbfc;
border: 1px solid #ccc;
&::before {
position: absolute;
top: -1px;
left: -10px;
width: 0px;
height: 0px;
content: '';
border-top: 0px;
border-right: 10px solid #ccc;
border-bottom: 5px solid transparent;
border-left: 0px;
}
}
}
}
.item.sender {
margin-right: 5px;
.avatar-wrapper {
float: right;
margin-left: 15px;
}
.info-wrapper {
float: right;
.item-content {
float: right;
background: #0095ff;
border: 1px solid #0095ff;
color: #ffffff;
&::before {
position: absolute;
top: -1px;
right: -10px;
width: 0px;
height: 0px;
content: '';
border-top: 0px;
border-right: 0px;
border-bottom: 5px solid transparent;
border-left: 10px solid #0095ff;
}
}
}
}
}
}
}
.common_chat-footer {
position: relative;
width: 100%;
border-top: 1px solid #ccc;
.opr-wrapper {
height: 20px;
padding: 10px;
text-align: left;
& > .item {
margin-right: 12px;
float: left;
font-weight: normal;
text-decoration: blink;
& > .iconfont {
color: #aaa;
font-size: 20px;
}
}
}
.input-wrapper {
position: relative;
padding: 2px 0px 0px 10px;
.inputContent {
width: 99%;
padding: 2px;
height: 85px;
resize: none;
overflow: auto;
line-height: 1.5;
outline: 0px solid transparent;
}
.shortcutPopover-wrapper {
position: absolute;
top: 30px;
left: 10px;
width: 440px;
max-height: 80px;
padding: 4px;
font-size: 12px;
overflow-y: auto;
border: 1px solid #9b9aab;
border-radius: 3px;
background-color: #fff;
cursor: pointer;
p {
padding: 4px;
&.selected {
background-color: #ded1cc;
}
.key {
display: inline-block;
width: 50px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.content {
display: inline-block;
width: 350px;
margin-left: 10px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.highlight {
color: #00a8d7;
}
}
}
.tips {
position: absolute;
top: 7px;
left: 20px;
width: auto;
color: #8d8d8d;
}
}
.send-btn {
float: right;
margin-right: 16px;
&.off,
&.end {
background-color: #ccc;
border-color: #ccc;
}
}
.off-wrapper {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.6);
font-size: 14px;
.content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
}
}
.imgView-dialog {
background: #00000080;
height: 100%;
.el-dialog {
max-width: 75%;
position: relative;
background: transparent;
box-shadow: none;
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0px;
text-align: center;
position: relative;
.header {
text-align: right;
position: relative;
height: 0px;
.fa-remove {
font-size: 32px;
color: white;
position: relative;
right: -50px;
top: -30px;
cursor: pointer;
}
}
.main {
.img {
max-width: 100%;
max-height: 100%;
height: 100%;
}
}
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More