禅道日报自动化脚本
下面展示了一种一键将 Microsoft TODO 待办事项导入到禅道日志并作为日报的脚本,在完成日报填写后执行了 WebHook 回调。可根据自己需求决定日报数据的来源和执行后的动作(比如发送邮件或发送信息到钉钉、Slack 等)。如果当日有修复 BUG 等任务,会自动将其清空并使用我们自己的数据源。
在绝大部分情况下,不需要任何手动干预即可完成数据导入。
此脚本需要 Chrome、对应 Chrome 版本的 Chrome WebDriver,Babashka,登录所需凭证在 config.edn
中配置或暴露为环境变量 EDU_PASS
。可配合 clj-runner 实现双击脚本直接运行。
所需的工具除了 Chrome 浏览器外如下所示:
-
chromedriver.exe
Chrome WebDriver 驱动,需要和 Chrome 版本一致,下载
-
bb.exe
Babashka SCI 执行环境,需要将其暴露到 PATH,下载
-
config.edn
内容如下所示,或不提供此文件且直接暴露环境变量 ZENDAO_PASS,格式为 {username}::{password}::{driver-path}。
{:user "" :pass "" :path-driver "chromedriver.exe"}
-
script.clj
内容如下所示,直接
clj-runner script.clj
执行或双击选择使用 clj-runner 打开(Windows)
#!/usr/bin/env bb -Sdeps '{:paths ["."] :deps {etaoin/etaoin {:mvn/version,"1.0.39"}}}'
(ns zendao-auto
"日报自动化,需要暴露变量 ZENDAO_PASS,格式为 {username}::{password}::{driver-path} 或提供 config.edn 文件。"
(:require
[clojure.string :as str]
[org.httpkit.client :as http]
[etaoin.api :as e]
[cheshire.core :as json])
(:import (java.util Base64)))
(def config (merge
(if-let [pass-env (System/getenv "ZENDAO_PASS")]
(let [pass (.split (String. (.decode (Base64/getDecoder) pass-env)) "::")]
{:user (first pass) :pass (second pass) :path-driver (last pass)})
{:user "xxx@xxx.com"
:pass "YOUR_PASS_HERE"
:path-driver (if (= "SCI" (-> *clojure-version* :qualifier))
"../resources/chromedriver.exe"
"resources/chromedriver.exe")})
{:cyberToken (System/getenv "CYBER_TOKEN")
:cyberUrl "日报的数据源(这里是我自己的 Miscrosoft TODO 接口)"
:callbackPostCyberUrl "执行后的回调接口"}))
(defn go-to-log-page [driver]
(println "try login now...")
(e/go driver "https://ZENDAO_URL")
(e/wait-visible driver {:id :account})
(e/fill driver {:id :account} (:user config))
(e/fill driver {:type :password} (:pass config))
(e/click driver {:id :submit})
(e/wait-visible driver "//*[@id=\"menuMainNav\"]/li[1]/a/span")
(println "login success, go to log page...")
(e/switch-frame driver {:id :appIframe-my})
(e/click driver {:xpath "/html/body/header/div/div/nav/ul/li[3]/a"})
(e/wait-visible driver {:css ".current"}))
(defn go-to-new-log-page
"进入新增日志模态框"
[driver]
(println "go to new log page...")
(e/click driver {:css ".current"})
(e/switch-frame driver {:id :iframe-triggerModal})
(try
(e/wait-visible driver {:id :submit})
(catch Exception _
(println "today have work log, please delete them first!")
(e/js-execute driver "alert('日志不为空,请删除日志后关闭程序并重试!')"))))
(defn fill-tasks-on-log-page [driver tasks]
(if (empty? tasks)
(do (println "no task today, exist now..."))
(do (e/click driver {:css ".modal-actions > button:nth-child(1)"})
(e/wait driver 1)
(let [contents (e/query-all driver {:id "work[]"})
objs (e/query-all driver {:id "objectType_chosen"})
lefts (filterv #(let [name (e/get-element-attr-el driver % :name)]
(and name (str/starts-with? name "left[")))
(e/query-all driver {:tag :input :class "form-control"}))]
(doseq [[{:keys [title hour] :as item} index]
(zipmap tasks [0 1 2 3 4 5 6 7 8])]
(println "now handling" item)
(e/click-el driver (nth contents index))
(e/fill-el driver (nth contents index) title)
(e/click-el driver (nth objs index))
(e/wait driver 0.5)
(let [suggest-obj-selects
(first (filter
#(let [title (e/get-element-attr-el driver % :title)]
(and title (str/includes? title "任务")))
(e/query-all driver {:css (format "#objectTable > tbody > tr:nth-child(%s) li"
(+ index 1))})))]
(println "choose task: " (e/get-element-attr-el driver suggest-obj-selects :title))
(e/click-el driver suggest-obj-selects)
(e/click-el driver (nth lefts index))
(e/fill-el driver (nth lefts index) "1")
(let [costs (e/query-all driver {:id "consumed[]"})]
(e/click-el driver (nth costs index))
(e/fill-el driver (nth costs index) (str hour))
(e/wait driver 0.5))))
(e/click driver {:id "submit"})))))
(defn -main []
(let [tasks (-> (http/request {:url (:cyberUrl config)
:headers {"authorization" (:cyberToken config)}})
deref
:body
(json/parse-string true)
:data)]
(if-not (empty? tasks)
(let [driver (e/chrome (merge {:path-driver (:path-driver config)
:log-level :warn}
(if-let [chrome (:chrome config)]
{:path-browser chrome}
{})))]
(go-to-log-page driver)
(go-to-new-log-page driver)
(Thread/sleep 2000)
(fill-tasks-on-log-page driver tasks)
(-> (http/request {:url (:callbackPostCyberUrl config)
:method :post
:headers {"authorization" (:cyberToken config)
"content-type" "application/json"}
:body (json/generate-string {:status "已完成日报"} true)})
deref
:body
(json/parse-string true)
println)
;等待手动检查确认无误
(Thread/sleep 5000)
(e/quit driver))
(println "no task in Microsoft TODO, exist now..."))))
(if (= "SCI" (-> *clojure-version* :qualifier))
(-main))
2024年5月16日更新
由于版本迭代,现在我使用一种新方法来快速打开禅道 BUG 页、日报页面,取消了自动填写日志的功能。此脚本需要篡改猴和 Chrome 浏览器支持:
// ==UserScript==
// @name 禅道快捷操作
// @namespace http://mazhangjing.com/quick-zendao
// @version 2024-05-16
// @description try to take over the world!
// @author You
// @match https://cd.icnrd.cn/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=icnrd.cn
// @grant unsafeWindow
// ==/UserScript==
(function() {
'use strict';
function clone(href, text) {
var systemLi = unsafeWindow.document.querySelector('ul#menuMainNav li[data-app="system"]');
if (systemLi) {
var clonedLi = systemLi.cloneNode(true);
var clonedLink = clonedLi.querySelector('a');
clonedLink.removeAttribute('data-pos');
clonedLink.removeAttribute('data-app');
clonedLink.removeAttribute('data-toggle');
clonedLink.removeAttribute('data-original-title');
clonedLink.removeAttribute('class');
clonedLink.removeAttribute('title');
clonedLink.href = href;
var span = clonedLink.querySelector('span');
if (span) {
span.innerText = text;
}
var li = clonedLink.querySelector('i');
if (li) {
li.removeAttribute('class');
}
return clonedLi;
} else {
console.log("Element not found");
return null;
}
}
window.onload = function() {
var myList = unsafeWindow.document.getElementById('menuMainNav');
if (myList) {
myList.appendChild(clone("/my-work-bug.html", "待处理BUG"))
myList.appendChild(clone("/my-contribute-bug-resolvedBy.html", "已处理BUG"))
myList.appendChild(clone('/effort-calendar.html', "写日报"))
} else {
console.log("Waiting for elements...");
}
}
})();