Chrome外掛開發入門:如何實現一鍵上班賴皮

wanthering發表於2019-03-03

很多人介紹過Chrome外掛,但必須要說,外掛開發就是擺弄一個小玩具,第一要素是實用,其次是好玩。 單純羅列各種功能是非常無趣的。 所以把一篇舊文拿出來與大家分享。

人,活著就是為了賴皮。

作為一個合格的開發人員,把30%的時間用來賴皮(上班偷懶)是值得推薦的。

因為,如果你工作時間無法賴皮,並不能說明你工作認真,只能說明你的工作自動化程度不夠。

賴皮狗,一般會在上班時間瀏覽:SGamer論壇、虎撲論壇、鬥魚、BiliBili這一類的網站。

但在瀏覽過程中會遇到以下痛點:

  • 老闆查崗,貼子或直播間開啟太多,不能及時關閉全部的賴皮站點。
  • 老闆走了重新賴皮,不記得之前開啟的貼子或直播間在哪裡。
  • 每次在瀏覽器裡輸入賴皮網址,打字真的很麻煩!
  • 工作時開啟了太多標籤頁,休息時很難找到想要的賴皮頁面。

所以,我們需要:

簡單的一鍵賴皮外掛功能:

  1. 開啟瀏覽器後,一個快捷鍵,立即開啟賴皮頁面,喜滋滋開始賴皮的一天。
  2. 老闆/leader查崗時,一個快捷鍵,立即關閉所有賴皮頁面。
  3. 老闆走後,或工作一段時間後,一個快捷鍵,立即開啟原來的賴皮貼子和直播間。

簡單的一鍵賴皮外掛功能:

  1. 包含簡單的一鍵賴皮站點功能
  2. 能自定義配置賴皮網站。
  3. 上傳Google,釋出外掛。

從零開始,開發簡單的一鍵賴皮外掛

90%的上班族都在使用Chrome瀏覽器賴皮,所以我們選擇採用Chrome外掛來實現功能。

Chrome外掛沒什麼大不了的,依然還是採用HTMLCSSJS的組合。

在這裡,我將手把手帶你從零開始製作外掛。

mainfest.json

就像node.js的package.json一樣,每一個外掛必須有一個manifest.json,作為最初配置檔案。

我們建立一個新的專案,在根目錄下建立manifest.json,填入以下程式碼

==mainfest.json==

 {
  "name": "上班一鍵賴皮工具",
  "version": "0.1",
  "description": "windows:按Alt+S開啟、關閉賴皮網站
mac:按Control+S開啟、關閉賴皮網站",
  "manifest_version": 2
 }
複製程式碼

解釋一下:

  • name: 外掛的名字
  • version: 外掛的版本
  • description: 外掛簡介欄
  • manifest_version: 這個是寫死的,每個檔案必須有

接下來請右鍵儲存虎撲logo到根目錄,名字還是如apple-touch-icon.png就行吧。

image

也可以點選
user-gold-cdn.xitu.io/2018/12/15/… 儲存圖片

修改mainfest.json,設定四個尺寸的icon都變成apple-touch-icon.png,以及外掛欄也顯示apple-touch-icon.png。

==mainfest.json==

{
  "name": "上班一鍵賴皮工具",
  "version": "0.1",
  "description": "windows:按Alt+S開啟、關閉賴皮網站  
mac:按Control+S開啟、關閉賴皮網站",
  "icons": {
    "16": "apple-touch-icon.png",
    "32": "apple-touch-icon.png",
    "48": "apple-touch-icon.png",
    "128": "apple-touch-icon.png"
  },
  "browser_action": {
    "default_icon": "apple-touch-icon.png",
    "default_popup": "popup.html"
  },
  "commands": {
    "toggle-tags": {
      "suggested_key": {
        "default": "Alt+S",
        "mac": "MacCtrl+S"
      },
      "description": "Toggle Tags"
    }
  },
  "manifest_version": 2
}
複製程式碼

解釋一下:

  • icons: 配置了顯示在不同地方的圖示
  • browser_action: 即右上角外掛,browser_action > default_icon即右上角外掛圖示
  • commands:一般用於快捷鍵命令。 commands > toggle-tags > suggested_key之下,設定了快捷鍵,只要按下快捷鍵,即會向Chrome就會向後臺釋出一個command,值為toggle-tags

