Jenkins 联合构建 (v4.0 - 底部悬浮)

在 Jenkins 页面底部添加悬浮的状态显示、进度条和“联合构建”按钮

当前为 2025-10-24 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Jenkins 联合构建 (v4.0 - 底部悬浮)
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  在 Jenkins 页面底部添加悬浮的状态显示、进度条和“联合构建”按钮
// @author       Tandy (修改 by Gemini)
// @match        http://10.9.31.83:9001/job/sz-newcis-dev/*
// @grant        none
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // -----------------------------------------------------------------
    //  v4.0 更新日志:
    //  1. UI 从左上角移至底部悬浮工具栏。
    //  2. 按钮在左侧,状态/进度条在右侧。
    //  3. 修复了 v3.3 的加载逻辑。
    // -----------------------------------------------------------------

    // 1. 定义所有函数

    function getMyJenkinsCrumb() {
        const crumbInput = document.querySelector('input[name="Jenkins-Crumb"]');
        if (crumbInput) {
            return crumbInput.value;
        }
        console.error("未能找到 Jenkins Crumb input 元素。");
        return null;
    }

    let statusDisplay, progressBar, progressContainer;

    function updateStatus(message, isError = false) {
        if (!statusDisplay) return; // 安全检查
        // console.log(message);
        statusDisplay.innerText = message;
        statusDisplay.style.color = isError ? 'red' : 'black';
        statusDisplay.style.borderColor = isError ? 'red' : '#ccc';
    }

    function startVisibleTimer(durationMs) {
        return new Promise(resolve => {
            if (!progressContainer || !progressBar) {
                console.error("进度条元素未初始化。");
                return resolve(); // 立即结束,防止卡住
            }

            progressContainer.style.display = 'block';
            progressBar.style.width = '0%';
            const startTime = Date.now();
            const updateIntervalMs = 100;

            const timerInterval = setInterval(() => {
                const elapsed = Date.now() - startTime;
                if (elapsed >= durationMs) {
                    clearInterval(timerInterval);
                    progressContainer.style.display = 'none';
                    progressBar.style.width = '0%';
                    resolve();
                } else {
                    const progressPercent = (elapsed / durationMs) * 100;
                    const remainingSeconds = (durationMs - elapsed) / 1000;
                    progressBar.style.width = `${progressPercent}%`;
                    updateStatus(`步骤 2: 等待中... ${remainingSeconds.toFixed(1)}s`);
                }
            }, updateIntervalMs);
        });
    }

    async function triggerSingleBuild(jobName, buildUrl, crumb) {
        updateStatus(`[${jobName}] 正在请求构建...`);
        try {
            const response = await fetch(buildUrl, {
                method: 'POST',
                headers: { 'Jenkins-Crumb': crumb },
                body: null
            });
            if (response.ok) {
                updateStatus(`[${jobName}] 构建请求已成功发送!`);
                return true;
            } else {
                updateStatus(`[${jobName}] 构建请求失败!状态: ${response.status}`, true);
                return false;
            }
        } catch (error) {
            updateStatus(`[${jobName}] 发送请求时发生网络错误: ${error}`, true);
            return false;
        }
    }

    async function startCombinedChain() {
        const crumb = getMyJenkinsCrumb();
        if (!crumb) {
            updateStatus("错误:无法获取 Crumb。", true);
            return;
        }

        updateStatus('步骤 1: 正在同时触发 Common 和 API ...');
        const commonUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-common/build?delay=0sec';
        const apiUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-api/build?delay=0sec';
        const parallelBuilds = [
            triggerSingleBuild('Common', commonUrl, crumb),
            triggerSingleBuild('API', apiUrl, crumb)
        ];
        const results = await Promise.all(parallelBuilds);
        const allSucceeded = results.every(res => res === true);
        if (!allSucceeded) {
            updateStatus("步骤 1 失败:Common 或 API 构建请求失败。构建链中止。", true);
            return;
        }
        updateStatus('Common 和 API 构建已全部触发。');

        await startVisibleTimer(60000);

        updateStatus('步骤 3: 正在触发 Bill Service ...');
        const billUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-bill-service/build?delay=0sec';
        let success = await triggerSingleBuild('Bill Service', billUrl, crumb);
        if (!success) {
            updateStatus("构建链因 Bill Service 失败而中止。", true);
            return;
        }

        updateStatus('步骤 4: 正在触发 Customer Service ...');
        const customerUrl = 'http://10.9.31.83:9001/job/sz-newcis-dev/job/sz-newcis-dev_cis-customer-service/build?delay=0sec';
        success = await triggerSingleBuild('Customer Service', customerUrl, crumb);
        if (!success) {
            updateStatus("Customer Service 构建失败。构建链结束。", true);
            return;
        }
        updateStatus("联合构建链 (Common/API -> Bill -> Customer) 全部完成。");
    }

    // 2. (已修改) 定义我们的 "主" 函数,用于创建和附加 UI 元素
    function createUI() {

        // --- 1. 创建主悬浮容器 (页脚工具栏) ---
        const footerBar = document.createElement('div');
        footerBar.id = 'gm-footer-bar';
        footerBar.style = `
            position: fixed;
            bottom: 0;
            left: 0;
            width: 100%;
            padding: 8px 12px;
            background-color: #f0f0f0;
            border-top: 1px solid #ccc;
            z-index: 9997;
            display: flex;
            justify-content: space-between; /* 核心:实现左右分离 */
            align-items: center;
            box-sizing: border-box;
            font-family: Arial, sans-serif;
        `;

        // --- 2. 创建左侧容器 (用于按钮) ---
        const leftControls = document.createElement('div');
        leftControls.id = 'gm-left-controls';

        // --- 3. 创建右侧容器 (用于状态和进度条) ---
        const rightControls = document.createElement('div');
        rightControls.id = 'gm-right-controls';
        rightControls.style = `
            display: flex;
            flex-direction: column; /* 状态和进度条垂直堆叠 */
            width: 250px; /* 保持原始宽度 */
        `;

        // --- 状态显示 (样式修改) ---
        statusDisplay = document.createElement('div');
        statusDisplay.id = 'gm-status-display';
        statusDisplay.style = `
            /* 移除了 position, top, left, z-index */
            padding: 8px;
            background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px;
            width: 100%; /* 宽度由父容器 rightControls 控制 */
            font-size: 13px;
            box-sizing: border-box;
            margin-bottom: 5px; /* 与下方进度条的间距 */
        `;
        statusDisplay.innerText = '准备就绪。';

        // --- 进度条 (样式修改) ---
        progressContainer = document.createElement('div');
        progressContainer.id = 'gm-progress-container';
        progressContainer.style = `
            /* 移除了 position, top, left, z-index */
            width: 100%; /* 宽度由父容器 rightControls 控制 */
            height: 10px; background-color: #e9ecef;
            border: 1px solid #ced4da; border-radius: 4px;
            box-sizing: border-box; display: none;
        `;
        progressBar = document.createElement('div');
        progressBar.id = 'gm-progress-bar';
        progressBar.style = `
            height: 100%; width: 0%; background-color: #007bff;
            border-radius: 2px; transition: width 0.1s linear;
        `;
        progressContainer.appendChild(progressBar);

        // --- 按钮 (样式修改) ---
        const combinedButton = document.createElement('button');
        combinedButton.innerText = '▶ 启动联合构建';
        combinedButton.style = `
            /* 移除了 position, left, top, z-index, width */
            padding: 8px 12px; color: white; border: none; border-radius: 4px;
            cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            text-align: left; background-color: #f0ad4e;
            box-sizing: border-box;
            font-size: 13px; /* 保持字体一致 */
        `;
        combinedButton.onmouseover = function() { combinedButton.style.backgroundColor = '#ec971f'; };
        combinedButton.onmouseout = function() { combinedButton.style.backgroundColor = '#f0ad4e'; };
        combinedButton.onclick = startCombinedChain;

        // --- 4. 组装 DOM ---
        
        // 按钮放入左侧
        leftControls.appendChild(combinedButton);
        
        // 状态和进度条放入右侧
        rightControls.appendChild(statusDisplay);
        rightControls.appendChild(progressContainer);
        
        // 左右侧放入主工具栏
        footerBar.appendChild(leftControls);
        footerBar.appendChild(rightControls);
        
        // 主工具栏放入页面
        document.body.appendChild(footerBar);

        updateStatus('联合构建工具已加载。');
    }

    // 3. (已恢复) 确保在 document.body 可用后才执行 UI 创建
    if (document.body) {
        createUI();
    } else {
        // 如果 body 还没好,就添加一个监听器
        window.addEventListener('load', createUI);
    }

})();