Full Code of fritx/wxbot for AI

next 38a99cceea22 cached
13 files
21.3 KB
7.2k tokens
41 symbols
1 requests
Download .txt
Repository: fritx/wxbot
Branch: next
Commit: 38a99cceea22
Files: 13
Total size: 21.3 KB

Directory structure:
gitextract_1_5is914/

├── .gitignore
├── Dockerfile
├── README.md
├── package.json
└── src/
    ├── index.js
    ├── ipc.js
    ├── parseMsg.js
    ├── preload.js
    ├── preloadIpc.js
    ├── replyMsg.js
    ├── runner.js
    ├── runnerIpc.js
    └── util.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules
download
*.out
*.log


================================================
FILE: Dockerfile
================================================
FROM ubuntu:latest

MAINTAINER huangruichang "532079207@qq.com"

RUN apt-get upgrade && apt-get update -y

EXPOSE 8233

# basic tools
RUN apt-get install git curl -y

# running electron on linux headless
# https://github.com/segmentio/nightmare/issues/224#issuecomment-141575361
RUN apt-get install -y libgtk2.0-0 libgconf-2-4 \
    libasound2 libxtst6 libxss1 libnss3 xvfb

# nodejs related
# https://nodejs.org/en/download/package-manager/
RUN curl -sL https://deb.nodesource.com/setup_7.x | bash
RUN apt-get install -y nodejs

# WXBOT HOME
ENV WXBOT_HOME /home/wxbot

# clone wxbot
RUN git clone https://github.com/fritx/wxbot.git $WXBOT_HOME

# replace npm and electron download source
RUN echo -e "registry=https://registry.npm.taobao.org\nelectron_mirror=https://npm.taobao.org/mirrors/electron/" > ~/.npmrc

RUN cd $WXBOT_HOME && npm install

# install http-server
RUN npm install http-server -g

# Start wxbot
CMD xvfb-run --server-args="-screen 0 1024x768x24" node $WXBOT_HOME & http-server /tmp -p 8233 -s


================================================
FILE: README.md
================================================
# wxbot 微信机器人

<a href="https://github.com/fritx/awesome-wechat"><img width="110" height="20" src="https://img.shields.io/badge/awesome-wechat-brightgreen.svg"></a>&nbsp;&nbsp;<a href="https://github.com/fritx/wxbot"><img width="74" height="20" src="https://img.shields.io/badge/github-dev-orange.svg"></a>