在windows環境下,我們將快捷鍵設定成Alt+S,在mac環境下,我們將快捷鍵設定成Control+S,配置檔案中寫作MacCtrl+S

現在我們有了命令,就需要後臺指令碼接收,在mainfest.json中新增後臺指令碼:
==mainfest.json==

...
  "background": {
    "scripts": [
      "background.js"
    ]
  }
...
複製程式碼

並在根目錄下建立background.js.
==background.js==

chrome.commands.onCommand.addListener(function(command) {
    alert(command)
    console.log(command)
})
複製程式碼

現在我們的目錄結構如下:

├── manifest.json
└── background.js
└── sgamers.png
複製程式碼

在chrome內載入外掛

點選Chorme右上角的三個點按鈕...> More Tools > Extensions

image

在右上角把Developer mode開啟

image

再找到頂部的LOAD UNPACKED,把專案的根目錄匯入進去

image

專案匯入後會出現一個新的卡片,是這個效果:

image

這時,你如果在Windows中按下Alt+S就會彈出訊息,訊息為toggle-tags,正好是我們在mainfest.json中定義好的。

同時我們可以點選上圖藍色鍵頭所指示的background page,開啟一個除錯工具,可以看到toggle-tags的輸出。

我們在之後本地編輯外掛後,可以按灰色鍵頭所指的重新整理,新功能就能立即重新整理載入了!

有了這些工作,意味著你可以進入下一步了!

標籤頁配置

一鍵開啟/關閉賴皮網站,實現原理其實就是chrome的標籤頁功能。

標籤頁功能訪問需要在manifest.json中新增許可權
==mainfest.json==

  ...
  "permissions": ["tabs"]
  ...
複製程式碼

接下來,我們寫一下background.js實現通過快捷鍵(windows的Alt+S,或mac的Ctrl+S)建立新的主頁:
==background.js==

// 輸入你想要的網站主頁
const MainPageUrl = `http://https://bbs.hupu.com/all-gambia`

chrome.commands.onCommand.addListener(function (command) {
  if (command === `toggle-tags`) {
    chrome.tabs.create({"url": MainPageUrl, "selected": true});
  }
})
複製程式碼

其實實現很簡單,就是呼叫chrome.tabs.create介面,就建立了一個新的標籤頁。
重新整理一下外掛,再試一試快捷鍵功能————是不是已經能控制瀏覽器彈出標籤 頁了!

image

實現具體邏輯:

稍顯複雜的地方是標籤頁isOpen狀態的處理,
下圖主要關注isOpen狀態的變化,以及tabCache的值變化。

image

具體後臺邏輯如下,可以跟據備註、對照流程圖進行理解:

//初始化isOpen和tabCache狀態
let isOpen = false
let tabCache = []

//新標籤開啟的主頁
const mainPageUrl = `https://bbs.hupu.com/all-gambia`
//四個賴皮網站的正則匹配表示式
const myPattern = `sgamer.com/|douyu.com|hupu.com|bilibili.com`
//當前頁面的Url
let currentPageUrl = ``

/**
 * 開始步驟: 判斷isOpen狀態
 * 情形一:isOpen為true,則移除頁面
 * 情形二:isOpen為false,則過載頁面
 */
chrome.commands.onCommand.addListener(function (command) {
  if (command === `toggle-tags`) {
    if (isOpen) {
      //情形一:isOpen為true
      removePages(myPattern)
      //情形二:isOpen為false
    } else {
      reloadPages(myPattern, mainPageUrl)
    }
  }
})


/**
 * 情形1:移除頁面
 * 1、清空tabCache快取
 * 2、關閉所有域名內標籤
 * 3、將關閉的標籤存入tabCache快取陣列
 * 4、將isOpen狀態改為false
 */
function removePages(patternStr) {
  tabCache = []
  chrome.tabs.query({active: true}, function (tab) {
    currentPageUrl = tab[0].url
  })
  let pattern = new RegExp(patternStr)
  walkEveryTab(function (tab) {
    if (pattern.test(tab.url)) {
      chrome.tabs.remove(tab.id,function(){
        tabCache.push(tab.url)
      })
    }
  },function(){
    isOpen = false
  })
}

/**
 * 情形2:過載頁面
 * 判斷有沒有快取:
 *    情形2-1無快取:開啟新標籤或定位到域名內的標籤
 *    情形2-2有快取:開啟全部快取內的頁面
 */
