backup: 2025-03-05
This commit is contained in:
parent
71b1ef4c19
commit
b99b645a6e
|
@ -1,8 +0,0 @@
|
|||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
|
@ -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>
|
|
@ -1,6 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
const rules = {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extends: '@chatie',
|
||||
rules,
|
||||
}
|
|
@ -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 }}
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"Aibot"
|
||||
]
|
||||
}
|
|
@ -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" ]
|
|
@ -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.
|
|
@ -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 如果不希望每个群都开启智能问答,需设置群白名单,首先需要将上图中的群白名单开关设置为开启
|
||||
|
||||
然后将群加入到问答白名单,在“群白名单”表中,加入需要开启的群ID(roomid),群ID在消息中查看(在群里发一条消息,然后控制台查看或在维格表中查找)
|
||||
|
||||
详细操作参考 [手把手教程](https://www.yuque.com/atorber/oegota/zm4ulnwnqp9whmd6)
|
||||
|
||||
8.4 重启程序,在指定群测试问答
|
||||
|
||||
## 使用环境变量启动
|
||||
|
||||
> 也可以不使用配置文件,通过配置环境变量启动
|
||||
|
||||
Mac、Linux操作系统下运行(仅支持使用wechaty-puppet-wechat和wechaty-puppet-padlocal)
|
||||
|
||||
```Shell
|
||||
export VIKA_TOKEN="替换成自己的维格表token"
|
||||
export VIKA_SPACENAME="替换成你的维格表空间名称"
|
||||
npm run sys-init
|
||||
npm start
|
||||
```
|
||||
|
||||
Windows操作系统下运行(支持使用wechaty-puppet-xp、wechaty-puppet-wechat、wechaty-puppet-padlocal)
|
||||
|
||||
推荐使用 wechaty-puppet-xp
|
||||
|
||||
```Shell
|
||||
set VIKA_TOKEN="替换成自己的维格表token"
|
||||
set VIKA_SPACENAME="替换成你的维格表空间名称"
|
||||
npm run sys-init
|
||||
npm run start
|
||||
```
|
||||
|
||||
## 在Docker中部署运行
|
||||
|
||||
注意,因为wechaty-puppet-xp必须依赖Windows微信客户端,所以不能使用Docker,但使用wechaty-puppet-padlocal、wechaty-puppet-service则可以用Doker来部署,
|
||||
|
||||
最新代码已经默认wechaty-puppet-wehcat为初始化puppet,mac、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项目,实现客服后台回复咨询消息
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
qabot.vlist.cc
|
|
@ -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 设置群白名单,将群加入到问答白名单,在“群白名单”表中,加入需要开启的群ID(roomid),群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">
|
||||
|
||||
- 添加白名单
|
||||
|
||||

|
||||
|
||||
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%">
|
|
@ -1,21 +0,0 @@
|
|||
大家好,今天给大家演示一下微信智能问答机器人,如果你是一个社群工作者、拼团团长、业务群运营经理,使用这个项目可以帮助你解决一些重复性问答。
|
||||
QA系统由微信开放对话平台提供,由后台系统可以操作,数据存储和配置使用维格表实现。
|
||||
下面为大家演示几个核心功能:
|
||||
一 到货信息看板和自动问答
|
||||
二 通知公告自动问答
|
||||
先看一下效果,群成员在群内发送问题内容,机器会将包含链接的回答内容自动回复发送,点击链接可以查看详情
|
||||
实现以上功能很简单,下面我来实际操作演示一下
|
||||
首先,我们需要在维格表后台发布一条公告,我们以核酸检测通知为例,选择发布日期、填写发布内容和发布者,提交之后,通知内容可以在列表中显示
|
||||
然后,我们再继续添加一个商品,输入商品名称苹果,提交,苹果被添加到商品列表
|
||||
接着,我们继续添加一条到货信息,选择日期,选择商品,商品可以选择多个,设置状态为待发货,提交之后,我们在到货信息列表中可以看到相关信息,可以以不同的视图模式显示
|
||||
以上信息创建完毕后,我们在问答系统中配置相应问题,这样机器人就可以在群内自动回复了
|
||||
复制一下到货信息列表的分享链接地址,在问答系统中创建一个问题 问题的内容是 ”什么时候到货“,把刚刚复制的链接替换到问题的回复内容中
|
||||
选择相似问题,系统推荐,系统会自动推荐一些相似的问题,按需要进行勾选,保存一下
|
||||
继续添加一个社区通知问答,到问题表中复制通知公告的分享链接地址,编辑社区通知问题,将链接替换到问题答案中,保存问题
|
||||
以上,我们已经添加了 ”什么时候到货“和”社区通知“ 两个问题,
|
||||
问题保存后需要发布上线一下,点击左侧的发布管理 上线发布 发布,提示发布成功后,我们在微信群众看一下效果
|
||||
发送消息内容 社区公告,机器人会自动返回回答内容;
|
||||
发送 什么时候到货,机器人自动回复到货信息
|
||||
点击回复中的链接地址,就可以查看相关内容了,在电脑网页中的显示是这样,卡片形式,很清晰明了
|
||||
我们切换到手机模式看一下,卡片视图,也不错
|
||||
只需要以上简单几步,就完成了两个自动通知场景的构建,欢迎大家进一步关注Wechat Openai QA Bot项目,如果觉得可以,给一个star
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -1 +0,0 @@
|
|||
declare module 'qrcode-terminal'
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
export * as types from './types.js'
|
|
@ -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,
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -1,9 +0,0 @@
|
|||
import {
|
||||
wxai,
|
||||
aibot,
|
||||
} from './wx-openai.js'
|
||||
|
||||
export {
|
||||
wxai,
|
||||
aibot,
|
||||
}
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
declare module '*'
|
||||
declare module './excel.js'
|
|
@ -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
|
|
@ -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
|
|
@ -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 }
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
declare module './chat-device.js'
|
||||
declare module './msg-format.js'
|
|
@ -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 }
|
|
@ -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()
|
|
@ -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
|
|
@ -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"]
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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": {}
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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服务,后台接口都在此处
|
|
@ -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'
|
||||
))
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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
|
|
@ -1,7 +0,0 @@
|
|||
'use strict'
|
||||
const merge = require('webpack-merge')
|
||||
const prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
})
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
'use strict'
|
||||
module.exports = {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
'use strict'
|
||||
const merge = require('webpack-merge')
|
||||
const devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
|
@ -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>
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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 |
|
@ -1,795 +0,0 @@
|
|||
/**
|
||||
* 工具模块,不依赖第三方代码
|
||||
*/
|
||||
var ak = ak || {};
|
||||
|
||||
ak.Base_URL = location.host;
|
||||
|
||||
/**
|
||||
* 工具模块,不依赖第三方代码
|
||||
* 包含:类型判断
|
||||
*/
|
||||
ak.Utils = {
|
||||
/**
|
||||
* 是否为JSON字符串
|
||||
* @param {String}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
isJSON(str) {
|
||||
if (typeof str == 'string') {
|
||||
try {
|
||||
var obj = JSON.parse(str);
|
||||
if (str.indexOf('{') > -1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* 去除字符串首尾两端空格
|
||||
* @param {String} str
|
||||
* @return {String}
|
||||
*/
|
||||
trim(str) {
|
||||
if (str) {
|
||||
return str.replace(/(^\s*)|(\s*$)/g, '');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 脱敏
|
||||
* @param {String} value 脱敏的对象
|
||||
* @return {String}
|
||||
*/
|
||||
desensitization: function(value) {
|
||||
if (value) {
|
||||
var valueNew = '';
|
||||
const length = value.length;
|
||||
valueNew = value
|
||||
.split('')
|
||||
.map((number, index) => {
|
||||
// 脱敏:从倒数第五位开始向前四位脱敏
|
||||
const indexMin = length - 8;
|
||||
const indexMax = length - 5;
|
||||
|
||||
if (index >= indexMin && index <= indexMax) {
|
||||
return '*';
|
||||
} else {
|
||||
return number;
|
||||
}
|
||||
})
|
||||
.join('');
|
||||
return valueNew;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断是否Array对象
|
||||
* @param {Object} value 判断的对象
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isArray: function(value) {
|
||||
return toString.call(value) === '[object Array]';
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断是否日期对象
|
||||
* @param {Object} value 判断的对象
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isDate: function(value) {
|
||||
return toString.call(value) === '[object Date]';
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断是否Object对象
|
||||
* @param {Object} value 判断的对象
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isObject: function(value) {
|
||||
return toString.call(value) === '[object Object]';
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断是否为空
|
||||
* @param {Object} value 判断的对象
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isEmpty: function(value) {
|
||||
return value === null || value === undefined || value === '' || (this.isArray(value) && value.length === 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断是否移动电话
|
||||
* @param {Number} value 判断的值
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isMobilePhone: function(value) {
|
||||
value = Number.parseInt(value);
|
||||
// 1)是否非数字
|
||||
if (Number.isNaN(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2)时候移动电话
|
||||
return /^1[3|4|5|7|8|9|6][0-9]\d{4,8}$/.test(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断是否为邮箱
|
||||
* @param {String} value 判断的值
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isEmail: function(value) {
|
||||
return /^[a-zA-Z\-_0-9]+@[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+$/.test(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* 转换服务器请求的对象为Js的对象:包含首字母转换为小写;属性格式转换为Js支持的格式
|
||||
* @param {Object} en 服务器的获取的数据对象
|
||||
*/
|
||||
transWebServerObj: function(en) {
|
||||
if (toString.call(en) == '[object Array]') {
|
||||
for (var i = 0, len = en.length; i < len; i++) {
|
||||
ak.Utils.transWebServerObj(en[i]);
|
||||
}
|
||||
} else {
|
||||
for (propertyName in en) {
|
||||
/*
|
||||
// 1.创建一个小写的首字母属性并赋值:ABC => aBC
|
||||
var newPropertyName = propertyName.charAt(0).toLowerCase() + propertyName.substr(1);
|
||||
en[newPropertyName] = en[propertyName];
|
||||
*/
|
||||
var tmpName = propertyName;
|
||||
// 2.判断此属性是否为数组,若是就执行递归
|
||||
if (toString.call(en[tmpName]) == '[object Array]') {
|
||||
for (var i = 0, len = en[tmpName].length; i < len; i++) {
|
||||
ak.Utils.transWebServerObj(en[tmpName][i]); // 数组里的每个对象再依次进行转换
|
||||
}
|
||||
} else if (toString.call(en[tmpName]) == '[object Object]') {
|
||||
ak.Utils.transWebServerObj(en[tmpName]); // 若属性的值是一个对象,也要进行转换
|
||||
} else {
|
||||
// 3.若不是其他类型,把此属性的值转换为Js的数据格式
|
||||
// 3.1)日期格式:后台为2015-12-08T09:23:23.917 => 2015-12-08 09:23:23
|
||||
if (new RegExp(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/).test(en[propertyName])) {
|
||||
// en[propertyName] = new RegExp(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/).exec(en[propertyName])[0].replace('T', ' ');
|
||||
|
||||
// 若为0001年,表示时间为空,就返回''空字符串
|
||||
if (en[propertyName].indexOf('0001') >= 0) {
|
||||
en[propertyName] = '';
|
||||
}
|
||||
} else if (toString.call(en[propertyName]) == '[object Number]' && new RegExp(/\d+[.]\d{3}/).test(en[propertyName])) {
|
||||
// 3.2)溢出的float格式:1.33333 = > 1.33
|
||||
en[propertyName] = en[propertyName].toFixed(2);
|
||||
} else if (en[propertyName] == null) {
|
||||
// 3.3)null值返回空
|
||||
en[propertyName] = '';
|
||||
} else if (
|
||||
['imgPath', 'loopImgPath', 'clubIcon', 'headImgPath'].indexOf(propertyName) >= 0 &&
|
||||
en[propertyName] &&
|
||||
en[propertyName].length > 0
|
||||
) {
|
||||
en[propertyName] = ak.Base_URL + en[propertyName].replace('..', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return en;
|
||||
},
|
||||
|
||||
/**
|
||||
*设置SessionStorage的值
|
||||
* @param key:要存的键
|
||||
* @param value :要存的值
|
||||
*/
|
||||
setSessionStorage: function(key, value) {
|
||||
if (this.isObject(value) || this.isArray(value)) {
|
||||
value = this.toJsonStr(value);
|
||||
}
|
||||
sessionStorage[key] = value;
|
||||
},
|
||||
|
||||
/**
|
||||
*获取SessionStorage的值
|
||||
* @param key:存的键
|
||||
*/
|
||||
getSessionStorage: function(key) {
|
||||
var rs = sessionStorage[key];
|
||||
try {
|
||||
if (rs != undefined) {
|
||||
var obj = this.toJson(rs);
|
||||
rs = obj;
|
||||
}
|
||||
} catch (error) {}
|
||||
return rs;
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除SessionStorage的值
|
||||
* @param key:存的键
|
||||
*/
|
||||
removeSessionStorage: function(key) {
|
||||
return sessionStorage.removeItem(key);
|
||||
},
|
||||
|
||||
/**
|
||||
*设置LocalStorage的值
|
||||
* @param key:要存的键
|
||||
* @param value :要存的值
|
||||
*/
|
||||
setLocalStorage: function(key, value) {
|
||||
if (this.isObject(value) || this.isArray(value)) {
|
||||
value = this.toJsonStr(value);
|
||||
}
|
||||
localStorage[key] = value;
|
||||
},
|
||||
|
||||
/**
|
||||
*获取LocalStorage的值
|
||||
* @param key:存的键
|
||||
*/
|
||||
getLocalStorage: function(key) {
|
||||
var rs = localStorage[key];
|
||||
try {
|
||||
if (rs != undefined) {
|
||||
var obj = this.toJson(rs);
|
||||
rs = obj;
|
||||
}
|
||||
} catch (error) {}
|
||||
return rs;
|
||||
},
|
||||
|
||||
/**
|
||||
* 对传入的时间值进行格式化。后台传入前台的时间有两种个是:Sql时间和.Net时间
|
||||
* @param {String|Date} sValue 传入的时间字符串
|
||||
* @param {dateFormat | bool} dateFormat 日期格式,日期格式:eg:'Y-m-d H:i:s'
|
||||
* @return {String} 2014-03-01 这种格式
|
||||
* @example
|
||||
* 1) Sql时间格式:2015-02-24T00:00:00
|
||||
* 2) .Net时间格式:/Date(1410744626000)/
|
||||
*/
|
||||
getDateTimeStr: function(sValue, dateFormat) {
|
||||
if (dateFormat == undefined) {
|
||||
dateFormat = 'Y-m-d'; // 默认显示年月日
|
||||
}
|
||||
|
||||
var dt;
|
||||
// 1.先解析传入的时间对象,
|
||||
if (sValue) {
|
||||
if (toString.call(sValue) !== '[object Date]') {
|
||||
// 不为Date格式,就转换为DateTime类型
|
||||
sValue = sValue + '';
|
||||
if (sValue.indexOf('T') > 0) {
|
||||
// 1)格式:2015-02-24T00:00:00
|
||||
var timestr = sValue.replace('T', ' ').replace(/-/g, '/'); //=> 2015/02/24 00:00:00
|
||||
dt = new Date(timestr);
|
||||
} else if (sValue.indexOf('Date') >= 0) {
|
||||
// 2).Net格式:/Date(1410744626000)/
|
||||
//Convert date type that .NET can bind to DateTime
|
||||
//var date = new Date(parseInt(sValue.substr(6)));
|
||||
var timestr = sValue.toString().replace(/\/Date\((\d+)\)\//gi, '$1'); //
|
||||
dt = new Date(Math.abs(timestr));
|
||||
} else {
|
||||
dt = new Date(sValue);
|
||||
}
|
||||
} else {
|
||||
dt = sValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 2.转换
|
||||
// 1)转换成对象 'Y-m-d H:i:s'
|
||||
var obj = {}; //返回的对象,包含了 year(年)、month(月)、day(日)
|
||||
obj.Y = dt.getFullYear(); //年
|
||||
obj.m = dt.getMonth() + 1; //月
|
||||
obj.d = dt.getDate(); //日期
|
||||
obj.H = dt.getHours();
|
||||
obj.i = dt.getMinutes();
|
||||
obj.s = dt.getSeconds();
|
||||
//2.2单位的月、日都转换成双位
|
||||
if (obj.m < 10) {
|
||||
obj.m = '0' + obj.m;
|
||||
}
|
||||
if (obj.d < 10) {
|
||||
obj.d = '0' + obj.d;
|
||||
}
|
||||
if (obj.H < 10) {
|
||||
obj.H = '0' + obj.H;
|
||||
}
|
||||
if (obj.i < 10) {
|
||||
obj.i = '0' + obj.i;
|
||||
}
|
||||
if (obj.s < 10) {
|
||||
obj.s = '0' + obj.s;
|
||||
}
|
||||
// 3.解析
|
||||
var rs = dateFormat
|
||||
.replace('Y', obj.Y)
|
||||
.replace('m', obj.m)
|
||||
.replace('d', obj.d)
|
||||
.replace('H', obj.H)
|
||||
.replace('i', obj.i)
|
||||
.replace('s', obj.s);
|
||||
|
||||
return rs;
|
||||
},
|
||||
|
||||
/**
|
||||
* 把总秒数转换为时分秒
|
||||
*/
|
||||
getSFM: function(seconds, dateFormat) {
|
||||
if (dateFormat == undefined) {
|
||||
dateFormat = 'H:i:s'; // 默认格式
|
||||
}
|
||||
var obj = {};
|
||||
obj.H = Number.parseInt(seconds / 3600);
|
||||
obj.i = Number.parseInt((seconds - obj.H * 3600) / 60);
|
||||
obj.s = Number.parseInt(seconds - obj.H * 3600 - obj.i * 60);
|
||||
if (obj.H < 10) {
|
||||
obj.H = '0' + obj.H;
|
||||
}
|
||||
if (obj.i < 10) {
|
||||
obj.i = '0' + obj.i;
|
||||
}
|
||||
if (obj.s < 10) {
|
||||
obj.s = '0' + obj.s;
|
||||
}
|
||||
|
||||
// 3.解析
|
||||
var rs = dateFormat
|
||||
.replace('H', obj.H)
|
||||
.replace('i', obj.i)
|
||||
.replace('s', obj.s);
|
||||
return rs;
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否同一天
|
||||
*/
|
||||
isSomeDay: function(dt1, dt2) {
|
||||
if (dt1.getFullYear() == dt2.getFullYear() && dt1.getMonth() == dt2.getMonth() && dt1.getDate() == dt2.getDate()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 对象转换为json字符串
|
||||
* @param {jsonObj} jsonObj Json对象
|
||||
* @return {jsonStr} Json字符串
|
||||
*/
|
||||
toJsonStr: function(jsonObj) {
|
||||
return JSON.stringify(jsonObj);
|
||||
},
|
||||
|
||||
/**
|
||||
* 讲json字符串转换为json对象
|
||||
* @param {String} jsonStr Json对象字符串
|
||||
* @return {jsonObj} Json对象
|
||||
*/
|
||||
toJson: function(jsonStr) {
|
||||
return JSON.parse(jsonStr);
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getCookieVal: function(offset) {
|
||||
var endstr = document.cookie.indexOf(';', offset);
|
||||
if (endstr == -1) {
|
||||
endstr = document.cookie.length;
|
||||
}
|
||||
return unescape(document.cookie.substring(offset, endstr));
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取指定key的cookie
|
||||
* @param {String} key cookie的key
|
||||
*/
|
||||
getCookie: function(key) {
|
||||
var arg = key + '=',
|
||||
alen = arg.length,
|
||||
clen = document.cookie.length,
|
||||
i = 0,
|
||||
j = 0;
|
||||
|
||||
while (i < clen) {
|
||||
j = i + alen;
|
||||
if (document.cookie.substring(i, j) == arg) {
|
||||
return this.getCookieVal(j);
|
||||
}
|
||||
i = document.cookie.indexOf(' ', i) + 1;
|
||||
if (i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置cookie
|
||||
* @param {String} key cookie的key
|
||||
* @param {String} value cookie的value
|
||||
*/
|
||||
setCookie: function(key, value) {
|
||||
var argv = arguments,
|
||||
argc = arguments.length,
|
||||
expires = argc > 2 ? argv[2] : null,
|
||||
path = argc > 3 ? argv[3] : '/',
|
||||
domain = argc > 4 ? argv[4] : null,
|
||||
secure = argc > 5 ? argv[5] : false;
|
||||
|
||||
document.cookie =
|
||||
key +
|
||||
'=' +
|
||||
escape(value) +
|
||||
(expires === null ? '' : '; expires=' + expires.toGMTString()) +
|
||||
(path === null ? '' : '; path=' + path) +
|
||||
(domain === null ? '' : '; domain=' + domain) +
|
||||
(secure === true ? '; secure' : '');
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否含有特殊字符
|
||||
* @param {String} value 传入的值
|
||||
* @return {Boolean} true 含有特殊符号;false 不含有特殊符号
|
||||
*/
|
||||
isHaveSpecialChar: function(value) {
|
||||
var oldLength = value.length;
|
||||
var newLength = value.replace(/[`~!@#$%^&*_+=\\{}:"<>?\[\];',.\/~!@#¥%……&*——+『』:“”《》?【】;‘’,。? \[\]()()]/g, '').length;
|
||||
if (newLength < oldLength) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 合并数组内成员的某个对象
|
||||
* @param {Array} arr 需要合并的数组
|
||||
* @param {String} fieldName 数组成员内的指定字段
|
||||
* @param {String} split 分隔符,默认为','
|
||||
* @example
|
||||
* var arr = [{name:'tom',age:13},{name:'jack',age:13}] => (arr, 'name') => tom,jack
|
||||
*/
|
||||
joinArray: function(arr, fieldName, split) {
|
||||
split = split == undefined ? ',' : split;
|
||||
var rs = arr
|
||||
.map((item) => {
|
||||
return item[fieldName];
|
||||
})
|
||||
.join(split);
|
||||
return rs;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* http交互模块
|
||||
* 包含:ajax
|
||||
*/
|
||||
ak.Http = {
|
||||
/**
|
||||
* 将`name` - `value`对转换为支持嵌套结构的对象数组
|
||||
*
|
||||
* var objects = toQueryObjects('hobbies', ['reading', 'cooking', 'swimming']);
|
||||
*
|
||||
* // objects then equals:
|
||||
* [
|
||||
* { name: 'hobbies', value: 'reading' },
|
||||
* { name: 'hobbies', value: 'cooking' },
|
||||
* { name: 'hobbies', value: 'swimming' },
|
||||
* ];
|
||||
*
|
||||
* var objects = toQueryObjects('dateOfBirth', {
|
||||
* day: 3,
|
||||
* month: 8,
|
||||
* year: 1987,
|
||||
* extra: {
|
||||
* hour: 4
|
||||
* minute: 30
|
||||
* }
|
||||
* }, true); // Recursive
|
||||
*
|
||||
* // objects then equals:
|
||||
* [
|
||||
* { name: 'dateOfBirth[day]', value: 3 },
|
||||
* { name: 'dateOfBirth[month]', value: 8 },
|
||||
* { name: 'dateOfBirth[year]', value: 1987 },
|
||||
* { name: 'dateOfBirth[extra][hour]', value: 4 },
|
||||
* { name: 'dateOfBirth[extra][minute]', value: 30 },
|
||||
* ];
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {object | Array} value
|
||||
* @param {boolean} [recursive=false] 是否递归
|
||||
* @return {array}
|
||||
*/
|
||||
toQueryObjects: function(name, value, recursive) {
|
||||
var objects = [],
|
||||
i,
|
||||
ln;
|
||||
|
||||
if (ak.Utils.isArray(value)) {
|
||||
for (i = 0, ln = value.length; i < ln; i++) {
|
||||
if (recursive) {
|
||||
objects = objects.concat(toQueryObjects(name + '[' + i + ']', value[i], true));
|
||||
} else {
|
||||
objects.push({
|
||||
name: name,
|
||||
value: value[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (ak.Utils.isObject(value)) {
|
||||
for (i in value) {
|
||||
if (value.hasOwnProperty(i)) {
|
||||
if (recursive) {
|
||||
objects = objects.concat(toQueryObjects(name + '[' + i + ']', value[i], true));
|
||||
} else {
|
||||
objects.push({
|
||||
name: name,
|
||||
value: value[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
objects.push({
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
return objects;
|
||||
},
|
||||
|
||||
/**
|
||||
* 把对象转换为查询字符串
|
||||
* e.g.:
|
||||
* toQueryString({foo: 1, bar: 2}); // returns "foo=1&bar=2"
|
||||
* toQueryString({foo: null, bar: 2}); // returns "foo=&bar=2"
|
||||
* toQueryString({date: new Date(2011, 0, 1)}); // returns "date=%222011-01-01T00%3A00%3A00%22"
|
||||
* @param {Object} object 需要转换的对象
|
||||
* @param {Boolean} [recursive=false] 是否递归
|
||||
* @return {String} queryString
|
||||
*/
|
||||
toQueryString: function(object, recursive) {
|
||||
var paramObjects = [],
|
||||
params = [],
|
||||
i,
|
||||
j,
|
||||
ln,
|
||||
paramObject,
|
||||
value;
|
||||
|
||||
for (i in object) {
|
||||
if (object.hasOwnProperty(i)) {
|
||||
paramObjects = paramObjects.concat(this.toQueryObjects(i, object[i], recursive));
|
||||
}
|
||||
}
|
||||
|
||||
for (j = 0, ln = paramObjects.length; j < ln; j++) {
|
||||
paramObject = paramObjects[j];
|
||||
value = paramObject.value;
|
||||
|
||||
if (ak.Utils.isEmpty(value)) {
|
||||
value = '';
|
||||
} else if (ak.Utils.isDate(value)) {
|
||||
value =
|
||||
value.getFullYear() +
|
||||
'-' +
|
||||
Ext.String.leftPad(value.getMonth() + 1, 2, '0') +
|
||||
'-' +
|
||||
Ext.String.leftPad(value.getDate(), 2, '0') +
|
||||
'T' +
|
||||
Ext.String.leftPad(value.getHours(), 2, '0') +
|
||||
':' +
|
||||
Ext.String.leftPad(value.getMinutes(), 2, '0') +
|
||||
':' +
|
||||
Ext.String.leftPad(value.getSeconds(), 2, '0');
|
||||
}
|
||||
|
||||
params.push(encodeURIComponent(paramObject.name) + '=' + encodeURIComponent(String(value)));
|
||||
}
|
||||
|
||||
return params.join('&');
|
||||
},
|
||||
|
||||
/**
|
||||
* 以get方式请求获取JSON数据
|
||||
* @param {Object} opts 配置项,可包含以下成员:
|
||||
* @param {String} opts.url 请求地址
|
||||
* @param {Object} opts.params 附加的请求参数
|
||||
* @param {Boolean} opts.isHideLoading 是否关闭'载入中'提示框,默认false
|
||||
* @param {String} opts.loadingTitle '载入中'提示框title,e.g. 提交中、上传中
|
||||
* @param {Function} opts.successCallback 成功接收内容时的回调函数
|
||||
* @param {Function} opts.failCallback 失败的回调函数
|
||||
*/
|
||||
get: function(opts) {
|
||||
if (!opts.isHideLoading) {
|
||||
ak.Msg.showLoading(opts.loadingTitle);
|
||||
}
|
||||
if (opts.url.substr(0, 1) == '/') {
|
||||
opts.url = opts.url.substr(1);
|
||||
}
|
||||
opts.url = ak.Base_URL + opts.url;
|
||||
if (opts.params) {
|
||||
opts.url = opts.url + '?' + this.toQueryString(opts.params);
|
||||
}
|
||||
// Jquery、Zepto
|
||||
$.getJSON(
|
||||
opts.url,
|
||||
function(res, status, xhr) {
|
||||
ak.Msg.hideLoading();
|
||||
if (res.resultCode == '0') {
|
||||
if (opts.successCallback) {
|
||||
opts.successCallback(res);
|
||||
}
|
||||
} else {
|
||||
ak.Msg.toast(res.resultText, 'error');
|
||||
if (opts.failCallback) {
|
||||
opts.failCallback(res);
|
||||
}
|
||||
}
|
||||
},
|
||||
'json'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 以get方式请求获取JSON数据
|
||||
* @param {Object} opts 配置项,可包含以下成员:
|
||||
* @param {String} opts.url 请求地址
|
||||
* @param {Object} opts.params 附加的请求参数
|
||||
* @param {Boolean} opts.ignoreFail 忽略错误,默认false,不管返回的结果如何,都执行 successCallback
|
||||
* @param {Boolean} opts.ignoreEmptyParam 忽略空值,默认true
|
||||
* @param {Boolean} opts.isHideLoading 是否关闭'载入中'提示框,默认false
|
||||
* @param {String} opts.loadingTitle '载入中'提示框title,e.g. 提交中、上传中
|
||||
* @param {Function} opts.successCallback 成功接收内容时的回调函数
|
||||
* @param {Function} opts.failCallback 失败的回调函数
|
||||
*/
|
||||
post: function(opts) {
|
||||
opts.ignoreFail = opts.ignoreFail == undefined ? false : opts.ignoreFail;
|
||||
opts.ignoreEmptyParam = opts.ignoreEmptyParam == undefined ? true : opts.ignoreEmptyParam;
|
||||
if (!opts.isHideLoading) {
|
||||
ak.Msg.showLoading(opts.loadingTitle);
|
||||
}
|
||||
if (opts.url.substr(0, 1) == '/') {
|
||||
opts.url = opts.url.substr(1);
|
||||
}
|
||||
opts.url = ak.Base_URL + opts.url; // test
|
||||
|
||||
// 去除params的空值
|
||||
if (opts.ignoreEmptyParam) {
|
||||
for (var key in opts.params) {
|
||||
if (opts.params[key] == undefined || opts.params[key] == '') {
|
||||
delete opts.params[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Jquery、Zepto
|
||||
$.post(
|
||||
opts.url,
|
||||
opts.params,
|
||||
function(res, status, xhr) {
|
||||
ak.Msg.hideLoading();
|
||||
if (res.resultCode == '0' || opts.ignoreFail) {
|
||||
if (opts.successCallback) {
|
||||
opts.successCallback(res);
|
||||
}
|
||||
} else {
|
||||
ak.Msg.toast(res.resultText, 'error');
|
||||
if (opts.failCallback) {
|
||||
opts.failCallback(res);
|
||||
}
|
||||
}
|
||||
},
|
||||
'json'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {Object} opts 配置项,可包含以下成员:
|
||||
* @param {Object} opts.params 上传的参数
|
||||
* @param {Object} opts.fileParams 上传文件参数
|
||||
* @param {String} opts.url 请求地址
|
||||
* @param {Function} opts.successCallback 成功接收内容时的回调函数
|
||||
* @param {Function} opts.failCallback 失败的回调函数
|
||||
*/
|
||||
uploadFile: function(opts) {
|
||||
// 1.解析url
|
||||
if (opts.url.substr(0, 1) == '/') {
|
||||
opts.url = opts.url.substr(1);
|
||||
}
|
||||
opts.url = ak.Base_URL + opts.url;
|
||||
if (opts.params) {
|
||||
opts.url = opts.url + '?' + this.toQueryString(opts.params);
|
||||
}
|
||||
|
||||
// 2.文件参数
|
||||
var formData = new FormData();
|
||||
for (var key in opts.fileParams) {
|
||||
formData.append(key, opts.fileParams[key]);
|
||||
}
|
||||
|
||||
// 3.发起ajax
|
||||
$.ajax({
|
||||
url: opts.url,
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function(res) {
|
||||
if (res.resultCode != '0') {
|
||||
ak.Msg.toast(res.resultText, 'error');
|
||||
}
|
||||
if (opts.successCallback) {
|
||||
opts.successCallback(res);
|
||||
}
|
||||
})
|
||||
.fail(function(res) {
|
||||
if (opts.failCallback) {
|
||||
opts.failCallback(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 消息模块
|
||||
* 包含:确认框、信息提示框
|
||||
*/
|
||||
ak.Msg = {
|
||||
/**
|
||||
* 提示框
|
||||
* msg {string} :信息内容
|
||||
*/
|
||||
alert: function(msg) {},
|
||||
|
||||
/**
|
||||
* 确认框
|
||||
* msg {string} :信息内容
|
||||
* callback {function} :点击'确定'时的回调函数。
|
||||
*/
|
||||
confirm: function(msg, callback) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示正在加载
|
||||
* @param {String} title 显示的title
|
||||
*/
|
||||
showLoading: function(title) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭正在加载
|
||||
*/
|
||||
hideLoading: function() {},
|
||||
|
||||
/**
|
||||
* 自动消失的提示框
|
||||
* @param {String} msg 信息内容
|
||||
*/
|
||||
toast: function(msg) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* 业务相关逻辑
|
||||
*/
|
||||
ak.BLL = {};
|
||||
|
||||
export default ak;
|
|
@ -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';
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
// divEl下的元素,依次插入到DocumentFragment
|
||||
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, '<').replace(/>/g, '>'));
|
||||
}
|
||||
}
|
||||
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
Loading…
Reference in New Issue