KhanHack

Khan Academy Answer Hack

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         KhanHack
// @namespace    https://greasyfork.org/users/783447
// @version      7.1
// @description  Khan Academy Answer Hack
// @author       Logzilla6
// @match        https://*.khanacademy.org/*
// @icon         https://i.ibb.co/K5g1KMq/Untitled-drawing-3.png
// ==/UserScript==

//ALL FOLLOWING CODE IS UNDER THE KHANHACK TRADEMARK. UNAUTHORIZED DISTRIBUTION CAN/WILL RESULT IN LEGAL ACTION

//Note that KhanHack™ is an independent initiative and is not affiliated with or endorsed by Khan Academy. We respect the work of Khan Academy and its mission to provide free education, but KhanHack™ operates separately with its own unique goals.

let answerQueue = [];
let currentAnswerIndex = 0;
let isGhostMode = false;
let waitForData = false;

const createMenu = () => {
    const style = document.createElement('style');
    style.innerHTML = `
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;700&display=swap');

        #khanhack-menu {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 320px;
            height: 420px;
            background-color: #123576;
            border: 3px solid #07152e;
            border-radius: 16px;
            padding: 0;
            color: white;
            font-family: 'Noto Sans', sans-serif;
            z-index: 99999;
            box-shadow: 0 10px 25px rgba(0,0,0,0.5);
            transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
            display: flex;
            flex-direction: column;
            overflow: hidden;
            font-size: 14px;
            opacity: 1;
        }

        #khanhack-menu.ghost-active {
            opacity: 0;
            transition: opacity 0.3s ease;
        }

        #khanhack-menu.ghost-active:hover {
            opacity: 1;
        }

        #khanhack-menu.minimized {
            width: 48px;
            height: 48px;
            border-radius: 50%;
            cursor: pointer;
            padding: 0;
            justify-content: center;
            align-items: center;
            opacity: 0.8;
            border: 2px solid #07152e;
        }

        #khanhack-menu.minimized:hover {
            opacity: 1;
            transform: scale(1.05);
        }

        /* Header Section */
        .kh-header {
            display: flex;
            align-items: center;
            padding: 12px 16px;
            background-color: rgba(7, 21, 46, 0.3);
            border-bottom: 1px solid rgba(255,255,255,0.1);
        }

        .kh-header-title {
            font-weight: 700;
            font-size: 18px;
            letter-spacing: 0.5px;
            color: #fff;
            display: flex;
            align-items: center;
            gap: 8px;
            flex-grow: 1;
        }

        .kh-controls {
            display: flex;
            gap: 12px;
            align-items: center;
        }

        .kh-icon-btn {
            width: 24px;
            height: 24px;
            cursor: pointer;
            opacity: 0.8;
            transition: opacity 0.2s;
        }

        .kh-icon-btn:hover {
            opacity: 1;
        }

        .kh-content {
            flex: 1;
            padding: 15px;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
            gap: 10px;
            scrollbar-width: thin;
            scrollbar-color: rgba(255,255,255,0.3) transparent;
        }

        .kh-content::-webkit-scrollbar {
            width: 6px;
        }

        .kh-content::-webkit-scrollbar-thumb {
            background-color: rgba(255,255,255,0.3);
            border-radius: 3px;
        }

        .kh-placeholder {
            text-align: center;
            color: rgba(255,255,255,0.6);
            margin-top: 50%;
            transform: translateY(-50%);
            font-style: italic;
        }

        .kh-card {
            background: #f0f0f0;
            color: #1a1a1a;
            padding: 12px;
            border-radius: 8px;
            border-left: 5px solid #2967d9;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            word-wrap: break-word;
            font-weight: 500;
            font-size: 15px;
            animation: fadeIn 0.3s ease;
            margin-bottom: 8px;
            cursor: pointer;
            transition: transform 0.1s;
        }

        .kh-card:active {
            transform: scale(0.98);
        }

        .kh-card img {
            max-width: 100%;
            border-radius: 4px;
            margin-top: 5px;
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(5px); }
            to { opacity: 1; transform: translateY(0); }
        }

        .kh-footer {
            padding: 10px 15px;
            background-color: rgba(7, 21, 46, 0.5);
            border-top: 1px solid rgba(255,255,255,0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .kh-nav-btn {
            background: #2967d9;
            border: none;
            color: white;
            padding: 6px 12px;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 600;
            font-size: 12px;
            transition: background 0.2s;
        }

        .kh-nav-btn:hover {
            background: #3b7ced;
        }

        .kh-nav-btn:disabled {
            background: #1e3a6e;
            color: #aaa;
            cursor: not-allowed;
        }

        .kh-status {
            font-size: 12px;
            color: rgba(255,255,255,0.7);
        }

        .kh-toggle {
            display: none;
            width: 24px;
            height: 24px;
            filter: brightness(0) invert(1);
        }

        .kh-settings-view {
            display: none;
            flex-direction: column;
            align-items: center;
            gap: 15px;
            padding-top: 20px;
            animation: fadeIn 0.3s ease;
        }

        .kh-setting-item {
            display: flex;
            align-items: center;
            gap: 10px;
            font-size: 16px;
        }

        .kh-beta-tag {
            background: #2967d9;
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 10px;
            font-weight: bold;
        }

        .kh-toggle-switch {
            position: relative;
            display: inline-block;
            width: 40px;
            height: 20px;
        }

        .kh-toggle-switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .kh-slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #07152e;
            transition: .4s;
            border-radius: 20px;
            border: 1px solid white;
        }

        .kh-slider:before {
            position: absolute;
            content: "";
            height: 14px;
            width: 14px;
            left: 3px;
            bottom: 2px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }

        input:checked + .kh-slider {
            background-color: #2967d9;
        }

        input:checked + .kh-slider:before {
            transform: translateX(20px);
        }

        #khanhack-menu.minimized .kh-header,
        #khanhack-menu.minimized .kh-content,
        #khanhack-menu.minimized .kh-footer {
            display: none;
        }

        #khanhack-menu.minimized .kh-toggle {
            display: block;
        }

        .kh-hidden {
            display: none !important;
        }
    `;
    document.head.appendChild(style);

    const menu = document.createElement('div');
    menu.id = 'khanhack-menu';

    const toggleIcon = document.createElement('img');
    toggleIcon.src = 'https://i.ibb.co/RpqPcR1/hamburger.png';
    toggleIcon.className = 'kh-toggle';

    const header = document.createElement('div');
    header.className = 'kh-header';

    const discordIcon = document.createElement('img');
    discordIcon.src = 'https://i.ibb.co/grF973h/discord.png';
    discordIcon.className = 'kh-icon-btn';
    discordIcon.style.marginRight = '10px';
    discordIcon.onclick = () => window.open('https://discord.gg/7xWK5n9Zbp', '_blank');

    const title = document.createElement('div');
    title.id = 'kh-title-text';
    title.className = 'kh-header-title';
    title.innerText = 'KhanHack';

    const controls = document.createElement('div');
    controls.className = 'kh-controls';

    const settingsIcon = document.createElement('img');
    settingsIcon.src = 'https://i.ibb.co/q0QVKGG/gearicon.png';
    settingsIcon.className = 'kh-icon-btn';
    settingsIcon.title = 'Settings';

    const collapseBtn = document.createElement('span');
    collapseBtn.innerHTML = '−';
    collapseBtn.className = 'kh-icon-btn';
    collapseBtn.style.fontSize = '24px';
    collapseBtn.style.lineHeight = '20px';
    collapseBtn.style.fontWeight = 'bold';
    collapseBtn.style.textAlign = 'center';
    collapseBtn.title = 'Minimize';
    collapseBtn.onclick = (e) => {
        e.stopPropagation();
        toggleMenu();
    };

    controls.appendChild(settingsIcon);
    controls.appendChild(collapseBtn);

    header.appendChild(discordIcon);
    header.appendChild(title);
    header.appendChild(controls);

    const content = document.createElement('div');
    content.className = 'kh-content';

    const answerView = document.createElement('div');
    answerView.id = 'kh-answer-view';

    const placeholder = document.createElement('div');
    placeholder.className = 'kh-placeholder';
    placeholder.id = 'kh-placeholder-text';
    placeholder.innerText = 'Waiting for questions...';

    const contentBox = document.createElement('div');
    contentBox.id = 'kh-content-box';

    answerView.appendChild(placeholder);
    answerView.appendChild(contentBox);

    const settingsView = document.createElement('div');
    settingsView.className = 'kh-settings-view';
    settingsView.id = 'kh-settings-view';

    const ghostItem = document.createElement('div');
    ghostItem.className = 'kh-setting-item';

    const ghostLabel = document.createElement('span');
    ghostLabel.innerText = "Ghost Mode:";

    const ghostSwitch = document.createElement('label');
    ghostSwitch.className = 'kh-toggle-switch';
    const ghostInput = document.createElement('input');
    ghostInput.type = 'checkbox';
    ghostInput.onchange = (e) => toggleGhostMode(e.target.checked);
    const ghostSlider = document.createElement('span');
    ghostSlider.className = 'kh-slider';

    ghostSwitch.appendChild(ghostInput);
    ghostSwitch.appendChild(ghostSlider);
    ghostItem.appendChild(ghostLabel);
    ghostItem.appendChild(ghostSwitch);

    const autoAnsItem = document.createElement('div');
    autoAnsItem.className = 'kh-setting-item';
    autoAnsItem.innerHTML = 'Auto Answer <span class="kh-beta-tag">BETA</span>';

    const pointFarmerItem = document.createElement('div');
    pointFarmerItem.className = 'kh-setting-item';
    pointFarmerItem.innerHTML = 'Point Farmer <span class="kh-beta-tag">BETA</span>';

    const versionText = document.createElement('div');
    versionText.style.marginTop = 'auto';
    versionText.style.opacity = '0.5';
    versionText.style.fontSize = '12px';
    versionText.innerText = 'KhanHack™ | 7.1';

    settingsView.appendChild(ghostItem);
    settingsView.appendChild(autoAnsItem);
    settingsView.appendChild(pointFarmerItem);
    settingsView.appendChild(versionText);

    content.appendChild(answerView);
    content.appendChild(settingsView);

    const footer = document.createElement('div');
    footer.className = 'kh-footer';
    footer.id = 'kh-footer';

    const prevBtn = document.createElement('button');
    prevBtn.className = 'kh-nav-btn';
    prevBtn.innerText = '◄ Prev';
    prevBtn.id = 'kh-prev-btn';
    prevBtn.onclick = () => navigateAnswer(-1);

    const statusText = document.createElement('span');
    statusText.className = 'kh-status';
    statusText.id = 'kh-status-text';
    statusText.innerText = '0 / 0';

    const nextBtn = document.createElement('button');
    nextBtn.className = 'kh-nav-btn';
    nextBtn.innerText = 'Next ►';
    nextBtn.id = 'kh-next-btn';
    nextBtn.onclick = () => navigateAnswer(1);

    footer.appendChild(prevBtn);
    footer.appendChild(statusText);
    footer.appendChild(nextBtn);

    menu.appendChild(toggleIcon);
    menu.appendChild(header);
    menu.appendChild(content);
    menu.appendChild(footer);
    document.body.appendChild(menu);

    let isMinimized = false;
    let isSettingsOpen = false;

    settingsIcon.onclick = () => {
        isSettingsOpen = !isSettingsOpen;
        if (isSettingsOpen) {
            answerView.classList.add('kh-hidden');
            footer.classList.add('kh-hidden');
            settingsView.style.display = 'flex';
            title.innerText = "Settings";
            discordIcon.classList.add('kh-hidden');
            settingsIcon.style.opacity = '1';
        } else {
            answerView.classList.remove('kh-hidden');
            footer.classList.remove('kh-hidden');
            settingsView.style.display = 'none';
            title.innerText = "KhanHack";
            discordIcon.classList.remove('kh-hidden');
        }
    };

    const toggleMenu = () => {
        isMinimized = !isMinimized;
        if (isMinimized) {
            menu.classList.add('minimized');
        } else {
            menu.classList.remove('minimized');
        }
    };

    const toggleGhostMode = (active) => {
        isGhostMode = active;
        if (active) {
            menu.classList.add('ghost-active');
        } else {
            menu.classList.remove('ghost-active');
        }
    };

    menu.addEventListener('click', (e) => {
        if (isMinimized) toggleMenu();
    });

    updateNavState();
};

