diff --git a/define.js b/define.js
new file mode 100644
index 0000000000000000000000000000000000000000..1083195a577268a71b8891fca710e8bf8eb2b807
--- /dev/null
+++ b/define.js
@@ -0,0 +1,64 @@
+'use strict'
+
+exports.wsEventType = [
+ 'log',
+ 'cmdRet',
+ 'userEvent',
+ 'sysEvent',
+]
+
+exports.userEvents = [
+ 'qrcode', // 登陆二维码
+ 'scan', // 扫码登陆状态
+ 'login', // 登陆完成
+ 'loaded', // 通讯录载入完毕
+ 'logout', // 注销登录(账户退出)
+ 'close', // 任务断线
+ 'warn', // 错误
+ 'sns', // 朋友圈事件(朋友圈小圆点)
+ 'notify', // 推送消息通知
+ 'autoLogin', // 自动重连成功通知
+ 'push', // 推送消息(系统、好友消息等)
+]
+
+exports.loginType = {
+ auto : 'auto', // 断线重连
+ request: 'request', // 二次登陆
+ qrcode : 'qrcode', // 扫码登陆
+ phone : 'phone', // 手机验证码登陆
+ user : 'user', // 账号密码登陆
+}
+
+exports.blacklist = [
+ 'appbrandcustomerservicemsg',
+ 'blogapp',
+ 'cardpackage',
+ 'facebookapp',
+ 'feedsapp',
+ 'filehelper',
+ 'floatbottle',
+ 'floatbottle', // 漂流瓶
+ 'fmessage', // 朋友推荐
+ 'helper_entry',
+ 'lbsapp',
+ 'linkedinplugin',
+ 'masssendapp',
+ 'medianote', // 语音记事本
+ 'mphelper', // 公众平台安全助手
+ 'newsapp', // 腾讯新闻
+ 'notification_messages',
+ 'notifymessage',
+ 'officialaccounts',
+ 'pc_share',
+ 'qmessage', // qq离线消息
+ 'qqfriend',
+ 'qqmail',
+ 'qqsync',
+ 'shakeapp',
+ 'tmessage',
+ 'voiceinputapp',
+ 'voicevoipapp',
+ 'voipapp',
+ 'weibo', // 微博-未知
+ 'weixin', // 腾讯团队
+]
\ No newline at end of file
diff --git a/demo.js b/demo.js
new file mode 100644
index 0000000000000000000000000000000000000000..e664c80c4ab17c2f6af4ebadfca3188e6d90f31d
--- /dev/null
+++ b/demo.js
@@ -0,0 +1,406 @@
+'use strict'
+
+const log4js = require('log4js')
+const Padchat = require('./index')
+const fs = require('fs')
+const util = require('util')
+const qrcode = require('qrcode-terminal')
+
+/**
+* 创建日志目录
+*/
+
+try {
+ require('fs').mkdirSync('./logs')
+} catch (e) {
+ if (e.code !== 'EEXIST') {
+ console.error('Could not set up log directory, error: ', e)
+ process.exit(1)
+ }
+}
+
+// try {
+// log4js.configure('./log4js.json')
+// } catch (e) {
+// console.error('载入log4js日志输出配置错误: ', e)
+// process.exit(1);
+// }
+
+const logger = log4js.getLogger('app')
+const dLog = log4js.getLogger('dev')
+
+logger.info('demo start!')
+
+const autoData = {
+ wxData: '',
+ token: '',
+}
+let server = ''
+server = 'ws://47.99.211.34:8181/'
+// 永久免费使用
+
+try {
+ const tmpBuf = fs.readFileSync('./config.json')
+ const data = JSON.parse(String(tmpBuf))
+ autoData.wxData = data.wxData
+ autoData.token = data.token
+ logger.info('载入设备参数与自动登陆数据:%o ', autoData)
+} catch (e) {
+ logger.warn('没有在本地发现设备登录参数或解析数据失败!如首次登录请忽略!')
+}
+
+const wx = new Padchat(server)
+logger.info('当前连接接口服务器为:', server)
+let disconnectCount = 0 // 断开计数
+let connected = false // 成功连接标志
+
+wx
+ .on('close', (code, msg) => {
+ // 需要注意关闭代码为3201-3203的错误,重连也用,具体参考文档中`close`事件说明
+ if (code > 3200) {
+ logger.warn(`Websocket已关闭!code: ${code} - ${msg}`)
+ return
+ }
+ logger.info(`Websocket已关闭!code: ${code} - ${msg}`)
+ // 根据是否成功连接过判断本次是未能连接成功还是与服务器连接中断
+ if (connected) {
+ connected = false
+ disconnectCount++
+ logger.info(`第 ${disconnectCount} 次与服务器连接断开!现在将重试连接服务器。`)
+ } else {
+ logger.debug(`未能连接服务器!将重试连接服务器。`)
+ }
+ // 重新启动websocket连接
+ wx.start()
+ })
+ .on('open', async () => {
+ let ret
+ logger.info('连接成功!')
+ connected = true
+
+ // 非首次登录时最好使用以前成功登录时使用的设备参数,
+ // 否则可能会被tx服务器怀疑账号被盗,导致手机端被登出
+ ret = await wx.init()
+ if (!ret.success) {
+ logger.error('新建任务失败!', ret)
+ return
+ }
+ logger.info('新建任务成功, json: ', ret)
+
+ //先尝试使用断线重连方式登陆
+ if (autoData.token) {
+ ret = await wx.login('auto', autoData)
+ if (ret.success) {
+ logger.info('断线重连请求成功!', ret)
+ return
+ }
+ logger.warn('断线重连请求失败!', ret)
+
+ ret = await wx.login('request', autoData)
+ if (ret.success) {
+ logger.info('自动登录请求成功!', ret)
+ return
+ }
+ logger.warn('自动登录请求失败!', ret)
+ }
+
+ ret = await wx.login('qrcode')
+ if (!ret.success) {
+ logger.error('使用qrcode登录模式失败!', ret)
+ return
+ }
+ logger.info('使用qrcode登录模式!')
+ })
+ .on('qrcode', data => {
+ // 如果存在url,则直接在终端中生成二维码并显示
+ if (data.url) {
+ logger.info(`登陆二维码内容为: "${data.url}",请使用微信扫描下方二维码登陆!`)
+ qrcode.generate(data.url, { small: true })
+ } else {
+ logger.error(`未能获得登陆二维码!`)
+ }
+ })
+ .on('scan', data => {
+ switch (data.status) {
+ case 0:
+ logger.info('等待扫码...', data)
+ break;
+ case 1:
+ // {
+ // status : 1,
+ // expiredTime: 239,
+ // headUrl : 'http://wx.qlogo.cn/mmhead/ver_1/xxxxxxx/0', //头像url
+ // nickName : '复仇者联盟' //昵称
+ // }
+ logger.info('已扫码,请在手机端确认登陆...', data)
+ break;
+ case 2:
+ // {
+ // password : '***hide***', // 可忽略
+ // status : 2,
+ // expiredTime: 238,
+ // headUrl : 'http://wx.qlogo.cn/mmhead/ver_1/xxxxxxx/0', //头像url
+ // subStatus : 0 // 登陆操作状态码
+ // 以下字段仅在登录成功时有效
+ // external : '1',
+ // email : '',
+ // uin : 149806460, // 微信账号uin,全局唯一
+ // deviceType : 'android', // 登陆的主设备类型
+ // nickName : '复仇者联盟' //昵称
+ // userName : 'wxid_xxxxxx', // 微信账号id,全局唯一
+ // phoneNumber: '18012345678', // 微信账号绑定的手机号
+ // }
+ switch (data.subStatus) {
+ case 0:
+ logger.info('扫码成功!登陆成功!', data)
+ break;
+ case 1:
+ logger.info('扫码成功!登陆失败!', data)
+ break;
+ default:
+ logger.info('扫码成功!未知状态码!', data)
+ break;
+ }
+ break;
+ // 如果等待登陆超时或手机上点击了取消登陆,需要重新调用登陆
+ case 3:
+ logger.info('二维码已过期!请重新调用登陆接口!', data)
+ break;
+ case 4:
+ logger.info('手机端已取消登陆!请重新调用登陆接口!', data)
+ break;
+ default:
+ logger.warn('未知登陆状态!请重新调用登陆接口!', data)
+ break;
+ }
+ })
+ .on('login', async () => {
+ logger.info('微信账号登陆成功!')
+ let ret
+
+ ret = await wx.getMyInfo()
+ logger.info('当前账号信息:', ret.data)
+
+ // 如果不想同步通讯录信息,可关闭同步通讯录
+ // await wx.setSyncContact(false)
+
+ // 主动同步通讯录
+ await wx.syncContact()
+
+ await saveAutoData()
+ })
+ .on('autoLogin', async () => {
+ // 自动重连后需要保存新的自动登陆数据
+ await saveAutoData()
+ })
+ .on('logout', ({ msg }) => {
+ logger.info('微信账号已退出!', msg)
+ })
+ .on('over', ({ msg }) => {
+ logger.info('任务实例已关闭!', msg)
+ })
+ .on('loaded', async () => {
+ logger.info('通讯录同步完毕!')
+
+ // 主动触发同步消息
+ await wx.syncMsg()
+
+ const ret = await wx.sendMsg('filehelper', '你登录了!')
+ logger.info('发送信息结果:', ret)
+ })
+ .on('sns', (data, msg) => {
+ logger.info('收到朋友圈事件!请查看朋友圈新消息哦!', msg)
+ })
+
+ .on('contact', async data => {
+ logger.info('收到推送联系人:%s - %s\n', data.userName, data.nickName, JSON.stringify(data))
+ })
+ .on('push', async data => {
+ // 消息类型 data.mType
+ // 1 文字消息
+ // 3 收到图片消息
+ // 34 语音消息
+ // 35 用户头像buf
+ // 37 收到好友请求消息
+ // 42 名片消息
+ // 43 视频消息
+ // 47 表情消息
+ // 48 定位消息
+ // 49 APP消息(文件 或者 链接 H5)
+ // 50 语音通话
+ // 51 状态通知(如打开与好友/群的聊天界面)
+ // 52 语音通话通知
+ // 53 语音通话邀请
+ // 62 小视频
+ // 2000 转账消息
+ // 2001 收到红包消息
+ // 3000 群邀请
+ // 9999 系统通知
+ // 10000 微信通知信息. 微信群信息变更通知,多为群名修改,进群,离群信息,不包含群内聊天信息
+ // 10002 撤回消息
+ // --------------------------------
+ // 注意,如果是来自微信群的消息,data.content字段中包含发言人的wxid及其发言内容,需要自行提取
+ // 各类复杂消息,data.content中是xml格式的文本内容,需要自行从中提取各类数据。(如好友请求)
+ let rawFile
+ switch (data.mType) {
+ case 3:
+ logger.info('收到来自 %s 的图片消息,包含图片数据:%s,xml内容:\n%s', data.fromUser, !!data.data, data.content)
+ rawFile = data.data || null
+ logger.info('图片缩略图数据base64尺寸:%d', rawFile.length)
+ await wx.getMsgImage(data)
+ .then(ret => {
+ rawFile = ret.data.image || ''
+ logger.info('获取消息原始图片结果:%s, 获得图片base64尺寸:%d', ret.success, rawFile.length)
+ })
+ logger.info('图片数据base64尺寸:%d', rawFile.length)
+ await wx.sendImage('filehelper', rawFile)
+ .then(ret => {
+ logger.info('转发图片信息给 %s 结果:', 'filehelper', ret)
+ })
+ .catch(e => {
+ logger.warn('转发图片信息异常:', e.message)
+ })
+ break
+
+ case 43:
+ logger.info('收到来自 %s 的视频消息,包含视频数据:%s,xml内容:\n%s', data.fromUser, !!data.data, data.content)
+ rawFile = data.data || null
+ if (!rawFile) {
+ await wx.getMsgVideo(data)
+ .then(ret => {
+ rawFile = ret.data.video || ''
+ logger.info('获取消息原始视频结果:%s, 获得视频base64尺寸:%d', ret.success, rawFile.length)
+ })
+ }
+ logger.info('视频数据base64尺寸:%d', rawFile.length)
+ break
+
+ case 1:
+ if (data.fromUser === 'newsapp') { // 腾讯新闻发的信息太长
+ break
+ }
+ logger.info('收到来自 %s 的文本消息:', data.fromUser, data.description || data.content)
+ if (/ding/.test(data.content)) {
+ await wx.sendMsg(data.fromUser, 'dong. receive:' + data.content)
+ .then(ret => {
+ logger.info('回复信息给%s 结果:', data.fromUser, ret)
+ })
+ .catch(e => {
+ logger.warn('回复信息异常:', e.message)
+ })
+ } else if (/^#.*/.test(data.content) || /^[\w]*:\n#.*/.test(data.content)) {
+ await onMsg(data)
+ .catch(e => {
+ logger.warn('处理信息异常:', e)
+ })
+ }
+ break
+
+ case 34:
+ logger.info('收到来自 %s 的语音消息,包含语音数据:%s,xml内容:\n%s', data.fromUser, !!data.data, data.content)
+ // 超过30Kb的语音数据不会包含在推送信息中,需要主动拉取
+ rawFile = data.data || null
+ if (!rawFile) {
+ // BUG: 超过60Kb的语音数据,只能拉取到60Kb,也就是说大约36~40秒以上的语音会丢失后边部分语音内容
+ await wx.getMsgVoice(data)
+ .then(ret => {
+ rawFile = ret.data.voice || ''
+ logger.info('获取消息原始语音结果:%s, 获得语音base64尺寸:%d,拉取到数据尺寸:%d', ret.success, rawFile.length, ret.data.size)
+ })
+ }
+ logger.info('语音数据base64尺寸:%d', rawFile.length)
+ if (rawFile.length > 0) {
+ let match = data.content.match(/length="(\d+)"/) || []
+ const length = match[1] || 0
+ match = data.content.match(/voicelength="(\d+)"/) || []
+ const ms = match[1] || 0
+ logger.info('语音数据语音长度:%d ms,xml内记录尺寸:%d', ms, length)
+
+ await wx.sendVoice('filehelper', rawFile, ms)
+ .then(ret => {
+ logger.info('转发语音信息给 %s 结果:', 'filehelper', ret)
+ })
+ .catch(e => {
+ logger.warn('转发语音信息异常:', e.message)
+ })
+ }
+ break
+
+ case 49:
+ logger.info('收到一条来自 %s 的appmsg富媒体消息:', data.fromUser, data)
+ break
+
+ case 10002:
+ if (data.fromUser === 'weixin') {
+ //每次登陆,会收到一条系统垃圾推送,过滤掉
+ break
+ }
+ logger.info('用户 %s 撤回了一条消息:', data.fromUser, data)
+ break
+
+ default:
+ logger.info('收到推送消息:', data)
+ break
+ }
+ })
+ .on('error', e => {
+ logger.error('ws 错误:', e.message)
+ })
+ .on('warn', e => {
+ logger.error('任务出现错误:', e.message)
+ })
+
+/**
+ * @description 保存自动登陆数据
+ */
+async function saveAutoData() {
+ let ret = await wx.getWxData()
+ if (!ret.success) {
+ logger.warn('获取设备参数未成功! json:', ret)
+ return
+ }
+ logger.info('获取设备参数成功, json: ', ret)
+ Object.assign(autoData, { wxData: ret.data.wxData })
+
+ ret = await wx.getLoginToken()
+ if (!ret.success) {
+ logger.warn('获取自动登陆数据未成功! json:', ret)
+ return
+ }
+ logger.info('获取自动登陆数据成功, json: ', ret)
+ Object.assign(autoData, { token: ret.data.token })
+
+ // NOTE: 这里将设备参数保存到本地,以后再次登录此账号时提供相同参数
+ fs.writeFileSync('./config.json', JSON.stringify(autoData, null, 2))
+ logger.info('设备参数已写入到 ./config.json文件')
+}
+
+async function onMsg(data) {
+ const content = data.content.replace(/^[\w:\n]*#/m, '')
+ let [cmd, ...args] = content.split('\n')
+
+ args = args.map(str => {
+ try {
+ str = JSON.parse(str)
+ } catch (e) {
+ }
+ return str
+ })
+ if (cmd && wx[cmd] && typeof wx[cmd] === 'function') {
+ logger.info('执行函数 %s,参数:', cmd, args)
+ await wx[cmd](...args)
+ .then(ret => {
+ logger.info('执行函数 %s 结果:%o', cmd, ret)
+ })
+ .catch(e => {
+ logger.warn('执行函数 %s 异常:', e)
+ })
+ }
+}
+process.on('uncaughtException', e => {
+ logger.error('Main', 'uncaughtException:', e)
+})
+
+process.on('unhandledRejection', e => {
+ logger.error('Main', 'unhandledRejection:', e)
+})
\ No newline at end of file
diff --git a/helper.js b/helper.js
new file mode 100644
index 0000000000000000000000000000000000000000..a7c8c2809292789b1e0518a770ff189bc1969703
--- /dev/null
+++ b/helper.js
@@ -0,0 +1,103 @@
+'use strict'
+
+// 将object中的属性名称转换为全驼峰格式
+
+
+module.exports = {
+ toCamelCase,
+ toUnderLine,
+ structureXml,
+}
+
+/**
+ * 将object中的属性名称从下划线转换为驼峰格式
+ * (包含子数据的属性名,会遍历转换)
+ *
+ * @param {any} obj 要转换的object
+ * @param {boolean} [big=true] 是否转换为大驼峰格式
+ * @returns {Object} 转换后的object
+ */
+function toCamelCase(obj, big = false) {
+ if (obj instanceof Array) {
+ return obj.map(item => toCamelCase(item))
+ }
+ if (!(obj instanceof Object)) {
+ return obj
+ }
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ let newKey = key.replace(/_(\w)/g, (match, val, offset) => { return val.toUpperCase() })
+ if (big) {
+ newKey = newKey.replace(/^(\w)/, (match, val, offset) => { return val.toUpperCase() })
+ } else {
+ // 将首字母转换为小写(部分字段名称开头两个为大写,比如`PYInitial`/`MMBizMenu`)
+ // 跳过`X-WECHAT-KEY`和`X-WECHAT-UIN`这种字段
+ newKey = newKey.replace(/^(\w{2})/, (match, val, offset) => { return val.toLowerCase() })
+ }
+ if ((obj[key] instanceof Array) || (obj[key] instanceof Object)) {
+ obj[key] = toCamelCase(obj[key])
+ }
+ if (newKey !== key) {
+ obj[newKey] = obj[key]
+ delete obj[key]
+ }
+ }
+ }
+ return obj
+}
+
+/**
+ * 将object中的属性名称从驼峰转换为下划线格式
+ *
+ * @param {any} obj 要转换的object
+ * @returns {Object} 转换后的object
+ */
+function toUnderLine(obj) {
+ const newObj = {}
+ if (!(obj instanceof Object)) {
+ return obj
+ }
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ const newKey = key.replace(/([A-Z])/g, (match, val, offset) => { return (offset ? '_' : '') + val }).toLowerCase()
+ newObj[newKey] = obj[key]
+ }
+ }
+ return newObj
+}
+
+/**
+ * 组装appmsg消息体
+ *
+ * @param {object} object 消息体参数
+ * @param {object} [object.appid] - appid,忽略即可
+ * @param {object} [object.sdkver] - sdk版本,忽略即可
+ * @param {object} [object.title] - 标题
+ * @param {object} [object.des] - 描述
+ * @param {object} [object.url] - 链接url
+ * @param {object} [object.thumburl] - 缩略图url
+ * ```
+ * {
+ * appid = '', //appid,忽略即可
+ * sdkver = '', //sdk版本,忽略即可
+ * title = '', //标题
+ * des = '', //描述
+ * url = '', //链接url
+ * thumburl = '', //缩略图url
+ * }
+ * ```
+ * @returns {String} 组装的消息体
+ */
+function structureXml(object) {
+ const { appid = '', sdkver = '', title = '', des = '', url = '', thumburl = '' } = object
+ return `
+${title}
+${des}
+view
+5
+0
+
+${url}
+${thumburl}
+`.replace(/\n/g, '')
+}
\ No newline at end of file
diff --git a/index.js b/index.js
index e664c80c4ab17c2f6af4ebadfca3188e6d90f31d..bd1daeda098c3456a886ef661e688c1e91944ad9 100644
--- a/index.js
+++ b/index.js
@@ -1,406 +1,3001 @@
'use strict'
-const log4js = require('log4js')
-const Padchat = require('./index')
-const fs = require('fs')
-const util = require('util')
-const qrcode = require('qrcode-terminal')
+const EventEmitter = require('events')
+const Websocket = require('ws')
+const UUID = require('uuid')
+
+const Helper = require('./helper')
+const {
+ wsEventType,
+ loginType,
+ blacklist,
+} = require('./define')
+
+const server = 'ws://127.0.0.1:7777'
/**
-* 创建日志目录
-*/
+ * Padchat模块
+ *
+ * 使用websocket与服务器进行通讯,拥有以下事件
+ *
+ * Event | 说明
+ * ---- | ----
+ * qrcode | 推送的二维码
+ * scan | 扫码状态
+ * push | 新信息事件
+ * login | 登录
+ * loaded | 通讯录载入完毕
+ * logout | 注销登录
+ * over | 实例注销(账号不退出)(要再登录需要重新调用init)
+ * warn | 错误信息
+ * sns | 朋友圈更新事件
+ *
+ * **接口返回数据结构:** 所有接口均返回以下结构数据:
+ * ```
+ {
+ success: true, // 执行是否成功
+ err : '', // 错误提示
+ msg : '', // 附加信息
+ data : {} // 返回结果
+ }
+ * ```
+ *
+ * @class Padchat
+ * @extends {EventEmitter}
+ */
+class Padchat extends EventEmitter {
+ /**
+ * Creates an instance of Padchat.
+ * @param {string} [url] - 服务器url,后缀你的token:`ws://127.0.0.1:7777/{YourToken}`
+ * 如:`ws://127.0.0.1:7777/mytoken_123456`
+ * @memberof Padchat
+ */
+ constructor(url = server) {
+ super()
+ this.url = url
+ // 向ws服务器提交指令后,返回结果的超时时间,单位毫秒
+ this.sendTimeout = 30 * 1000
+ this.connected = false
+ this._lastStartTime = 0
+ this.ws = {}
+ this.openSyncMsg = true //是否同步消息
+ this.openSyncContact = true //是否同步联系人
+ this.loaded = false //通讯录载入完毕
+ this.cmdSeq = 1
+ this._starting = false
+ this.start()
+ }
-try {
- require('fs').mkdirSync('./logs')
-} catch (e) {
- if (e.code !== 'EEXIST') {
- console.error('Could not set up log directory, error: ', e)
- process.exit(1)
+ /**
+ * 启动websocket连接
+ *
+ * @memberof Padchat
+ */
+ async start() {
+ if (this._starting) {
+ return
+ }
+ this._starting = true
+ // 限制启动ws连接间隔时间
+ if (Date.now() - this._lastStartTime < 200) {
+ await sleep(1000)
+ }
+ this._lastStartTime = Date.now()
+ if (this.ws instanceof Websocket && this.ws.readyState === this.ws.OPEN) {
+ this.ws.terminate()
+ }
+ this.ws = new Websocket(this.url)
+ .on('message', (msg) => {
+ onWsMsg.call(this, msg)
+ })
+ .on('open', () => {
+ this.connected = true
+ /**
+ * Open event
+ * websocket连接打开事件
+ *
+ * @event Padchat#open
+ * @example
+ * const wx = new Padchat()
+ * wx.on('open',()=>{
+ * console.log(`连接成功!`)
+ * })
+ *
+ * @memberof Padchat
+ */
+ this.emit('open')
+ })
+ .on('close', (code = 0, msg = '') => {
+ this.connected = false
+ /**
+ * Close event
+ * websocket连接关闭事件。可在此事件中调用`Padchat.start()`发起重连
+ *
+ * @event Padchat#close
+ * @property {number} code - 关闭代码
+ *
`3201`: 你的Token是无效的,请联系我们获取有效的Token,以连接padchat-sdk
+ *
`3202`: 你的Token已经登录了一个机器人,请不要用同一个Token登录多个机器人
+ *
`3203`: 你的Token已经过期了,请联系我们进行续费
+ * @property {string} [msg] - 关闭说明
+ * @example
+ * const wx = new Padchat()
+ * wx.on('close',(code,msg)=>{
+ * console.log(`Websocket 已关闭!code: ${code} - ${msg}`)
+ * wx.start()
+ * })
+ *
+ * @memberof Padchat
+ */
+ this.emit('close', code, msg)
+ })
+ .on('error', (e) => {
+ /**
+ * Error event
+ * 错误事件
+ *
+ * @event Padchat#error
+ * @property {error} error - 报错信息
+ * @example
+ * const wx = new Padchat()
+ * wx.on('error',e=>{
+ * console.log('Websocket 错误:', e.message)
+ * })
+ *
+ * @memberof Padchat
+ */
+ this.emit('error', e)
+ })
+ this._starting = false
}
-}
-// try {
-// log4js.configure('./log4js.json')
-// } catch (e) {
-// console.error('载入log4js日志输出配置错误: ', e)
-// process.exit(1);
-// }
+ /**
+ * ws发送数据
+ *
+ * @param {object} data - 数据
+ * @returns {Promise