// ==UserScript== // @name 天和 SQL 工单通知助手 // @namespace https://tianhe.vip.sftcwl.com/ // @version 0.0.1 // @description 在历史工单与流水线列表右侧增加“通知”按钮,点击后发送钉钉机器人消息 // @match https://tianhe.vip.sftcwl.com/paasapp/mysql* // @grant GM_xmlhttpRequest // @connect oapi.dingtalk.com // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const WEBHOOK_URL = 'https://oapi.dingtalk.com/robot/send?access_token=dbf7d2ed85ea9145f2ddce30c1dbd1eaeb1ddce6334b5e93988bec919e6b1b4f'; const TARGET_PATH = '/paasapp/mysql'; const TARGET_HASH = '#/sqleditor/SqlFlowOwn'; const FIXED_PAGE_URL = `https://tianhe.vip.sftcwl.com${TARGET_PATH}${TARGET_HASH}`; const BTN_CLASS = 'js-workorder-notify-btn'; const STYLE_ID = 'js-workorder-notify-style'; const AT_CONFIG = { name: '丁锐', atMobiles: ['13520041679'], atUserIds: ['e0g-kgfjs0dgt'] }; let isRunning = false; let observer = null; let appendTimer = null; function isTargetPage() { return location.pathname === TARGET_PATH && location.hash.startsWith(TARGET_HASH); } function cleanText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } function extractDisplayName(value) { const text = cleanText(value); const match = text.match(/([^\s()]{1,20})\(\d{8}\)/); return match ? match[1] : ''; } function injectStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` .${BTN_CLASS} { margin-left: 8px; padding: 0; border: none; background: transparent; color: #409EFF; cursor: pointer; font-size: 12px; line-height: 1; } .${BTN_CLASS}:hover { color: #66b1ff; } .${BTN_CLASS}[disabled] { color: #c0c4cc; cursor: not-allowed; } `; document.head.appendChild(style); } function showMessage(type, message) { if (window.ELEMENT && typeof window.ELEMENT.Message === 'function') { window.ELEMENT.Message({ type, message }); return; } console.log(`[${type}] ${message}`); } function getActionRows() { return document.querySelectorAll('.el-table__fixed-right .el-table__fixed-body-wrapper tbody tr.el-table__row'); } function getActionCell(tr) { const tds = tr.querySelectorAll('td'); const lastTd = tds[tds.length - 1]; return lastTd ? lastTd.querySelector('.cell') : null; } function extractRowInfo(tr) { const cells = [...tr.querySelectorAll('td .cell')].map(el => cleanText(el.textContent)); return { id: cells[0] || '' }; } function getLoginUserName() { const selectors = [ '.user-name', '.user-name *', '.el-dropdown-link', '.el-dropdown-link *', '.navigation .user-name', '.navigation .el-dropdown-link' ]; for (const selector of selectors) { const elements = document.querySelectorAll(selector); for (const el of elements) { const name = extractDisplayName(el.textContent || ''); if (name) return name; } } return ''; } function buildTextMessage(info) { const loginUser = getLoginUserName() || '未知用户'; const lines = [ '工单通知', `工单ID:${info.id || '未知'}`, `发送人:${loginUser}`, `页面:${FIXED_PAGE_URL}`, '麻烦关注下这个工单。' ]; if (AT_CONFIG.name) { lines.push(`@${AT_CONFIG.name}`); } return lines.join('\n'); } function buildPayload(info) { return { msgtype: 'text', text: { content: buildTextMessage(info) }, at: { atMobiles: AT_CONFIG.atMobiles, atUserIds: AT_CONFIG.atUserIds, isAtAll: false } }; } function sendWebhook(payload) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('GM_xmlhttpRequest 不可用,请确认在暴力猴/油猴环境运行')); return; } GM_xmlhttpRequest({ method: 'POST', url: WEBHOOK_URL, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(payload), onload(response) { try { const data = JSON.parse(response.responseText || '{}'); if (data.errcode === 0) { resolve(data); return; } reject(new Error(data.errmsg || '发送失败')); } catch (err) { reject(err); } }, onerror() { reject(new Error('网络请求失败')); }, ontimeout() { reject(new Error('请求超时')); } }); }); } async function handleNotify(btn, tr) { if (btn.disabled) return; const info = extractRowInfo(tr); btn.disabled = true; btn.textContent = '发送中...'; try { await sendWebhook(buildPayload(info)); btn.textContent = '已通知'; showMessage('success', `工单 ${info.id || ''} 通知成功`); setTimeout(() => { btn.disabled = false; btn.textContent = '通知'; }, 1200); } catch (err) { btn.disabled = false; btn.textContent = '重试通知'; showMessage('error', err.message || '发送失败'); console.error('工单通知发送失败', err); } } function appendNotifyButtons() { if (!isRunning || !isTargetPage()) return; const rows = getActionRows(); if (!rows.length) return; rows.forEach((tr) => { const actionCell = getActionCell(tr); if (!actionCell) return; if (actionCell.querySelector(`.${BTN_CLASS}`)) return; const btn = document.createElement('button'); btn.type = 'button'; btn.className = BTN_CLASS; btn.textContent = '通知'; btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); handleNotify(btn, tr); }); actionCell.appendChild(btn); }); } function removeNotifyButtons() { document.querySelectorAll(`.${BTN_CLASS}`).forEach((btn) => btn.remove()); } function startObserver() { if (observer) return; observer = new MutationObserver(() => { clearTimeout(appendTimer); appendTimer = setTimeout(() => { appendNotifyButtons(); }, 150); }); observer.observe(document.body, { childList: true, subtree: true }); } function stopObserver() { if (observer) { observer.disconnect(); observer = null; } clearTimeout(appendTimer); appendTimer = null; } function startApp() { if (isRunning || !isTargetPage()) return; isRunning = true; injectStyle(); appendNotifyButtons(); startObserver(); } function stopApp() { if (!isRunning) return; isRunning = false; stopObserver(); removeNotifyButtons(); } function syncAppState() { if (isTargetPage()) { startApp(); return; } stopApp(); } function installRouteListeners() { const notifyRouteChange = () => { setTimeout(syncAppState, 0); }; const rawPushState = history.pushState; history.pushState = function (...args) { const result = rawPushState.apply(this, args); notifyRouteChange(); return result; }; const rawReplaceState = history.replaceState; history.replaceState = function (...args) { const result = rawReplaceState.apply(this, args); notifyRouteChange(); return result; }; window.addEventListener('hashchange', notifyRouteChange); window.addEventListener('popstate', notifyRouteChange); } installRouteListeners(); syncAppState(); })();