const updateMenuContent = () => {
    const contentBox = document.getElementById('kh-content-box');
    const placeholder = document.getElementById('kh-placeholder-text');

    if (answerQueue.length === 0) {
        if(placeholder) placeholder.style.display = 'block';
        return;
    }

    if(placeholder) placeholder.style.display = 'none';

    contentBox.innerHTML = '';

    const currentAnsArray = answerQueue[currentAnswerIndex];

    if (currentAnsArray && Array.isArray(currentAnsArray)) {
        currentAnsArray.forEach(rawAns => {
            const currentAns = String(rawAns);

            const card = document.createElement('div');
            card.className = 'kh-card';

            if (currentAns.startsWith('[Image]')) {
                const parts = currentAns.match(/\[Image\] (.*)\((.*)\)/);
                if (parts) {
                    if (parts[1].trim()) {
                        const textP = document.createElement('div');
                        textP.innerText = parts[1].trim();
                        card.appendChild(textP);
                    }
                    const img = document.createElement('img');
                    img.src = parts[2];
                    card.appendChild(img);
                } else {
                    card.innerText = currentAns;
                }
            } else {
                card.innerText = currentAns;
            }

            card.title = "Click to copy";
            card.onclick = () => {
                navigator.clipboard.writeText(currentAns.replace(/\[Image\] .*/, 'Image copied'));

                const originalBg = card.style.backgroundColor;
                card.style.backgroundColor = '#d1e7dd';

                setTimeout(() => {
                    card.style.backgroundColor = originalBg;
                }, 200);
            };

            contentBox.appendChild(card);
        });
    }

    updateNavState();
};