- __普通个人号 微信机器人/外挂__ (不同于[webot](https://github.com/node-webot/webot)等公众号机器人)
- 意义: 个人号可充当公众号使用 关系增强/门槛降低/更多行为/依旧自动化
- 与[qqbot](https://github.com/xhan/qqbot)/[wqq](https://github.com/fritx/wqq)等不同: 基于浏览器/用户行为自动化 更贴近用户/更可靠
- 基于浏览器桌面平台[electron](https://github.com/atom/electron) 跨平台win/linux/mac
- 基于微信网页版 <https://wx.qq.com>
- 目前处于高度开发和观察阶段
- 目前代码提供自动回复 可自行定制

请使用较新版本的electron>=v1.0
如果electron=v0.x 可以查看分支[wxbot#electron-v0](https://github.com/fritx/wxbot/tree/electron-v0)

```plain
$ cd wxbot
$ npm install
$ node .  # 运行 需扫二维码登录
```

<img width="643" src="https://raw.githubusercontent.com/fritx/wxbot/dev/screenshot.jpeg">

## 功能实现

- [x] 自动回复
- [x] 识别并回复相同的文本/表情/emoji
- [x] 识别图片/语音/视频/小视频
- [x] 识别位置/名片/链接/附件
- [x] 识别转账/在线聊天/实时对讲
- [x] 发送图片
- [x] 下载自定义表情/名片/图片/语音/附件
- [ ] 下载视频/小视频
- [ ] 接受好友请求并回复
- [ ] 感应系统消息 时间/邀请加群/红包等
- [ ] loop错误超时解锁
- [x] 探索运行于无界面平台 [atom/electron#228](https://github.com/atom/electron/issues/228)

## 无界面linux运行

- 从命令行输出 获取二维码图片url 自行打开/扫描
- 参照配置 [segmentio/nightmare#224 (comment)](https://github.com/segmentio/nightmare/issues/224#issuecomment-141575361)

## Docker 相关

```plain
$ cd wxbot
$ docker build -t wxbot .
$ docker run -d -p 8233:8233 # 浏览器访问 docker 的 8233 端口,即可获取图片
```

## 如何正确地下载electron

参考 <http://blog.fritx.me/?weekly/150904>


================================================
FILE: package.json
================================================
{
  "name": "wxbot",
  "version": "0.1.0",
  "description": "普通个人号 微信机器人/外挂",
  "repository": "git@github.com:fritx/wxbot.git",
  "homepage": "https://github.com/fritx/wxbot",
  "keywords": [
    "weixin",
    "wx",
    "qq",
    "bot"
  ],
  "main": "src",
  "scripts": {
    "start": "node ."
  },
  "dependencies": {
    "electron": "^1.6.2",
    "mime": "^1.3.4"
  }
}


================================================
FILE: src/index.js
================================================
let { spawn } = require('child_process')
let { join } = require('path')
let ipc = require('./ipc')

let electron = require('electron')
let runner = join(__dirname, 'runner.js')
let proc = spawn(electron, ['--js-flags="--harmony"', runner], {
  stdio: [null, null, null, 'ipc']
})

let child = ipc(proc)
child.on('runner', (k, args) => {
  console[k](`runner:${k}`, ...args)
})

process.on('exit', end)
process.on('SIGINT', end)
process.on('SIGTERM', end)
process.on('SIGQUIT', end)
process.on('SIGHUP', end)
process.on('SIGBREAK', end)

function end () {
  if (proc.connected) proc.disconnect()
  proc.kill()
}


================================================
FILE: src/ipc.js
================================================
/* eslint-disable */
// https://github.com/segmentio/nightmare/blob/master/lib%2Fipc.js

/**
 * Module dependencies
 */

var Emitter = require('events').EventEmitter;
// var sliced = require('sliced');

/**
 * Export `ipc`
 */

module.exports = ipc;

/**
 * Initialize `ipc`
 */

function ipc(process) {
  var emitter = new Emitter();
  var emit = emitter.emit;

  // no parent
  if (!process.send) {
    return emitter;
  }

  process.on('message', function(data) {
    // emit.apply(emitter, sliced(data));
    emit.apply(emitter, [...data]);
  });

  emitter.emit = function() {
    if(process.connected){
      // process.send(sliced(arguments));
      process.send(Array.from(arguments));
    }
  }

  return emitter;
}


================================================
FILE: src/parseMsg.js
================================================
function debug(...args){
  var json = JSON.stringify(args)
  console.log(json)
}

module.exports = parseMsg

// msg = {
//   from, room, style,
//   type:
//     not supported|not recognized
//     text|picture|app|card|location|attach
//     sticker|emoticon|transfer
//     voice|video|microvideo|video/voice call
//     real-time location|real-time voice
//     ----
//     red packet|recall
//     new member|member is stranger
//     real-time location ended|real-time voice ended
//   text|title|desc|src|poster...
// }
function parseMsg ($msg) {
  var msg = {}
  var $message = $msg.closest('.message')
  var $nickname = $message.find('.nickname')
  var $titlename = $('.title_name')

  if ($nickname.length) { // 群聊
    var from = $nickname.text()
    var room = $titlename.text()
  } else { // 单聊
    var from = $titlename.text()
    var room = null
  }
  Object.assign(msg, {
    from, room
  })
  debug('来自', from, room) // 这里的nickname会被remark覆盖

  if ($msg.is('.message_system')) {
    var ctn = $msg.find('.content').text()
    debug('接收', '系统标记', ctn)
    Object.assign(msg, {
      style: 'system',
      text: ctn
    })

    var mat
    if (ctn === '收到红包,请在手机上查看' ||
        ctn === 'Red packet received. View on phone.') {
      // text = '发毛红包'
      Object.assign(msg, {
        type: 'red packet'
      })
    } else if (ctn === '位置共享已经结束' ||
        ctn === 'Real-time Location session ended.') {
      // text = '位置共享已经结束'
      Object.assign(msg, {
        type: 'real-time location ended'
      })
    } else if (ctn === '实时对讲已经结束') {
      // text = '实时对讲已经结束'
      Object.assign(msg, {
        type: 'real-time voice ended'
      })
    } else if (mat = ctn.match(/"(.+)"邀请"(.+)"加入了群聊/)) {
      // text = '加毛人'
      Object.assign(msg, {
        type: 'new member',
        by: mat[1],
        who: mat[2]
      })
    } else if (mat = ctn.match(/"(.+)"与群里其他人都不是微信朋友关系,请注意隐私安全/)) {
      // text = '加毛人'
      Object.assign(msg, {
        type: 'member is stranger',
        who: mat[1]
      })
    } else if (mat = ctn.match(/You were removed from the group chat by "(.+)"/)) {
      Object.assign(msg, {
        type: 'removed',
        by: mat[1]
      })
    } else if (mat = ctn.match(/(.+)(撤回了一条消息| withdrew a message)/)) {
      // text = '撤你妹'
      Object.assign(msg, {
        type: 'recall',
        by: mat[1]
      })
    } else {
      // 无视
      Object.assign(msg, {
        type: 'not recognized',
        text: ctn
      })
    }
  } else if ($msg.is('.emoticon')) { // 用户自定义表情
    var src = $msg.find('.msg-img').prop('src')
    debug('接收', 'emoticon', src)
    // reply.text = '发毛表情'
    Object.assign(msg, {
      type: 'emoticon',
      src
    })
  } else if ($msg.is('.picture')) {
    var src = $msg.find('.msg-img').prop('src')
    debug('接收', 'picture', src)
    // reply.text = '发毛图片'
    // reply.image = join(__dirname, '../fuck.jpeg')
    Object.assign(msg, {
      type: 'picture',
      src
    })
  } else if ($msg.is('.location')) {
    var src = $msg.find('.img').prop('src')
    var desc = $msg.find('.desc').text()
    debug('接收', 'location', desc)
    // reply.text = desc
    Object.assign(msg, {
      type: 'location',
      src, desc
    })
  } else if ($msg.is('.attach')) {
    var title = $msg.find('.title').text()
    var size = $msg.find('span:first').text()
    var $download = $msg.find('a[download]') // 可触发下载
    var src = $download.prop('href')
    debug('接收', 'attach', title, size)
    // reply.text = title + '\n' + size
    Object.assign(msg, {
      type: 'attach',
      title, size, src
    })
  } else if ($msg.is('.microvideo')) {
    var poster = $msg.find('img').prop('src') // 限制
    var src = $msg.find('video').prop('src') // 限制
    debug('接收', 'microvideo', poster)
    // reply.text = '发毛小视频'
    Object.assign(msg, {
      type: 'microvideo',
      poster, src
    })
  } else if ($msg.is('.video')) {
    var poster = $msg.find('.msg-img').prop('src') // 限制
    debug('接收', 'video', poster)
    // reply.text = '发毛视频'
    Object.assign(msg, {
      type: 'video',
      poster
    })
  } else if ($msg.is('.voice')) {
    $msg[0].click()
    var duration = parseInt($msg.find('.duration').text())
    var src = $('#jp_audio_1').prop('src') // 认证限制
    debug('接收', 'voice', `${duration}s`, src)
    // reply.text = '发毛语音'
    Object.assign(msg, {
      type: 'voice',
      duration, src
    })
  } else if ($msg.is('.card')) {
    var name = $msg.find('.display_name').text()
    var wxid = $msg.find('.signature').text() // 微信注释掉了
    var img = $msg.find('.img').prop('src') // 认证限制
    debug('接收', 'card', name, wxid)
    // reply.text = name + '\n' + wxid
    Object.assign(msg, {
      type: 'card',
      name, img
    })
  } else if ($msg.is('a.app')) {
    var url = $msg.attr('href')
    url = decodeURIComponent(url.match(/requrl=(.+?)&/)[1])
    var title = $msg.find('.title').text()
    var desc = $msg.find('.desc').text()
    var img = $msg.find('.cover').prop('src') // 认证限制
    debug('接收', 'link', title, desc, url)
    // reply.text = title + '\n' + url
    Object.assign(msg, {
      type: 'app',
      url, title, desc, img
    })
  } else if ($msg.is('.plain')) {
    var text = ''
    var ctn = ''
    var normal = false
    var $text = $msg.find('.js_message_plain')
    $text.contents().each(function(i, node){
      if (node.nodeType === Node.TEXT_NODE) {
        ctn += node.nodeValue
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        var $el = $(node)
        if ($el.is('br')) ctn += '\n'
        else if ($el.is('.qqemoji, .emoji')) {
          ctn += $el.attr('text').replace(/_web$/, '')
        }
      }
    })
    if (ctn === '[收到了一个表情,请在手机上查看]' ||
        ctn === '[Send an emoji, view it on mobile]' ||
        ctn === '[Received a sticker. View on phone]') { // 微信表情包
      // text = '发毛表情'
      Object.assign(msg, {
        type: 'sticker' // 微信内部表情
      })
    } else if (ctn === '[收到一条微信转账消息,请在手机上查看]' ||
        ctn === '[Received a micro-message transfer message, please view on the phone]' ||
        ctn === '[Received transfer. View on phone.]') {
      // text = '转毛帐'
      Object.assign(msg, {
        type: 'transfer'
      })
    } else if (ctn === '[收到一条视频/语音聊天消息,请在手机上查看]' ||
        ctn === '[Receive a video / voice chat message, view it on your phone]' ||
        ctn === '[Received video/voice chat message. View on phone.]') {
      // text = '聊jj'
      Object.assign(msg, {
        type: 'video/voice call'
      })
    } else if (ctn === '我发起了实时对讲') {
      // text = '对讲你妹'
      Object.assign(msg, {
        type: 'real-time voice'
      })
    } else if (ctn === '该类型暂不支持,请在手机上查看' ||
        ctn === '[收到一条网页版微信暂不支持的消息类型,请在手机上查看]') {
      // text = '不懂'
      Object.assign(msg, {
        type: 'not supported'
      })
    } else if (ctn.match(/(.+)发起了位置共享,请在手机上查看/) ||
        ctn.match(/(.+)Initiated location sharing, please check on the phone/) ||
        ctn.match(/(.+)started a real\-time location session\. View on phone/)) {
      // text = '发毛位置共享'
      Object.assign(msg, {
        type: 'real-time location'
      })
    } else {
      normal = true
      // text = ctn
      Object.assign(msg, {
        type: 'text',
        text: ctn
      })
    }
    debug('接收', 'text', ctn)
    // if (normal && !text.match(/叼|屌|diao|丢你|碉堡/i)) text = ''
    // reply.text = text
  } else {
    console.log('未成功解析消息', $msg.html())
    Object.assign(msg, {
      type: 'not recognized'
    })
  }

  return msg
}


================================================
FILE: src/preload.js
================================================
require('./preloadIpc')
let { clipboard, nativeImage } = require('electron')
let { s, sa, delay, download } = require('./util')
let parseMsg = require('./parseMsg')
let replyMsg = require('./replyMsg')

// 禁用微信网页绑定的beforeunload
// 导致页面无法正常刷新和关闭
window.__defineSetter__('onbeforeunload', () => {
  // noop
})

document.addEventListener('DOMContentLoaded', () => {
  // 禁止外层网页滚动 影响使用
  document.body.style.overflow = 'hidden'

  detectPage()
})

async function autoReply () {
  while (true) { // 保持回复消息
    try {
      let msg = await detectMsg()
      console.log('解析得到msg', JSON.stringify(msg))

      let reply = await replyMsg(msg)
      console.log('reply', JSON.stringify(reply))

      if (reply) {
        // continue // test: 不作回复
        pasteMsg(reply)
        await clickSend(reply)
      }
    } catch (err) {
      console.error('自动回复出现err', err)
    }
  }
}

async function detectMsg () {
  // 重置回"文件传输助手" 以能接收未读红点
  s('img[src*=filehelper]').closest('.chat_item').click()

  let reddot
  while (true) {
    await delay(100)
    reddot = s('.web_wechat_reddot, .web_wechat_reddot_middle')
    if (reddot) break
  }

  let item = reddot.closest('.chat_item')
  item.click()

  await delay(100)
  let $msg = $([
    '.message:not(.me) .bubble_cont > div',
    '.message:not(.me) .bubble_cont > a.app',
    '.message:not(.me) .emoticon',
    '.message_system'
  ].join(', ')).last()

  let msg = parseMsg($msg)
  return msg
}

async function clickSend (opt) {
  if (opt.text) {
    s('.btn_send').click()
  } else if (opt.image) {
    // fixme: 超时处理
    while (true) {
      await delay(300)
      let btn = s('.dialog_ft .btn_primary')
      if (btn) {
        btn.click() // 持续点击
      } else {
        return
      }
    }
  }
}

// 借用clipboard 实现输入文字 更新ng-model=EditAreaCtn
function pasteMsg (opt) {
  let oldImage = clipboard.readImage()
  let oldHtml = clipboard.readHtml()
  let oldText = clipboard.readText()

  clipboard.clear() // 必须清空
  if (opt.image) {
    // 不知为啥 linux上 clipboard+nativeimage无效
    try {
      clipboard.writeImage(nativeImage.createFromPath(opt.image))
    } catch (err) {
      opt.image = null
      opt.text = '妈蛋 发不出图片'
    }
  }
  if (opt.html) clipboard.writeHtml(opt.html)
  if (opt.text) clipboard.writeText(opt.text)
  s('#editArea').focus()
  document.execCommand('paste')

  clipboard.writeImage(oldImage)
  clipboard.writeHtml(oldHtml)
  clipboard.writeText(oldText)
}

function detectPage () {
  let ps = [
    detectCache(), // 协助跳转
    detectLogin(),
    detectChat()
  ]

  // 同时判断login和chat 判断完成则同时释放
  Promise.race(ps)
    .then(data => {
      ps.forEach(p => p.cancel())

      let { page, qrcode } = data
      console.log(`目前处于${page}页面`)

      if (page === 'login') {
        download(qrcode)
      } else if (page === 'chat') {
        autoReply()
      }
    })
}

// 需要定制promise 提供cancel方法
function detectChat () {
  let toCancel = false

  let p = (async () => {
    while (true) {
      if (toCancel) return
      await delay(300)

      let item = s('.chat_item')
      if (item) {
        return { page: 'chat' }
      }
    }
  })()

  p.cancel = () => {
    toCancel = true
  }
  return p
}

// 需要定制promise 提供cancel方法
function detectLogin () {
  let toCancel = false

  let p = (async () => {
    while (true) {
      if (toCancel) return
      await delay(300)

      // 共有两次load事件 仅处理后一次
      // 第1次src https://res.wx.qq.com/a/wx_fed/webwx/res/static/img/2z6meE1.gif
      // 第2次src https://login.weixin.qq.com/qrcode/IbAG40QD6A==
      let img = s('.qrcode img')
      if (img && img.src.endsWith('==')) {
        return {
          page: 'login',
          qrcode: img.src
        }
      }
    }
  })()

  p.cancel = () => {
    toCancel = true
  }
  return p
}

// 需要定制promise 提供cancel方法
// 可能跳到缓存了退出登陆用户头像的界面,手动点一下切换用户,以触发二维码下载
function detectCache () {
  let toCancel = false

  let p = (async () => {
    while (true) {
      if (toCancel) return
      await delay(300)

      let btn = s('.association .button_default')
      if (btn) btn.click() // 持续点击
    }
  })()

  p.cancel = () => {
    toCancel = true
  }
  return p
}


================================================
FILE: src/preloadIpc.js
================================================
let { ipcRenderer } = require('electron')

;['log', 'info', 'warn', 'error'].forEach(k => {
  let fn = console[k].bind(console)
  console[k] = (...args) => {
    fn(...args)
    ipcRenderer.send('renderer', k, args)
  }
})


================================================
FILE: src/replyMsg.js
================================================
let { download } = require('./util')
let { join } = require('path')

module.exports = replyMsg

let handlers = {
  'text' ({ text }) {
    return { text }
  },

  'picture' ({ src }) {
    saveMedia(src)

    let largeSrc = src.replace('&type=slave', '')
    saveMedia(largeSrc, 'large') // 大图
    return {
      image: join(__dirname, '../fuck.jpeg')
    }
  },

  'emoticon' () { // 用户自定义表情
    return { text: '发毛表情' }
  },

  'sticker' () { // 微信内部表情
    return { text: '发毛表情' }
  },

  'voice' ({ src }) {
    saveMedia(src)
    return { text: '发毛语音' }
  },

  'video' () {
    return { text: '发毛视频' }
  },

  'microvideo' () {
    return { text: '发毛小视频' }
  },

  'location' ({ src, desc }) {
    saveMedia(src)
    return { text: desc }
  },

  'attach' ({ src, title, size }) {
    saveMedia(src)
    return {
      text: title + '\n' + size
    }
  },

  'app' ({ title, url }) {
    return {
      text: title + '\n' + url
    }
  },

  'card' ({ name, wxid }) {
    return { text: name }
  },

  'transfer' () {
    return { text: '转毛帐' }
  },

  'video/voice call' () {
    return { text: '聊jj' }
  },

  // 似乎功能已经不支持
  // 'real-time voice' () {
  //   return { text: '对讲你妹' }
  // },

  /* 以下为无法自动感应病回应的消息 */
  'red packet' () {
    return { text: '发毛红包' }
  },

  'recall' ({ by }) {
    // return { text: `${by} 撤回了消息` }
    return { text: '撤jj' }
  },

  'new member' ({ who, by }) {
    // return { text: `${by} 邀请了 ${who}` }
    return { text: '加毛人' }
  },

  'member is stranger' ({ who }) {
    return { text: `大家要小心 ${who}` }
  },

  'real-time location ended' () {
    return { text: '位置共享已经结束' }
  },

  // 似乎功能已经不支持
  // 'real-time voice ended' () {
  //   return { text: '实时对讲已经结束' }
  // },

  'removed' () {
    return null
  },

  'not recognized' () {
    return null // 忽略消息 不回应
  },
  /* 以上为无法自动感应病回应的消息 */

  'not supported' () {
    return { text: '不懂' }
  }
}

// fixme: attach文件附带content-deposition 覆盖download属性设置的filename
// https://stackoverflow.com/questions/23872902/chrome-download-attribute-not-working
function saveMedia (src, suffix) {
  let mat = src.match(/msgid=(\d+)/i)
  let msgid = mat && mat[1]
  let filename = msgid || ''
  if (suffix) filename += '_' + suffix
  download(src, filename)
}

async function replyMsg (msg) {
  let handler = handlers[msg.type]
    || handlers['not recognized']

  let reply = await toPromise(
    handler(msg)
  )
  return reply
}

function toPromise (ret) {
  if (ret && ret.then) return ret
  return Promise.resolve(ret)
}


================================================
FILE: src/runner.js
================================================
require('./runnerIpc')
let { app, session, ipcMain, BrowserWindow } = require('electron')
let { tmpdir } = require('os')
let { join } = require('path')
// let open = require('open')
let mime = require('mime')

let downloadDir = join(__dirname, '../download')
try {
  fs.mkdirSync(downloadDir)
} catch (err) {
  // ignore
}

let win

// 将renderer的输出 转发到terminal
ipcMain.on('renderer', (e, k, args) => {
  console[k]('renderer', k, args)
})

app.on('activate', () => {
  if (win) win.show()
})

app.on('ready', () => {
  let show = true // 是否显示浏览器窗口
  let preload = join(__dirname, 'preload.js')

  win = new BrowserWindow({
    webPreferences: {
      preload,
      nodeIntegration: false
    },
    width: 900,
    height: 610,
    show
  })

  // Ctrl+C只会发送win.close 并且如果已登录  窗口还关不掉
  // 所以干脆改为窗口关闭 直接退出
  // https://github.com/electron/electron/issues/5273
  win.on('close', e => {
    e.preventDefault()
    win.destroy()
  })

  win.once('ready-to-show', () => {
    win.show()
  })
  win.loadURL('https://wx.qq.com')

  let sess = session.defaultSession
  sess.on('will-download', async (e, item) => {
    let url = item.getURL()

    if (/\/qrcode\/.+==/.test(url)) { // 登录二维码
      let dest = join(tmpdir(), `qrcode_${Date.now()}.jpg`)
      let state = await saveItem(item, dest, '二维码保存')

      // todo: 如果是运行在无界面环境 则需要将二维码通过url展示出来
      // 如果不显示浏览器窗口 则调用程序单独打开二维码
      // if (!show && state === 'completed') {
      //   open(dest, err => {
      //     if (err) {
      //       console.error('二维码打开 err:', err)
      //     }
      //   })
      // }
    }
    else { // 下载消息中的多媒体文件 图片/语音
      let mimeType = item.getMimeType()
      let filename = item.getFilename()
      let ext = mime.extension(mimeType)

      // 修复mime缺少映射关系: `audio/mp3` => `mp3`
      if (mimeType === 'audio/mp3') ext = 'mp3'
      if (ext === 'bin') ext = ''
      if (ext) filename += '.' + ext
      
      let date = new Date().toJSON()
      filename = date + '_' + filename

      // 跨平台文件名容错
      // http://blog.fritx.me/?weekly/160227
      filename = filename.replace(/[\\\/:\*\,"\?<>|]/g, '_')

      let dest = join(downloadDir, filename)
      await saveItem(item, dest, `文件保存 ${filename}`)
    }
  })
})

async function saveItem (item, dest, log) {
  item.setSavePath(dest)
  return await new Promise(rs => {
    item.on('done', (e, state) => {
      console.log(`${log} state:${state}`)
      rs(state)
    })
  })
}


================================================
FILE: src/runnerIpc.js
================================================
let parent = require('./ipc')(process)

;['log', 'info', 'warn', 'error'].forEach(k => {
  let fn = console[k].bind(console)
  console[k] = (...args) => {
    fn(...args)
    parent.emit('runner', k, args)
  }
})


================================================
FILE: src/util.js
================================================
exports.s = s
exports.sa = sa
exports.download = download
exports.delay = delay

async function delay (duration) {
  return new Promise(rs => {
    setTimeout(() => rs(), duration)
  })
}

function download (href, filename = '') {
  console.log('触发下载', filename, href)
  let a = document.createElement('a')
  a.download = filename
  a.href = href
  a.click()
}

function s (selector) {
  return document.querySelector(selector)
}
function sa (selector) {
  return document.querySelectorAll(selector)
}
Download .txt
gitextract_1_5is914/

├── .gitignore
├── Dockerfile
├── README.md
├── package.json
└── src/
    ├── index.js
    ├── ipc.js
    ├── parseMsg.js
    ├── preload.js
    ├── preloadIpc.js
    ├── replyMsg.js
    ├── runner.js
    ├── runnerIpc.js
    └── util.js
Download .txt
SYMBOL INDEX (41 symbols across 7 files)

FILE: src/index.js
  function end (line 23) | function end () {

FILE: src/ipc.js
  function ipc (line 21) | function ipc(process) {

FILE: src/parseMsg.js
  function debug (line 1) | function debug(...args){
  function parseMsg (line 22) | function parseMsg ($msg) {

FILE: src/preload.js
  function autoReply (line 20) | async function autoReply () {
  function detectMsg (line 40) | async function detectMsg () {
  function clickSend (line 66) | async function clickSend (opt) {
  function pasteMsg (line 84) | function pasteMsg (opt) {
  function detectPage (line 109) | function detectPage () {
  function detectChat (line 133) | function detectChat () {
  function detectLogin (line 155) | function detectLogin () {
  function detectCache (line 184) | function detectCache () {

FILE: src/replyMsg.js
  method 'text' (line 7) | 'text' ({ text }) {
  method 'picture' (line 11) | 'picture' ({ src }) {
  method 'emoticon' (line 21) | 'emoticon' () { // 用户自定义表情
  method 'sticker' (line 25) | 'sticker' () { // 微信内部表情
  method 'voice' (line 29) | 'voice' ({ src }) {
  method 'video' (line 34) | 'video' () {
  method 'microvideo' (line 38) | 'microvideo' () {
  method 'location' (line 42) | 'location' ({ src, desc }) {
  method 'attach' (line 47) | 'attach' ({ src, title, size }) {
  method 'app' (line 54) | 'app' ({ title, url }) {
  method 'card' (line 60) | 'card' ({ name, wxid }) {
  method 'transfer' (line 64) | 'transfer' () {
  method 'video/voice call' (line 68) | 'video/voice call' () {
  method 'red packet' (line 78) | 'red packet' () {
  method 'recall' (line 82) | 'recall' ({ by }) {
  method 'new member' (line 87) | 'new member' ({ who, by }) {
  method 'member is stranger' (line 92) | 'member is stranger' ({ who }) {
  method 'real-time location ended' (line 96) | 'real-time location ended' () {
  method 'removed' (line 105) | 'removed' () {
  method 'not recognized' (line 109) | 'not recognized' () {
  method 'not supported' (line 114) | 'not supported' () {
  function saveMedia (line 121) | function saveMedia (src, suffix) {
  function replyMsg (line 129) | async function replyMsg (msg) {
  function toPromise (line 139) | function toPromise (ret) {

FILE: src/runner.js
  function saveItem (line 94) | async function saveItem (item, dest, log) {

FILE: src/util.js
  function delay (line 6) | async function delay (duration) {
  function download (line 12) | function download (href, filename = '') {
  function s (line 20) | function s (selector) {
  function sa (line 23) | function sa (selector) {
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (26K chars).
[
  {
    "path": ".gitignore",
    "chars": 34,
    "preview": "node_modules\ndownload\n*.out\n*.log\n"
  },
  {
    "path": "Dockerfile",
    "chars": 1016,
    "preview": "FROM ubuntu:latest\n\nMAINTAINER huangruichang \"532079207@qq.com\"\n\nRUN apt-get upgrade && apt-get update -y\n\nEXPOSE 8233\n\n"
  },
  {
    "path": "README.md",
    "chars": 1587,
    "preview": "# wxbot 微信机器人\n\n<a href=\"https://github.com/fritx/awesome-wechat\"><img width=\"110\" height=\"20\" src=\"https://img.shields.i"
  },
  {
    "path": "package.json",
    "chars": 373,
    "preview": "{\n  \"name\": \"wxbot\",\n  \"version\": \"0.1.0\",\n  \"description\": \"普通个人号 微信机器人/外挂\",\n  \"repository\": \"git@github.com:fritx/wxbo"
  },
  {
    "path": "src/index.js",
    "chars": 611,
    "preview": "let { spawn } = require('child_process')\nlet { join } = require('path')\nlet ipc = require('./ipc')\n\nlet electron = requi"
  },
  {
    "path": "src/ipc.js",
    "chars": 725,
    "preview": "/* eslint-disable */\n// https://github.com/segmentio/nightmare/blob/master/lib%2Fipc.js\n\n/**\n * Module dependencies\n */\n"
  },
  {
    "path": "src/parseMsg.js",
    "chars": 7496,
    "preview": "function debug(...args){\n  var json = JSON.stringify(args)\n  console.log(json)\n}\n\nmodule.exports = parseMsg\n\n// msg = {\n"
  },
  {
    "path": "src/preload.js",
    "chars": 4112,
    "preview": "require('./preloadIpc')\nlet { clipboard, nativeImage } = require('electron')\nlet { s, sa, delay, download } = require('."
  },
  {
    "path": "src/preloadIpc.js",
    "chars": 223,
    "preview": "let { ipcRenderer } = require('electron')\n\n;['log', 'info', 'warn', 'error'].forEach(k => {\n  let fn = console[k].bind(c"
  },
  {
    "path": "src/replyMsg.js",
    "chars": 2506,
    "preview": "let { download } = require('./util')\nlet { join } = require('path')\n\nmodule.exports = replyMsg\n\nlet handlers = {\n  'text"
  },
  {
    "path": "src/runner.js",
    "chars": 2424,
    "preview": "require('./runnerIpc')\nlet { app, session, ipcMain, BrowserWindow } = require('electron')\nlet { tmpdir } = require('os')"
  },
  {
    "path": "src/runnerIpc.js",
    "chars": 213,
    "preview": "let parent = require('./ipc')(process)\n\n;['log', 'info', 'warn', 'error'].forEach(k => {\n  let fn = console[k].bind(cons"
  },
  {
    "path": "src/util.js",
    "chars": 502,
    "preview": "exports.s = s\nexports.sa = sa\nexports.download = download\nexports.delay = delay\n\nasync function delay (duration) {\n  ret"
  }
]

About this extraction

This page contains the full source code of the fritx/wxbot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (21.3 KB), approximately 7.2k tokens, and a symbol index with 41 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!