wudi пре 6 месеци
комит
ee0a0e35e9
8 измењених фајлова са 396 додато и 0 уклоњено
  1. 11 0
      background/index.js
  2. 211 0
      background/utils.js
  3. 20 0
      common/helper.js
  4. 3 0
      content/content-before.js
  5. 11 0
      content/index.js
  6. 101 0
      content/utils.js
  7. BIN
      images/icon.png
  8. 39 0
      manifest.json

+ 11 - 0
background/index.js

@@ -0,0 +1,11 @@
+// 监听消息
+listenContentMessage()
+
+// 拦截请求
+beforeWxPriceRequest()
+
+// 拦截计划 该方法无法判断出价方式为 ocpa 还是 ocpx, 因此控制在input哪里实现
+// beforeWxPlansRequest()
+
+// 拦截出价列表并再次请求并设置广告出价类型映射
+beforeWxRequestPriceList()

+ 211 - 0
background/utils.js

@@ -0,0 +1,211 @@
+~(function (exports) {
+  // 消息策略
+  const tactics = {
+    // 请求价格限制配置
+    async request(requestOptions, sender, sendResponse, callback) {
+      const res = await exports.req(requestOptions).catch(err => console.log(err))
+      Object.assign(state.wx, res['weixin.qq.com'])
+      Object.assign(state.tencent, res['e.qq.com'])
+      sendResponse(state)
+      callback && callback(state)
+    }
+  }
+
+  const simpleRequest = new Set(['get', 'head'])
+
+  // xhr请求
+  exports.req = function (options = {}) {
+    const { url = "", method = "get", data = {}, headers = {}, responseType = '', withCredentials = false, timeout = 18000 } = options
+
+    const xhr = new XMLHttpRequest()
+
+    const defaultHeaders = {
+      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
+    }
+
+    const finalHeaders = { ...defaultHeaders, ...headers }
+
+    const finalData = (Object.prototype.toString.call(data).slice(8, -1) === 'Object' && String(finalHeaders['Content-Type']).includes('x-www-form-urlencoded')) ?
+      Object.keys(data).map(key => `${key}=${encodeURIComponent(data[key])}`).join("&") :
+      data
+
+    const finalUrl = simpleRequest.has(method.toLowerCase()) ? `${url}?${finalData}` : url
+
+    xhr.responseType = responseType
+    xhr.withCredentials = withCredentials
+    xhr.timeout = timeout
+
+    return new Promise((resolve, reject) => {
+      xhr.addEventListener("readystatechange", function () {
+        if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
+          try {
+            resolve(JSON.parse(xhr.responseText))
+          } catch {
+            resolve(xhr.responseText)
+          } finally {
+            resolve(xhr.response)
+          }
+        }
+      })
+
+      xhr.addEventListener("error", function () {
+        reject(xhr.response)
+      })
+
+      xhr.addEventListener('timeout', function () {
+        reject('timeout')
+      })
+
+      xhr.open(method, finalUrl)
+
+      Object.keys(finalHeaders).forEach(key => {
+        xhr.setRequestHeader(key, headers[key])
+      })
+
+      xhr.send(simpleRequest.has(method.toLowerCase()) ? null : finalData)
+    })
+  }
+
+  // 状态
+  exports.state = {
+    // 微信公众平台价格限制
+    wx: {
+      ocpa: 0,
+      ocpx: 0
+    },
+    // 腾讯广告价格限制
+    tencent: {
+      ocpa: 0,
+      ocpx: 0
+    },
+
+    // 广告id -> 出价类型 映射 
+    adMap: new Map()
+  }
+
+  // 监听content-scripts 消息
+  exports.listenContentMessage = function () {
+    return new Promise((resolve, reject) => {
+      chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
+        const handler = tactics[message.type]
+        handler && handler(message.payload, sender, sendResponse, function (res) {
+          resolve(res)
+        })
+        return true
+      })
+    })
+  }
+
+  // 拦截出价
+  exports.beforeWxPriceRequest = function () {
+    chrome.webRequest.onBeforeRequest.addListener(
+      details => {
+        const { args } = details.requestBody.formData || {}
+
+        if (!args) return
+
+        const jsonArgs = JSON.parse(args)
+
+        const priceType = exports.state.adMap.get((jsonArgs.oper_aids[0] || {}).aid)
+
+        const max = +state.wx[priceType]
+        let invalid = jsonArgs.oper_aids.some(item => item.bid / 100 > max)
+
+        if (invalid) {
+          return {
+            cancel: true
+          }
+        }
+      },
+      { urls: ["https://mp.weixin.qq.com/promotion/v3/batch_update_adgroup"] },
+      ["requestBody", "blocking"]
+    )
+  }
+
+  // 拦截计划
+  exports.beforeWxPlansRequest = function (priceType = 'ocpa') {
+    chrome.webRequest.onBeforeRequest.addListener(
+      details => {
+        const { formData } = details.requestBody
+        const { args } = formData || {}
+
+        const max = +state.wx[priceType]
+
+        let invalid = !args || JSON.parse(args).target_groups.some(target => target.ad_groups.some(ad => ad.ad_group.bid / 100 > max))
+
+        if (invalid) {
+          return {
+            cancel: true
+          }
+        }
+      },
+      { urls: [
+          "https://mp.weixin.qq.com/promotion/v3/update_campaign_info", 
+          'https://mp.weixin.qq.com/promotion/v3/create_campaign_info'
+        ] 
+      },
+      ["requestBody", "blocking"]
+    )
+  }
+
+  // 获取cookie
+  function getCookie(callback) {
+    chrome.tabs.query({
+      active: true
+    }, (tabs) => {
+      const tabId = tabs[0] && tabs[0].id
+      chrome.tabs.sendMessage(tabId, { type: 'getCookie' }, res => {
+        callback(res)
+      })
+    })
+  }
+
+  // 拦截出价列表
+  exports.beforeWxRequestPriceList = function () {
+    const filters = {
+      urls: ["https://mp.weixin.qq.com/promotion/as_rock*"]
+    }
+
+    const typeMap = {
+      '按下单出价': 'ocpx',
+      '按关注出价': 'ocpa',
+      '按曝光出价': 'ocpa',
+      '按点击出价': 'ocpa'
+    }
+
+    const callback = function (details) {
+      const query = helper.queryToJson(details.url)
+      if (query.action === 'get_adgroup_data' && query.custom !== '1') {
+        let headers = details.requestHeaders.reduce((pre, cur) => {
+          pre[cur.name] = cur.value
+          return pre
+        }, {})
+
+        getCookie(async (Cookie) => {
+          const res = await exports.req({
+            method: 'get',
+            url: `${details.url}&custom=1`,
+            headers: {
+              ...headers,
+              Cookie,
+              'Accept-Encoding': 'gzip, deflate, br',
+              'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
+              Connection: 'keep-alive',
+              Host: 'mp.weixin.qq.com',
+              Referer: 'https://mp.weixin.qq.com/promotion/readtemplate?t=campaign/manage&lang=zh_CN&token=1483508456&type=1'
+            }
+          })
+          if (res.msg === 'ok') {
+            let map = new Map()
+            res.list && res.list.forEach(item => {
+              map.set(item.adgroup_info.aid, typeMap[item.adgroup_info.bid_action_type] || 'ocpa')
+            })
+            exports.state.adMap = map
+          }
+        })
+      }
+    }
+
+    chrome.webRequest.onBeforeSendHeaders.addListener(callback, filters, ['requestHeaders'])
+  }
+})(this || window)