const updateNavState = () => {
    const prevBtn = document.getElementById('kh-prev-btn');
    const nextBtn = document.getElementById('kh-next-btn');
    const statusText = document.getElementById('kh-status-text');

    if (prevBtn && nextBtn && statusText) {
        statusText.innerText = `${answerQueue.length > 0 ? currentAnswerIndex + 1 : 0} / ${answerQueue.length}`;

        prevBtn.disabled = currentAnswerIndex <= 0;
        nextBtn.disabled = currentAnswerIndex >= answerQueue.length - 1;
    }
};

const navigateAnswer = (direction) => {
    const newIndex = currentAnswerIndex + direction;
    if (newIndex >= 0 && newIndex < answerQueue.length) {
        currentAnswerIndex = newIndex;
        updateMenuContent();
    }
};

const addAnswerToQueue = (answerGroup) => {
    answerQueue.push(answerGroup);

    if (answerQueue.length === 1) {
        currentAnswerIndex = 0;
        updateMenuContent();
        waitForData = false;
    } else if (waitForData) {
        navigateAnswer(1);
        waitForData = false;
    } else {
        updateNavState();
    }
};

const setupAutoAdvance = () => {
    document.addEventListener('click', (e) => {
        const target = e.target.closest('button');
        if (!target) return;

        const testId = target.getAttribute('data-testid');
        const ariaLabel = target.getAttribute('aria-label');
        const text = target.innerText || '';
        if (
            testId === 'exercise-next-question-button' ||
            (ariaLabel && ariaLabel.includes('Next question')) ||
            text.includes('Next question')
        ) {
            setTimeout(() => {
                navigateAnswer(1);
            }, 200);
        }
    });

    document.addEventListener('keyup', (e) => {
        if (e.key === 'Enter') {
            waitForData = true;
        }
    });
};

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
        createMenu();
        setupAutoAdvance();
    });
} else {
    createMenu();
    setupAutoAdvance();
}