function reloadPages(patternStr, mainPageUrl) {
  if (tabCache.length === 0) {
    focusOrCreateTab(patternStr, mainPageUrl)
  } else {
    openAllCachedTab(tabCache)
  }
}

/**
 * 情形2-1:開啟新標籤或定位到域名內的標籤
 * 1、遍歷全部標籤,記錄符合域名的標籤的url,以及最後一個標籤頁
 * 2、如果沒有符合域名的標籤,則建立主頁,並將isOpen狀態改為true
 * 3、如果有符合域名的標籤:
 *        1、獲取當前的頁面url
 *        2、如果當前頁面url不符合域名,則定位到這個標籤頁,將isOpen狀態改為true
 *        3、如果當前頁面url符合域名,則關閉所有標籤頁(按情形1處理),將isOpen狀態改為false
 */
function focusOrCreateTab(patternStr, url) {
  let pattern = new RegExp(patternStr)
  let theTabs = []
  let theLastTab = null
  walkEveryTab(function (tab) {
      if (pattern.test(tab.url)) {
        theTabs.push(tab.url)
        theLastTab = tab
      }
    }, function () {
      if (theTabs.length > 0) {
        chrome.tabs.query({active: true}, function (tab) {
          let currentUrl = tab[0].url
          if (theTabs.indexOf(currentUrl) > -1) {
            removePages(patternStr)
            isOpen = false
          } else {
            chrome.tabs.update(theLastTab.id, {"selected": true});
            isOpen = true
          }
        })
      } else {
        chrome.tabs.create({"url": url, "selected": true});
        isOpen = true
      }
    }
  )
}

/**
 * 情形2-2:
 * 1、把tabCache所有標籤頁重新開啟
 * 2、將isOpen狀態改為true
 */
function openAllCachedTab(tabCache) {
  let focusTab = null
  tabCache.forEach(function (url, index) {
    chrome.tabs.create({`url`: url}, function (tab) {
      if (tab.url === currentPageUrl) {
        focusTab = tab.id
      }
      if (index === tabCache.length-1 - 1) {
        if (focusTab) {
          chrome.tabs.update(focusTab, {"selected": true},function(){
          });
        }
      }
    })
  })
  isOpen = true
}




/**
 *
 * @param callback
 * @param lastCallback
 * 包裝一下遍歷全部標籤的函式,建立兩個回撥。
 * 一個回撥是每一次遍歷的過程中就執行一遍。
 * 一個回撥是全部遍歷完後執行一遍。
 */
function walkEveryTab(callback, lastCallback) {
  chrome.windows.getAll({"populate": true}, function (windows) {
    for (let i in windows) {
      let tabs = windows[i].tabs;
      for (let j in tabs) {
        let tab = tabs[j];
        callback(tab)
      }
    }
    if(lastCallback) lastCallback()
  })
}

複製程式碼

上傳與釋出外掛

我們需要在Chrome的開發者中心釋出外掛,進入 Developer Dashboard

好了,一個簡單易用的上班賴皮外掛做好了!在除錯模式下,你可以用ctrl+s來快捷尋找、開啟、關閉、重新開啟賴皮頁面。隨時隨地、全方位賴皮,從容面對老闆查崗。

可配置的高階賴皮外掛

現在我希望我的外掛都可以隨時配置站點:

那麼就需要用到chrome.storage了。

你需要開啟storage許可權:

manifest.json內新增

...
  "permissions": [
    "tabs","storage"
  ],
...
複製程式碼

然後使用

chrome.storage.local.set({
        `value1`:theValue1,
        `value2`,theValue2
})
複製程式碼

這種形式來存放storage。
這後使用

chrome.storage.local.get([`value1`],(res)=>{
    const theValue1 = res.value1
})
複製程式碼

這樣得到存放的value值。

開始改寫background.js

我們把mainPageUrlmyPattern改成從storage中獲取。

const INIT_SITES_LIST = [`bilibili.com`,`douyu.com`,`sgamer.com`,`hupu.com`]
const INIT_MAIN_PAGE = `https://bbs.hupu.com/all-gambia`

// 在安裝時即設定好storage
chrome.runtime.onInstalled.addListener(function() {
  chrome.storage.local.set({
    sites: INIT_SITES_LIST,
    mainPage:INIT_MAIN_PAGE
  })
});


