Greasy Fork

Greasy Fork is available in English.

优学院知识图谱

自动完成优学院知识图谱

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         优学院知识图谱
// @namespace    http://greasyfork.icu/zh-CN/users/953334
// @version      0.0.1
// @description  自动完成优学院知识图谱 
// @author       itsdapi
// @match        https://kg.ulearning.cn/pc.html#/stuLearn/*
// @icon         https://www.ulearning.cn/ulearning/favicon.ico
// @run-at       document-start
// @grant        unsafeWindow
// @license MIT
// ==/UserScript==
 
(function () {
    'use strict';
    const SPEED = 1
    // Intercept site requests to capture quiz list instead of sending our own
    const QUIZ_LIST_PATH = '/questionRelation/quizList';
    const quizListWaiters = new Map(); // key: knowledgeId -> Array<resolve>
    const latestQuizListWaiters = []; // Array<resolve> waiting for the latest quiz list regardless of knowledgeId

    function resolveWaiters(knowledgeId, data) {
        if (!knowledgeId) return;
        const key = String(knowledgeId);
        const waiters = quizListWaiters.get(key);
        if (!waiters || waiters.length === 0) return;
        quizListWaiters.delete(key);
        for (const resolve of waiters) {
            try { resolve(data); } catch (_) { /* noop */ }
        }
    }

    function resolveLatestWaiters(data) {
        if (!latestQuizListWaiters.length) return;
        const waiters = latestQuizListWaiters.splice(0, latestQuizListWaiters.length);
        for (const resolve of waiters) {
            try { resolve(data); } catch (_) { /* noop */ }
        }
    }

    function extractKnowledgeIdFromUrl(url) {
        try {
            const u = new URL(url, window.location.origin);
            return u.searchParams.get('knowledgeId');
        } catch (_) {
            return null;
        }
    }

    function notifyQuizListIfMatch(url, payload) {
        if (!url) return;
        // Accept any request hitting the quiz list path
        if (String(url).includes(QUIZ_LIST_PATH)) {
            const kid = extractKnowledgeIdFromUrl(url) || (payload && payload.knowledgeId) || null;
            const result = payload && (payload.result || payload.data || payload);
            resolveWaiters(kid, result);
            resolveLatestWaiters(result);
        }
    }

    // Patch fetch
    (function patchFetch() {
        if (!window.fetch) return;
        const originalFetch = window.fetch;
        window.fetch = function patchedFetch(input, init) {
            const url = (typeof input === 'string') ? input : (input && input.url);
            return originalFetch.apply(this, arguments).then((response) => {
                try {
                    const clone = response.clone();
                    clone.json().then((data) => {
                        try { notifyQuizListIfMatch(url, data); } catch (_) { /* noop */ }
                    }).catch(() => { /* not json, ignore */ });
                } catch (_) {
                    // Ignore clone/json issues
                }
                return response;
            });
        };
    })();

    main()

    async function main() {
        try {
            while (true) {
                await runKnowledgeQuiz()
                await sleep(3000)
                const nextBtn = findNextKnowledgeButton()
                if (!nextBtn) break
                nextBtn.click()
            }
        } catch (error) {
            console.log(error)
        }

    }

    // Patch XMLHttpRequest
    (function patchXHR() {
        if (!window.XMLHttpRequest) return;
        const openOrig = XMLHttpRequest.prototype.open;
        const sendOrig = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.open = function (method, url) {
            try { this.__tm_url = url; } catch (_) { /* noop */ }
            return openOrig.apply(this, arguments);
        };
        XMLHttpRequest.prototype.send = function (body) {
            try {
                this.addEventListener('load', () => {
                    try {
                        if (this.status >= 200 && this.status < 400) {
                            const url = this.__tm_url;
                            // Only attempt JSON parse if it looks like the endpoint
                            if (String(url || '').includes(QUIZ_LIST_PATH)) {
                                try {
                                    const data = JSON.parse(this.responseText);
                                    notifyQuizListIfMatch(url, data);
                                } catch (_) { /* ignore parse errors */ }
                            }
                        }
                    } catch (_) { /* noop */ }
                });
            } catch (_) { /* noop */ }
            return sendOrig.apply(this, arguments);
        };
    })();

    function waitLatestQuizListFromPage(timeoutMs = 45000) {
        return new Promise((resolve, reject) => {
            // push a wrapped resolver so we can clear the timer on resolve
            let wrappedResolve = null;
            const timer = setTimeout(() => {
                const idx = latestQuizListWaiters.indexOf(wrappedResolve);
                if (idx >= 0) latestQuizListWaiters.splice(idx, 1);
                reject(new Error('Timed out waiting for latest quiz list request.'));
            }, timeoutMs);
            wrappedResolve = (value) => {
                clearTimeout(timer);
                resolve(value);
            };
            latestQuizListWaiters.push(wrappedResolve);
        });
    }
 
 
    async function runKnowledgeQuiz() {
        await waitLoad()

        await sleep(1000)
        const quizButton = await waitForElement('button.ul-button.go-quiz', {
            timeout: 20000,
            predicate: (el) => (el.textContent || '').includes('去测验')
        })
        await sleep(1000)
        quizButton.click()

        const quizList = await getLatestQuizList();

        for (const quiz of (quizList && quizList.list) || []) {
            const questionUl = await waitForElement('.question-ul', {
                timeout: 20000
            });
            await sleep(1000)
            const currentLi = questionUl.querySelector('li.question-item:last-child');
            await sleep(1000)
            const tfngArea = currentLi.querySelector('.answer-area');
            const choiceList = currentLi.querySelector('.choice-list');
            if (tfngArea) {
                await handleTFNG(quiz, tfngArea);
            } else if (choiceList) {
                await handleSingleChoice(quiz, choiceList);
            } else if (currentLi.querySelectorAll('input.blank-item-input').length > 0) {
                await handleFillInTheBlank(quiz, currentLi);
            } else {
                
            }
            await sleep(1000)
            const submitButton = currentLi.querySelector('.submit-button')
            if (!submitButton || !((submitButton.textContent || '').includes('提交'))) {
                
            } else {
                await sleep(1000)
                submitButton.click()
            }
        }
    }
 
    function waitLoad() {
        return new Promise((res) => {
            setTimeout(res, 500)
        })
    }

    async function handleTFNG(quiz, answerAreaElement) {
        await sleep(1000)
        const answer = getAnswer(quiz);
        if (!answerAreaElement) return;
        // Normalize answer to string 'true' | 'false'
        const answerValue = String(answer).toLowerCase() === 'true' ? 'true' : 'false';
        // Prefer clicking the label that contains the matching input to ensure UI frameworks react
        const input = answerAreaElement.querySelector(`input.ul-radio__original[value="${answerValue}"]`);
        if (!input) return;
        const label = input.closest('label');
        if (label) {
            label.click();
        } else {
            // Fallback: click the input and dispatch common events
            input.click();
            input.dispatchEvent(new Event('input', { bubbles: true }));
            input.dispatchEvent(new Event('change', { bubbles: true }));
        }
    }

    async function handleSingleChoice(quiz, choiceListElement) {
        await sleep(300)
        const answer = getAnswer(quiz);
        if (!choiceListElement) return;
        // Answers typically A/B/C/D; normalize
        const value = String(answer).trim().toUpperCase();
        const input = choiceListElement.querySelector(`input.ul-radio__original[value="${value}"]`);
        if (!input) return;
        const label = input.closest('label');
        if (label) {
            label.click();
        } else {
            input.click();
            input.dispatchEvent(new Event('input', { bubbles: true }));
            input.dispatchEvent(new Event('change', { bubbles: true }));
        }
    }

    async function handleFillInTheBlank(quiz, fillInTheBlankElement) {
        await sleep(300)
        if (!fillInTheBlankElement) return;

        const inputs = Array.from(fillInTheBlankElement.querySelectorAll('input.blank-item-input'));
        if (inputs.length === 0) return;

        const rawAnswer = getAnswer(quiz);

        function normalizeAnswers(value) {
            if (Array.isArray(value)) return value.map(v => String(v).trim());
            if (value == null) return [];
            const str = String(value).trim();
            // JSON array string
            if ((str.startsWith('[') && str.endsWith(']')) || (str.startsWith('"') && str.endsWith('"'))) {
                try {
                    const parsed = JSON.parse(str);
                    if (Array.isArray(parsed)) return parsed.map(v => String(v).trim());
                    return [String(parsed).trim()];
                } catch (_) { /* fallthrough */ }
            }
            // Common separators: |, ||, comma variants, semicolons, '、'
            const parts = str.split(/\|\||\||,|,|;|;|、/).map(s => s.trim()).filter(Boolean);
            if (parts.length > 0) return parts;
            return [str];
        }

        const answers = normalizeAnswers(rawAnswer);

        for (let i = 0; i < inputs.length; i++) {
            const input = inputs[i];
            const value = answers[i] != null ? answers[i] : (answers.length === 1 ? answers[0] : '');
            if (value == null) continue;
            input.focus();
            input.value = value;
            input.dispatchEvent(new Event('input', { bubbles: true }));
            input.dispatchEvent(new Event('change', { bubbles: true }));
            input.blur();
            await sleep(100);
        }
    }

    function getAnswer(quiz) {
        return quiz.correctAnswer;
    }

    async function getLatestQuizList() {
        // Wait for the page's next/most recent quiz list request and reuse its response
        const result = await waitLatestQuizListFromPage(45000);
        return result;
    }

	async function sleep(ms) {
		const scaled = Math.max(0, Math.round(ms / SPEED));
		return new Promise(resolve => setTimeout(resolve, scaled));
	}
 
    function waitForElement(selector, options = {}) {
        const { timeout = 15000, predicate } = options
        return new Promise((resolve, reject) => {
            const tryFind = () => {
                const candidates = Array.from(document.querySelectorAll(selector))
                const match = predicate ? candidates.find(predicate) : candidates[0]
                if (match) {
                    cleanup()
                    resolve(match)
                }
            }
            const observer = new MutationObserver(() => tryFind())
            const observeTarget = document.documentElement || document
            observer.observe(observeTarget, { childList: true, subtree: true })
            const timeoutId = setTimeout(() => {
                cleanup()
                reject(new Error('waitForElement timeout'))
            }, timeout)
            function cleanup() {
                clearTimeout(timeoutId)
                observer.disconnect()
            }
            tryFind()
        })
    }

    function findNextKnowledgeButton() {
        const buttons = Array.from(document.querySelectorAll('button.ul-button'))
        return buttons.find(btn => (btn.textContent || '').includes('下一个知识点')) || null
    }
})();