let originalJson = JSON.parse;

JSON.parse = function (jsonString) {
    let parsedData = originalJson(jsonString);

    try {
        if (parsedData.data) {
            let assessmentItem = parsedData.data.assessmentItemById || parsedData.data.assessmentItemByProblemNumber;

            if (assessmentItem && assessmentItem.item) {
                let itemData = JSON.parse(assessmentItem.item.itemData);
                let hasGradedWidget = Object.values(itemData.question.widgets).some(widget => widget.graded === true);

                if (hasGradedWidget) {
                    const currentItemAnswers = [];

                    for (let widgetKey in itemData.question.widgets) {
                        let widget = itemData.question.widgets[widgetKey];

                        if (widgetHandlers[widget.type]) {
                            const answer = widgetHandlers[widget.type](widget);
                            if (answer !== undefined && answer !== null) {
                                if (Array.isArray(answer)) {
                                    currentItemAnswers.push(...answer);
                                } else {
                                    currentItemAnswers.push(answer);
                                }
                            }
                        }
                    }

                    if (currentItemAnswers.length > 0) {
                         addAnswerToQueue(currentItemAnswers);
                    }
                }
            }
        }
    } catch (error) {
    }

    return parsedData;
};

const cleanLatex = (text) => {
    if (typeof text !== 'string') return text;
    return text
        .replace(/\\begin{align}/g, '\\begin{aligned}')
        .replace(/\\end{align}/g, '\\end{aligned}')
        .replace(/\$/g, '');
};