+ 20 - 0
common/helper.js

@@ -0,0 +1,20 @@
+
+!function (exports) {
+  const helper = {}
+
+  exports.helper = helper
+
+  helper.queryToJson = function (url) {
+    if (!url) return {}
+    let query = url.replace(/.+\?/, '')
+    let ret = {}
+    let match = query.match(/[^&]*=[^&]*/g)
+    let reg = /(.+)=(.*)/
+    match && match.forEach(item => {
+      if (reg.test(item) ) {
+        ret[RegExp.$1] = decodeURIComponent(RegExp.$2)
+      }
+    })
+    return ret
+  }
+}(this || window)

+ 3 - 0
content/content-before.js

@@ -0,0 +1,3 @@
+// 应对微信后台无法调试的问题, 在dom插入之前别名
+const log = console.log
+const dir = console.dir

+ 11 - 0
content/index.js

@@ -0,0 +1,11 @@
+// 监听background 过来的信息
+listenBackground()
+
+// 请求出价限制
+requestLimits()
+
+// 页面初始化
+window.addEventListener('load', pagechange)
+
+// 页面跳转时
+window.addEventListener('hashchange', pagechange)

+ 101 - 0
content/utils.js

@@ -0,0 +1,101 @@
+// 从服务端请求到的数据
+let state = null
+
+// opca / ocpx 下单为ocpx 其它为ocpa
+let priceType = 'ocpa'
+
+~function(exports) {
+  let priceElement = null // 出价input
+  let priceTypeButtons = [] // 出价切换button
+  let optimizeRadioGroup = null // 优化目标组
+
+  function onPriceElementInput(e) {
+    if (!state) return
+    const max = +state.wx[priceType]
+    if (+e.target.value > max) {
+      e.target.value = max
+    }
+  }
+
+  // 优化目标点击, 获取出价类型, 在选完出价类型(CPM或智能优化)后才能触发
+  function onOptimizeRadioGroupClick(e) {
+    setTimeout(() => {
+      priceElement && priceElement.removeEventListener('input', onPriceElementInput)
+      priceElement = document.getElementById('price_input')
+      priceElement.addEventListener('input', onPriceElementInput)
+      priceType = priceElement.parentElement.nextSibling.textContent.includes('下单') ? 'ocpx' : 'ocpa'
+    })
+  }
+
+  // 出价方式类型点击
+  function onPriceTypeButtonClick(e) {
+    // 如果选中的是智能优化投放方式则获取优化目标 radio元素
+    optimizeRadioGroup && optimizeRadioGroup.removeEventListener('click', onOptimizeRadioGroupClick)
+    setTimeout(() => {
+      let act = document.querySelector('.adui-button-active')
+
+      if (act.textContent !== 'CPM') {
+        // 智能优化
+        optimizeRadioGroup = document.querySelectorAll('.adui-radio-group')[1]
+        optimizeRadioGroup && optimizeRadioGroup.addEventListener('click', onOptimizeRadioGroupClick)
+      } else {
+        // CPM
+        optimizeRadioGroup = null
+      }
+    })
+  }
+
+  exports.requestLimits = function() {
+    !state && chrome.runtime.sendMessage(
+      {
+        type: "request",
+        payload: {
+          url: "http://baiyun.kwnpv.com/admin/advertise/throw_price_config/getAllPrice",
+          method: "get"
+        }
+      },
+      res => state = res
+    )
+  }
+  
+  exports.pagechange = function() {
+    if (priceElement) {
+      priceElement.removeEventListener('input', onPriceElementInput)
+    }
+    if (priceTypeButtons.length) {
+      priceTypeButtons.forEach(button => button.removeEventListener('click', onPriceTypeButtonClick))
+    }
+    setTimeout(() => {
+      // 出价输入框
+      priceElement = document.getElementById('price_input')
+      // 所有出价类型按钮
+      priceTypeButtons = document.querySelectorAll('.adui-button-group button')
+
+      // 该输入框只是刚进页面时的输入框, 在切换方式的时候应该重新获取
+      if (priceElement) {
+        priceType= priceElement.parentElement.nextSibling.textContent.includes('下单') ? 'ocpx' : 'ocpa'
+        priceElement.addEventListener('input', onPriceElementInput)
+      }
+      if (priceTypeButtons.length) {
+        onPriceTypeButtonClick()
+        priceTypeButtons.forEach(button => {
+          button.addEventListener('click', onPriceTypeButtonClick)
+        })
+      }
+    }, 500)
+  }
+
+  const listenBackgroundTactics = {
+    getCookie(payload, sendRes) {
+      sendRes(document.cookie)
+    }
+  }
+
+  exports.listenBackground = function() {
+    chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
+      const handler = listenBackgroundTactics[message.type]
+      handler && handler(message.payload, sendResponse, sender)
+      return true
+    }) 
+  }
+}(window)


+ 39 - 0
manifest.json

@@ -0,0 +1,39 @@
+{
+  "name": "投放出价监控插件",
+  "description": "投放出价监控插件",
+  "version": "1.0",
+  "manifest_version": 2,
+  "icons": {
+    "16": "images/icon.png",
+    "48": "images/icon.png",
+    "128": "images/icon.png"
+  },
+  "background": {
+    "scripts": ["common/helper.js", "background/utils.js", "background/index.js"]
+  },
+  "page_action": {
+    "default_icon": "images/icon.png",
+    "default_title": "投放出价监控插件"
+  },
+  "content_scripts": [
+    {
+      "matches": ["*://mp.weixin.qq.com/*", "*://e.qq.com/*"],
+      "js": ["content/content-before.js"],
+      "run_at": "document_start"
+    },
+    {
+      "matches": ["*://mp.weixin.qq.com/*", "*://e.qq.com/*"],
+      "js": ["content/utils.js", "content/index.js"],
+      "run_at": "document_end"
+    }
+  ],
+  "permissions": [
+    "tabs",
+    "activeTab",
+    "storage",
+    "http://*/",
+    "https://*/",
+    "webRequest",
+    "webRequestBlocking"
+  ]
+}