华为云在线考试助手 (V1.3 纯净防护版)

包含多重防护、可上传图片的摄像头伪装,并提供“一键复制题库”的辅助功能。

// ==UserScript==
// @name         华为云在线考试助手 (V1.3 纯净防护版)
// @namespace    http://tampermonkey.net/
// @version      1.3.0
// @description  包含多重防护、可上传图片的摄像头伪装,并提供“一键复制题库”的辅助功能。
// @author       妖火id31944
// @match        https://*.huaweicloud.com/*
// @grant        unsafeWindow
// @grant        GM_setClipboard
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    /*
     * ===================================================================================
     * P1: 核心防护模块 (Core Protection Module)
     * 职责: 破解平台的防作弊机制,为用户提供一个安全的考试环境。
     * - multiLayerVideoProtection: 强制静音所有用于监控的视频流。
     * - antiScreenSwitch: 屏蔽切屏、窗口失焦等事件监听,防止系统检测到用户离开考试页面。
     * ===================================================================================
    */

    (function multiLayerVideoProtection() {
        if (!window.HTMLVideoElement) return;
        // 策略1: 覆写 play 方法
        (function patchPlayMethod() {
            const originalPlay = HTMLVideoElement.prototype.play;
            HTMLVideoElement.prototype.play = function(...args) {
                if (this.srcObject instanceof MediaStream) { this.muted = true; }
                return originalPlay.apply(this, args);
            };
        })();
        // 策略2: 覆写 srcObject setter
        (function patchSrcObject() {
            const videoProto = HTMLVideoElement.prototype;
            const originalSrcObjectDescriptor = Object.getOwnPropertyDescriptor(videoProto, 'srcObject');
            if (!originalSrcObjectDescriptor || typeof originalSrcObjectDescriptor.set !== 'function') { return; }
            Object.defineProperty(videoProto, 'srcObject', {
                ...originalSrcObjectDescriptor,
                set: function(stream) { if (stream instanceof MediaStream && stream.getVideoTracks().length > 0) { this.muted = true; } return originalSrcObjectDescriptor.set.call(this, stream); }
            });
        })();
        // 策略3: 覆写 muted setter
        (function patchMutedProperty() {
            const videoProto = HTMLVideoElement.prototype;
            const originalMutedDescriptor = Object.getOwnPropertyDescriptor(videoProto, 'muted');
            if (!originalMutedDescriptor || typeof originalMutedDescriptor.set !== 'function') { return; }
            Object.defineProperty(videoProto, 'muted', {
                ...originalMutedDescriptor,
                set: function(value) { if (this.srcObject instanceof MediaStream && this.srcObject.getVideoTracks().length > 0) { return originalMutedDescriptor.set.call(this, true); } return originalMutedDescriptor.set.call(this, value); }
            });
        })();
        // 策略4: DOM 变动监听,处理动态加载的 video 元素
        (function setupDOMWatcher() {
            const processVideoElement = (video) => { if (video.tagName === 'VIDEO') { try { video.muted = true; } catch(e){} video.addEventListener('loadstart', () => { video.muted = true; }); video.addEventListener('canplay', () => { video.muted = true; }); } };
            document.querySelectorAll('video').forEach(processVideoElement);
            const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'VIDEO') { processVideoElement(node); } else if (node.querySelectorAll) { node.querySelectorAll('video').forEach(processVideoElement); } } }); }); });
            if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); }); }
        })();
    })();

    (function antiScreenSwitch() {
        const window = unsafeWindow; if (!window) return;
        const blackList = new Set(["visibilitychange", "blur", "pagehide", "mouseleave"]);
        function patchAddEventListener(obj) {
            if (!obj || !obj.addEventListener) return;
            obj._addEventListener = obj.addEventListener;
            obj.addEventListener = (...args) => { if (blackList.has(args[0])) { console.log(`[防护模块] 已屏蔽对事件 '${args[0]}' 的监听。`); return; } return obj._addEventListener(...args); };
        }
        patchAddEventListener(window);
        patchAddEventListener(document);
        Object.defineProperties(document, {
            hidden: { value: false, configurable: true }, webkitHidden: { value: false, configurable: true },
            visibilityState: { value: "visible", configurable: true }, webkitVisibilityState: { value: "visible", configurable: true },
            hasFocus: { value: () => true, configurable: true }
        });
    })();


    /*
     * ===================================================================================
     * P2: 统一UI与摄像头伪装模块 (Unified UI & Webcam Spoofing Module)
     * 职责: 创建用户操作界面,并提供核心的摄像头伪装功能。
     * - createFloatingWindow: 构建可拖动的悬浮窗UI。
     * - ensureWebcamHook: 覆写 navigator.mediaDevices.getUserMedia,当启用伪装时,返回由Canvas生成的虚假视频流。
     * ===================================================================================
    */

    (function unifiedUIAndWebcam() {
        const STORAGE_KEY_SPOOF = 'exam_helper_spoof_enabled';
        const STORAGE_KEY_IMG = 'exam_helper_fake_image_base64';
        let isWebcamSpoofingEnabled = localStorage.getItem(STORAGE_KEY_SPOOF) === 'true';
        let userImageDataBase64 = localStorage.getItem(STORAGE_KEY_IMG) || null;
        let cachedMediaStream = null;
        let animationInterval = null;

        const createFakeStream = () => new Promise(async (resolve) => {
            if (cachedMediaStream) return resolve(cachedMediaStream);
            const canvas = document.createElement('canvas'); canvas.width = 640; canvas.height = 480;
            const ctx = canvas.getContext('2d', { willReadFrequently: true });
            const loadedImg = await new Promise(resolveImg => {
                if (userImageDataBase64) { const img = new Image(); img.onload = () => resolveImg(img); img.onerror = () => resolveImg(null); img.src = userImageDataBase64; } else { resolveImg(null); }
            });
            if (!loadedImg) { ctx.fillStyle = '#333'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'white'; ctx.font = '20px Arial'; ctx.textAlign = 'center'; ctx.fillText('请上传照片', canvas.width / 2, canvas.height / 2); }
            if (animationInterval) clearInterval(animationInterval);
            animationInterval = setInterval(() => { if (loadedImg) { ctx.drawImage(loadedImg, 0, 0, canvas.width, canvas.height); } }, 100);
            cachedMediaStream = canvas.captureStream(10);
            resolve(cachedMediaStream);
        });

        function ensureWebcamHook() {
            if (!navigator.mediaDevices) navigator.mediaDevices = {};
            if (navigator.mediaDevices.getUserMedia.isPatchedByHelper) return;
            const originalGetUserMedia = navigator.mediaDevices.getUserMedia;
            navigator.mediaDevices.getUserMedia = async function(constraints) {
                if (isWebcamSpoofingEnabled && constraints?.video) { console.log("[摄像头模块] 伪装已激活,返回虚假视频流。"); const fakeStream = await createFakeStream(); return fakeStream ? fakeStream : originalGetUserMedia.apply(this, [constraints]); }
                return originalGetUserMedia.apply(this, [constraints]);
            };
            navigator.mediaDevices.getUserMedia.isPatchedByHelper = true;
        }

        function createFloatingWindow() {
            if (document.getElementById('exam-helper-container')) return;
            const container = document.createElement('div'); container.id = 'exam-helper-container';
            const header = document.createElement('div'); const floatingWindowBody = document.createElement('div'); floatingWindowBody.id = 'floating-window-body';
            const spoofButton = document.createElement('button'); const statusText = document.createElement('p');
            const fileInput = document.createElement('input'); const uploadLabel = document.createElement('label');
            function updateSpoofButton() { spoofButton.innerText = `摄像头伪装: ${isWebcamSpoofingEnabled ? '已开启' : '已关闭'}`; spoofButton.style.backgroundColor = isWebcamSpoofingEnabled ? '#dc3545' : '#6c757d'; }
            spoofButton.onclick = () => { isWebcamSpoofingEnabled = !isWebcamSpoofingEnabled; localStorage.setItem(STORAGE_KEY_SPOOF, isWebcamSpoofingEnabled); updateSpoofButton(); if (!isWebcamSpoofingEnabled) { if (animationInterval) clearInterval(animationInterval); cachedMediaStream = null; } };
            Object.assign(spoofButton.style, { color: 'white', padding: '8px 12px', borderRadius: '5px', cursor: 'pointer', border: 'none', display: 'block', width: '100%', marginBottom: '10px', transition: 'background-color 0.3s' });
            header.textContent = '华为云考试助手V1.3'; statusText.textContent = userImageDataBase64 ? '状态: 已加载照片' : '状态: 未上传照片'; statusText.style.cssText = 'text-align: center; font-size: 12px; color: #555; margin: 0;';
            fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.id = 'fake-cam-upload'; fileInput.style.display = 'none';
            uploadLabel.htmlFor = 'fake-cam-upload'; uploadLabel.textContent = '选择/更换照片';
            Object.assign(uploadLabel.style, { display: 'block', textAlign: 'center', backgroundColor: '#007bff', color: 'white', padding: '8px 12px', borderRadius: '5px', cursor: 'pointer', marginTop: '10px', transition: 'background-color 0.3s' });
            floatingWindowBody.append(spoofButton, statusText, fileInput, uploadLabel);
            container.append(header, floatingWindowBody); document.body.appendChild(container); updateSpoofButton();
            fileInput.addEventListener('change', (event) => {
                const file = event.target.files?.[0]; if (!file) return;
                const reader = new FileReader();
                reader.onload = (e) => { userImageDataBase64 = e.target.result; localStorage.setItem(STORAGE_KEY_IMG, userImageDataBase64); statusText.textContent = '状态: 照片已更新!'; cachedMediaStream = null; };
                reader.readAsDataURL(file);
            });
            Object.assign(container.style, { position: 'fixed', top: '20px', right: '20px', zIndex: '99999', backgroundColor: '#fff', border: '1px solid #ccc', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.2)', minWidth: '220px', fontFamily: 'sans-serif' });
            Object.assign(header.style, { padding: '10px', backgroundColor: '#f0f0f0', cursor: 'move', borderBottom: '1px solid #ccc', borderTopLeftRadius: '8px', borderTopRightRadius: '8px', fontWeight: 'bold', userSelect: 'none', textAlign: 'center' });
            Object.assign(floatingWindowBody.style, { padding: '12px' });
            let isDragging = false, offset = { x: 0, y: 0 };
            header.onmousedown = (e) => { isDragging = true; offset = { x: e.clientX - container.offsetLeft, y: e.clientY - container.offsetTop }; container.style.right = 'auto'; };
            document.onmousemove = (e) => { if (isDragging) { container.style.left = `${e.clientX - offset.x}px`; container.style.top = `${e.clientY - offset.y}px`; } };
            document.onmouseup = () => { isDragging = false; };
        }

        ensureWebcamHook();
        (function spaNavigationHandler() { const wrap = (m) => { const o = history[m]; history[m] = function(...a) { const r = o.apply(this, a); window.dispatchEvent(new Event(m.toLowerCase())); return r; }; }; wrap('pushState'); wrap('replaceState'); const reapply = () => { setTimeout(ensureWebcamHook, 100); }; window.addEventListener('popstate', reapply); window.addEventListener('pushstate', reapply); window.addEventListener('replacestate', reapply); })();
        if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', createFloatingWindow); } else { createFloatingWindow(); }
    })();


    /*
     * ===================================================================================
     * P3: 题库导出工具模块 (Question Exporter Module)
     * 职责: 通过劫持 XMLHttpRequest,捕获试卷数据,并提供一个“一键复制题库”的按钮到UI上,
     * 以便用户可以手动复制所有题目和选项。
     * ===================================================================================
    */

    (function questionExporter() {
        const targetUrlPart = '/svc/innovation/userapi/exam2d/so/servlet/getExamPaper';
        const originalXhrSend = XMLHttpRequest.prototype.send;

        /**
         * 将题目JSON数据格式化为易于阅读的文本。
         * @param {Array<Object>} questions - 题目对象数组。
         * @returns {string} - 格式化后的纯文本题库。
         */
        function formatQuestions(questions) {
            return questions.map((q, index) => {
                let typeStr = q.type === 0 ? '单选题' : q.type === 1 ? '多选题' : q.type === 2 ? '判断题' : '未知题';
                let questionText = `${index + 1}. [${typeStr}] ${q.content ? q.content.trim() : '无题干'}\n`;
                if (q.options?.length > 0) {
                    questionText += q.options.map(opt => `   ${opt.optionOrder ? opt.optionOrder + '. ' : '- '}${opt.optionContent ? opt.optionContent.trim() : '无选项内容'}`).join('\n');
                }
                return questionText;
            }).join('\n\n');
        }

        /**
         * 向UI面板添加“一键复制题库”按钮。
         * @param {string} formattedText - 已格式化好的题库文本。
         */
        function addCopyToPanel(formattedText) {
            const floatingWindowBody = document.getElementById('floating-window-body');
            // 如果按钮已存在,则不重复添加
            if (!floatingWindowBody || document.getElementById('copy-questions-btn')) return;

            const button = document.createElement('button');
            button.id = 'copy-questions-btn';
            button.innerText = '一键复制题库';
            Object.assign(button.style, {
                backgroundColor: '#28a745', color: 'white', padding: '8px 12px',
                borderRadius: '5px', cursor: 'pointer', border: 'none',
                display: 'block', width: '100%', marginTop: '10px'
            });

            button.onclick = () => {
                GM_setClipboard(formattedText, 'text');
                button.innerText = '复制成功!';
                button.style.backgroundColor = '#007bff';
                setTimeout(() => {
                    button.innerText = '一键复制题库';
                    button.style.backgroundColor = '#28a745';
                }, 2000);
            };

            floatingWindowBody.appendChild(button);
        }

        // 劫持 XHR
        XMLHttpRequest.prototype.send = function() {
            this.addEventListener('load', function() {
                if (this.readyState === 4 && this.status === 200 && this.responseURL.includes(targetUrlPart)) {
                    try {
                        const data = JSON.parse(this.responseText);
                        if (data?.result?.questions) {
                            console.log('[导出模块 P3] 成功截获题库数据,准备生成复制按钮。');
                            // 核心改造:直接调用函数在UI上生成按钮,而不是暴露全局变量
                            addCopyToPanel(formatQuestions(data.result.questions));
                        }
                    } catch (e) {
                        console.error('[导出模块 P3] 解析题库JSON失败:', e);
                    }
                }
            });
            return originalXhrSend.apply(this, arguments);
        };
    })();

})();