const widgetHandlers = {
    "numeric-input": (w) => w.options.answers[0].value,

    "input-number": (w) => w.options.value,

    "dropdown": (w) => w.options.choices.find(c => c.correct)?.content,

    "expression": (w) => cleanLatex(w.options.answerForms[0].value),

    "matcher": (w) => cleanLatex(w.options.right),

    "grapher": (w) => w.options.correct.coords.join(' | '),

    "interactive-graph": (w) => {
        return w.options.correct.coords
            .filter(c => c !== undefined)
            .join(' | ');
    },

    "categorizer": (w) => {
        return w.options.values
            .map(v => w.options.categories[v])
            .join(", ");
    },

    "matrix": (w) => w.options.answers.join(' | '),

    "label-image": (w) => {
        const markers = w.options.markers;
        return markers
            .filter(m => m.answers)
            .map((m, i) => {
                const label = m.label ? m.label.replace('Point ', '').replace(/\./g, '').trim() : '';
                const cleanAns = cleanLatex(m.answers.toString());
                return label ? `${label}: ${cleanAns}` : cleanAns;
            })
            .join(" | ");
    },

    "radio": (w) => {
        const choices = w.options.choices;
        const isNone = choices.some(c => c.isNoneOfTheAbove && c.correct);
        if (isNone) return "None of the above";

        return choices
            .filter(c => c.correct)
            .flatMap(c => {
                const content = c.content;
                const imageMatch = content.match(/!\[([^\]]*)\]\(([^)]+)\)/);

                if (imageMatch) {
                    let text = imageMatch[1];
                    let url = imageMatch[2];

                    if (url.includes('web+graphie')) {
                        url = url.replace('web+graphie:', 'https:') + '.svg';
                    }

                    const result = [`[Image] (${url})`];
                    if (text && text.trim().length > 0) {
                        result.push(text.trim());
                    }
                    return result;
                }

                return cleanLatex(content);
            });
    }
};