//初始化isOpen和tabCache狀態
let isOpen = false
let tabCache = []
let currentPageUrl = ``

/**
 * 開始步驟: 判斷isOpen狀態
 * 情形一:isOpen為true,則移除頁面
 * 情形二:isOpen為false,則過載頁面
 */
chrome.commands.onCommand.addListener(function (command) {
  if (command === `toggle-tags`) {
    chrome.storage.local.get([`sites`,`mainPage`],function(res){
      let sites =  res.sites
      let mainPageUrl = res.mainPage
      let myPattern = sites.map(item=>item.replace(`.`,`\.`)).join(`|`)
      console.log(myPattern)

      if (isOpen) {
        //情形一:isOpen為true
        removePages(myPattern)
        //情形二:isOpen為false
      } else {
        reloadPages(myPattern, mainPageUrl)
      }
    })

  }
})
// ======================== 下面的部分不需要改動,看到這裡就夠了)

/**
 * 情形1:移除頁面
 * 1、清空tabCache快取
 * 2、關閉所有域名內標籤
 * 3、將關閉的標籤存入tabCache快取陣列
 * 4、將isOpen狀態改為false
 */
function removePages(patternStr) {
  tabCache = []
  chrome.tabs.query({active: true}, function (tab) {
    currentPageUrl = tab[0].url
  })
  let pattern = new RegExp(patternStr)
  walkEveryTab(function (tab) {
    if (pattern.test(tab.url)) {
      chrome.tabs.remove(tab.id,function(){
        tabCache.push(tab.url)
      })
    }
  },function(){
    isOpen = false
  })
}

/**
 * 情形2:過載頁面
 * 判斷有沒有快取:
 *    情形2-1無快取:開啟新標籤或定位到域名內的標籤
 *    情形2-2有快取:開啟全部快取內的頁面
 */
function reloadPages(patternStr, mainPageUrl) {
  if (tabCache.length === 0) {
    focusOrCreateTab(patternStr, mainPageUrl)
  } else {
    openAllCachedTab(tabCache)
  }
}

/**
 * 情形2-1:開啟新標籤或定位到域名內的標籤
 * 1、遍歷全部標籤,記錄符合域名的標籤的url,以及最後一個標籤頁
 * 2、如果沒有符合域名的標籤,則建立主頁,並將isOpen狀態改為true
 * 3、如果有符合域名的標籤:
 *        1、獲取當前的頁面url
 *        2、如果當前頁面url不符合域名,則定位到這個標籤頁,將isOpen狀態改為true
 *        3、如果當前頁面url符合域名,則關閉所有標籤頁(按情形1處理),將isOpen狀態改為false
 */
function focusOrCreateTab(patternStr, url) {
  let pattern = new RegExp(patternStr)
  let theTabs = []
  let theLastTab = null
  walkEveryTab(function (tab) {
      if (pattern.test(tab.url)) {
        theTabs.push(tab.url)
        theLastTab = tab
      }
    }, function () {
      if (theTabs.length > 0) {
        chrome.tabs.query({active: true}, function (tab) {
          let currentUrl = tab[0].url
          if (theTabs.indexOf(currentUrl) > -1) {
            removePages(patternStr)
            isOpen = false
          } else {
            chrome.tabs.update(theLastTab.id, {"selected": true});
            isOpen = true
          }
        })
      } else {
        chrome.tabs.create({"url": url, "selected": true});
        isOpen = true
      }
    }
  )
}

/**
 * 情形2-2:
 * 1、把tabCache所有標籤頁重新開啟
 * 2、將isOpen狀態改為true
 */
function openAllCachedTab(tabCache) {
  let focusTab = null
  tabCache.forEach(function (url, index) {
    chrome.tabs.create({`url`: url}, function (tab) {
      if (tab.url === currentPageUrl) {
        focusTab = tab.id
      }
      if (index === tabCache.length-1 - 1) {
        if (focusTab) {
          chrome.tabs.update(focusTab, {"selected": true},function(){
          });
        }
      }
    })
  })
  isOpen = true
}




/**
 *
 * @param callback
 * @param lastCallback
 * 包裝一下遍歷全部標籤的函式,建立兩個回撥。
 * 一個回撥是每一次遍歷的過程中就執行一遍。
 * 一個回撥是全部遍歷完後執行一遍。
 */
function walkEveryTab(callback, lastCallback) {
  chrome.windows.getAll({"populate": true}, function (windows) {
    for (let i in windows) {
      let tabs = windows[i].tabs;
      for (let j in tabs) {
        let tab = tabs[j];
        callback(tab)
      }
    }
    if(lastCallback) lastCallback()
  })
}



複製程式碼

那麼我們可以寫一個popup頁面,如果點選圖示就會顯示,如圖:

image

下面我們完善一下popup.html和popup.css和pupup.js頁面

所有js檔案都可以直接呼叫chrome.storage.local.get

只需要特別注意一下js檔案的chrome.storage呼叫部分

其它的拷貝即可,我們不是來學頁面佈局的

popup.html

<html>
<head>
  <title>常用網站配置頁面</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
  <h2 class="lapi-title">常用賴皮站點域名</h2>
  <ul class="lapi-content">
  </ul>
  <p>
  <label><input type="text" id="add" class="add"></label>
  <button class="button add-button ">+</button>
  </p>
  <p></p>
  <p></p>
  <p></p>
  <h2>我的賴皮主頁</h2>
  <div id="change-content">
  <span class="main-page-inactive" id="main-page-inactive"></span><button class="button change-button " id="change">✎</button>
  </div>

  <p class="lapi-tip">按<span class="lapi-key">Alt+S</span>快速開啟/關閉賴皮站點</p>
</div>
<script src="zepto.min.js"></script>
<script src="popup.js"></script>
</body>
</html>
複製程式碼

popup.css

* {
    margin: 0;
    padding: 0;
    color:#6a6f77;
}


input, button, select, textarea {
    outline: none;
    -webkit-appearance: none;
    border-radius: 0;
    border: none;
}

input:focus{
    list-style: none;
    box-shadow: none;
}

ol, ul {
    list-style: none;
}

li{
    margin: 5px 0;
}

.container {
    width: 200px;
    padding: 10px;
}

.container h2{
    margin: 10px;
    text-align: center;
    display: block;
}

.lapi-content li{
    transition: opacity 1s;
}

.site{
    cursor: pointer;
    color: #00b0ff;
}

.add, .main-page{
    box-sizing: border-box;
    text-align:center;
    font-size:14px;
    /*height:27px;*/
    border-radius:3px;
    border:1px solid #c8cccf;
    color:#6a6f77;
    outline:0;
    padding:0 10px;
    text-decoration:none;
    width: 170px;
}

#main-page{
    font-size: 12px;
    text-align: left;
    width: 166px;
    margin: 0;
    padding: 2px;
}

.add{
    height: 27px;
}

.main-page{
    width: 170px;
    outline: none;
    resize: none;

}

.main-page-inactive{
    width: 160px;
    line-break: auto;
    word-break: break-word;
    overflow: hidden;
    display: inline-block;
    cursor: pointer;
    color: #00b0ff;
    margin: 3px;
}

.button{

    font-size: 16px;
    /*border: 1px solid #c8cccf;*/
    color: #c8cccf;
    /*border: none;*/
    padding: 0 4px 1px 3px;
    border-radius: 3px;
}

.close-button{
    transition: all 1s;
}

.button:hover{
    background: #E27575;
    color: #FFF;
}

.add-button{
    transition:all 1s;
    font-size: 20px;
    padding: 0 6px 1px 5px;
}

.change-button{
    position: absolute;
    transition:all 1s;
    font-size: 20px;
    padding: 0 6px 1px 5px;
}
.change-button:hover{
    background: #f9a825;
    color: #FFF;
}

#change-check{
    color: #f9a825;
}

#change-check:hover{
    color: #fff;
}

.add-button:hover{
    background: #B8DDFF;
    color: #FFF;
}

.submit{
    transition: all 1s;
    margin: 10px;
    padding: 5px 10px;
    font-size: 16px;
    border-radius: 4px;
    background: #B8DDFF;
    border: 1px solid #B8DDFF;
    color: #FFF;
}

.submit:hover{
    border: 1px solid #B8DDFF;
    background: #fff;
    color: #B8DDFF;
}

.fade{
    opacity: 0;
}

.add-wrong,.add-wrong:focus{
    border: #e91e63 1px solid;
    box-shadow:0 0 5px rgba(233,30,99,.3);
}

.lapi-tip{
    margin-top: 20px;
    border-top: 1px solid #c8cccf;
    padding-top: 8px;
    color: #c8cccf;
    text-align: center;
}

.lapi-key{
    color: #B8DDFF;

}
複製程式碼

重點關注:chrome.storage部分

popup.js

let sites = []
let mainPage = ``
const isMac = /Macintosh/.test(navigator.userAgent)
let $lapiKey = $(`.lapi-key`)

isMac? $lapiKey.text(`Control+S`):$lapiKey.text(`Alt+S`)

// 從storage中取出site和mainPage欄位,並設定在頁面上。
chrome.storage.local.get([`sites`,`mainPage`], function (res) {
  if (res.sites) {
    sites = res.sites
    mainPage = res.mainPage
    sites.forEach(function (item) {
      let appendEl = `<li><span class="site">` + item + `</span>
` +
        `<button class="button close-button">&times</button>
` +
        `</li>`
      $(`ul.lapi-content`).append(appendEl)
    })

  }
  $(`#main-page`).val(mainPage)
  $(`#main-page-inactive`).html(mainPage)
})


$(`#save`).on(`click`, function () {
  alert()
})

$(`#change-content`).delegate(`#main-page-inactive`,`click`,function(){
  let mainPageUrl = $(this).html()
  if(/^http://|^https:///.test(mainPageUrl)){
    chrome.tabs.create({"url": mainPageUrl, "selected": true})
  }else{
    chrome.tabs.create({"url": `http://`+mainPageUrl, "selected": true})
  }
})


let addEl = $(`#add`)
addEl.focus()
let lapiCon = $(`ul.lapi-content`)

lapiCon.delegate(`.close-button`, `click`, function () {
  let $this = $(this)
  let siteValue = $this.siblings().html()
  sites = sites.filter(function (item) {
    return item !== siteValue
  })
  chrome.storage.local.set({sites: sites})
  $this.parent().addClass(`fade`)
  setTimeout(function () {
    $this.parent().remove()
  }, 800)
})


$(`.add-button`).on(`click`,addEvent)
addEl.bind(`keypress`,function(event){
  if(event.keyCode === 13) addEvent()
})


function addEvent(){
  if(!validate(addEl.val())){
    addEl.addClass(`add-wrong`)
  }else{
    let appendEl = `<li><span class="site">` + addEl.val() + `</span>
` +
      `<button class="button close-button">&times</button>
` +
      `</li>`
    $(`ul.lapi-content`).append(appendEl)
    sites.push(addEl.val())
    chrome.storage.local.set({sites:sites})
    addEl.removeClass(`add-wrong`)
    addEl.focus().val(``)
  }
}

function validate(value){
  value = value.trim()
  if(value.length ===0){
    return false
  }
  return /^([w_-]+.)*[w_-]+$/.test(value)
}

lapiCon.delegate(`.site`,`click`,function(){
  let siteUrl = $(this).html()
  chrome.tabs.create({"url": `http://`+siteUrl, "selected": true})
})

$(`#change-content`).delegate(`#change`,`click`,function(){
  changeMainPage($(this))
}).delegate(`#change-check`,`click`,function(){
  changeCheck($(`#change-check`))
}).delegate(`#main-page`,`blur`,function(){
  changeCheck($(`#change-check`))
})


function changeMainPage($this){
  $this.siblings().remove()
  $this.parent().prepend(`<label><textarea id="main-page" class="main-page"></textarea></label>`)
  $this.parent().append(`<button class="button change-button " id="change-check">✓</button>`)
  $(`#main-page`).val(mainPage).focus()
  $this.remove()
}


function changeCheck($this){
  let mainPageVal = $(`#main-page`).val()
  $this.siblings().remove()
  $this.parent().prepend(`<span class="main-page-inactive" id="main-page-inactive"></span>`)
  $(`#main-page-inactive`).text(mainPageVal)
  chrome.storage.local.set({mainPage:mainPageVal})
  $this.parent().append(`<button class="button change-button " id="change">✎</button>`)
}
複製程式碼

好了,一個優雅的賴皮外掛就做好了,大家可以檢視
github.com/wanthering/…

祝大家上班賴得開心。 別老躲廁所玩手機了,不健康!賴皮外掛用起來!

相關文章