🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

多平台问卷AI自动填写,问卷星、腾讯问卷、飞书通用

Extracts DOM structure, sends it to Gemini AI for semantic analysis, and simulates human input to fill forms. Supports custom user profiles and works on modern frameworks (React/Vue).

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         多平台问卷AI自动填写,问卷星、腾讯问卷、飞书通用
// @name:en-US   Multi-Platform Questionnaire AI Auto-Filler
// @namespace    https://github.com/kelryry
// @version      1.1
// @description  Extracts DOM structure, sends it to Gemini AI for semantic analysis, and simulates human input to fill forms. Supports custom user profiles and works on modern frameworks (React/Vue).
// @description:zh-CN 提取页面DOM结构,发送给Gemini AI进行语义分析,并模拟人类输入进行填表。支持自定义用户画像,兼容现代前端框架(React/Vue)。
// @author       kelryry
// @license      GPL-3.0-only
// @homepageURL  https://github.com/kelryry/questionnaire-auto-filling
// @match        https://example.com/replace-this-with-your-target-url/*
// @match        https://docs.qq.com/form/*
// @match        https://*.wjx.cn/*
// @match        https://*.wjx.top/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @connect      generativelanguage.googleapis.com
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // =========================================================================
    // [USER CONFIGURATION] - MUST BE CONFIGURED BEFORE USE
    // =========================================================================
    const CONFIG = {
        // 1. Google Gemini API Key (Required)
        // Get yours at: https://aistudio.google.com/
        apiKey: "YOUR_GEMINI_API_KEY_HERE",

        // 2. Model Selection
        // Recommended: "gemini-2.5-flash-lite" for speed, "gemini-2.5-flash" for balance.
        modelName: "gemini-2.5-flash-lite",

        // 3. Thinking Budget
        // Set to 0 for maximum speed (Flash/Lite).
        // For "Pro" models, you can set a budget (e.g., 1024) if reasoning is needed.
        thinkingBudget: 0,

        // 4. Auto Submit Configuration
        // autoSubmit: If true, clicks the submit button automatically.
        // submitDelay: Delay in milliseconds before clicking submit.
        autoSubmit: false,
        submitDelay: 1000,

        // 5. Scheduled Execution
        // targetTime: Format "YYYY-MM-DD HH:MM:SS". If in the past, runs immediately.
        // preLoadOffset: Milliseconds to start scanning before the target time (counteract network latency).
        targetTime: "2025-12-03 15:00:00",
        preLoadOffset: 500,

        // 6. User Profile
        // The AI will use this information to answer questions.
        userProfile: `
            Name: John Doe
            Phone: 13800138000
            ID Card: 110101199001011234
            Email: [email protected]
            Address: Chaoyang District, Beijing
            Education: Bachelor
            Occupation: Developer
            Note: None
            Agree to Terms: Yes/Agree
        `,

        // 7. Debug Mode
        // If true, prints the HTML payload and AI plan to the console.
        debug: true
    };

    // =========================================================================
    // [UI & STATUS MANAGEMENT]
    // =========================================================================
    let statusDiv = null;
    let elementMap = new Map();

    // Initialize the floating status bar
    function initUI() {
        if (statusDiv) return;
        statusDiv = document.createElement('div');
        statusDiv.style.cssText = `
            position: fixed; top: 10px; right: 10px; z-index: 2147483647;
            background: rgba(0,0,0,0.8); color: #00ff00; padding: 8px 12px;
            border-radius: 4px; font-family: sans-serif; font-size: 12px;
            pointer-events: none; user-select: none; transition: all 0.2s;
        `;
        statusDiv.innerText = '🤖 Gemini: Standby';
        document.body.appendChild(statusDiv);
    }

    // Update status text and color
    function updateStatus(text, color = '#00ff00') {
        if (!statusDiv) initUI();
        statusDiv.style.color = color;
        statusDiv.innerText = `🤖 ${text}`;
        console.log(`[Gemini] ${text}`);
    }

    // =========================================================================
    // [CORE UTILS: INPUT SIMULATION]
    // Handles React/Vue/Angular state hijacking issues
    // =========================================================================

    function simulateInput(element, value) {
        if (!element) return;
        element.focus();

        const tag = element.tagName.toLowerCase();
        let proto;

        // Determine the correct prototype to bypass framework proxies
        if (tag === 'textarea') {
            proto = window.HTMLTextAreaElement.prototype;
        } else if (tag === 'select') {
            proto = window.HTMLSelectElement.prototype;
        } else {
            proto = window.HTMLInputElement.prototype;
        }

        // Try to call the native value setter
        try {
            const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value").set;
            if (nativeSetter) {
                nativeSetter.call(element, value);
            } else {
                element.value = value; // Fallback
            }
        } catch (e) {
            console.warn(`Native setter failed for ${tag}, fallback to direct assignment.`, e);
            element.value = value;
        }

        // Dispatch events to ensure the framework detects the change
        const eventTypes = ['input', 'change', 'blur', 'focusout'];
        eventTypes.forEach(type => {
            element.dispatchEvent(new Event(type, { bubbles: true }));
        });
    }

    function simulateClick(element) {
        if (!element) return;
        try {
            // Scroll to view to ensure visibility
            element.scrollIntoView({ behavior: 'auto', block: 'center' });

            // Standard click
            element.click();

            // Additional events for stubborn elements
            element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
            element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
        } catch (e) {
            console.error("Click failed", e);
        }
    }

    // =========================================================================
    // [DOM EXTRACTION]
    // Converts HTML to a simplified format for the LLM to save tokens
    // =========================================================================

    function isInteractive(el) {
        const tag = el.tagName.toLowerCase();
        if (['input', 'textarea', 'select', 'button'].includes(tag)) return true;

        // Detect custom styled radio/checkboxes (divs acting as buttons)
        const style = window.getComputedStyle(el);
        return style.cursor === 'pointer';

    }

    function generateSimplifiedDOM(root) {
        elementMap.clear();
        let idCounter = 0;
        let output = [];

        function traverse(node, depth) {
            // Skip invisible elements or non-element nodes
            if (!node || node.nodeType !== 1 ||
                node.offsetWidth <= 0 && node.offsetHeight <= 0 && node.tagName !== 'OPTION') {
                return;
            }

            const tag = node.tagName.toLowerCase();
            // Skip irrelevant tags
            if (['script', 'style', 'svg', 'path', 'noscript', 'meta', 'link'].includes(tag)) return;

            // Extract direct text content
            let directText = "";
            node.childNodes.forEach(child => {
                if (child.nodeType === 3) directText += child.textContent.trim() + " ";
            });
            directText = directText.trim();

            const placeholder = node.getAttribute('placeholder');
            const ariaLabel = node.getAttribute('aria-label');
            const type = node.getAttribute('type');

            const interactive = isInteractive(node);

            // Record the node if it's interactive, has text, or is a label
            if (interactive || directText || placeholder || ariaLabel || tag === 'label') {
                const myId = `el_${idCounter++}`;
                elementMap.set(myId, node);

                const indent = "  ".repeat(depth);
                let line = `${indent}<${tag}`;

                // Assign ID only to interactive elements for the AI to reference
                if (interactive) line += ` _ai_id="${myId}"`;

                if (type) line += ` type="${type}"`;
                if (placeholder) line += ` placeholder="${placeholder}"`;
                if (ariaLabel) line += ` label="${ariaLabel}"`;

                line += ">";
                if (directText) line += ` ${directText}`;

                // Indicate state for checkboxes/radios
                if (tag === 'input' && (type === 'radio' || type === 'checkbox')) {
                    line += node.checked ? " [CHECKED]" : "";
                }

                output.push(line);
            }

            Array.from(node.children).forEach(child => traverse(child, depth + 1));
        }

        traverse(root, 0);
        return output.join("\n");
    }

    // =========================================================================
    // [GEMINI AGENT LOGIC]
    // =========================================================================

    async function runAgent() {
        updateStatus("Scanning Page...", "yellow");

        // 1. Snapshot DOM
        const simplifiedHTML = generateSimplifiedDOM(document.body);

        if(CONFIG.debug) {
            console.log("--- Payload Sent to Gemini ---");
            console.log(simplifiedHTML);
        }

        // 2. Construct System Prompt
        const prompt = `
        You are an auto-filling agent.
        Your goal: Fill the form based on User Profile.

        User Profile:
        ${CONFIG.userProfile}

        Simplified HTML Structure:
        ${simplifiedHTML}

        Instructions:
        1. Analyze the structure to link Labels with Inputs (based on indentation/hierarchy).
        2. Output a JSON plan.
        3. Use "fill" for inputs/textareas/selects.
        4. Use "click" for radio options, checkboxes, and buttons.
        5. IMPORTANT: For "fill", output the string value.
        6. IMPORTANT: For "click", choose the element that looks like the option text (e.g. the div containing "Male").
        
        Response JSON Schema (Array of objects):
        [
          {"id": "el_xxx", "action": "fill", "value": "my value"},
          {"id": "el_yyy", "action": "click", "reason": "Select Gender"}
        ]
        `;

        updateStatus("Gemini Thinking...", "#00ffff");

        try {
            const url = `https://generativelanguage.googleapis.com/v1beta/models/${CONFIG.modelName}:generateContent?key=${CONFIG.apiKey}`;

            // Construct API Payload
            const payload = {
                contents: [{ parts: [{ text: prompt }] }],
                generationConfig: {
                    responseMimeType: "application/json",
                    // Only apply thinkingConfig for non-lite models with budget > 0
                    thinkingConfig: CONFIG.modelName.includes('2.5') && !CONFIG.modelName.includes('lite') && CONFIG.thinkingBudget > 0
                        ? { thinkingBudget: CONFIG.thinkingBudget } : undefined
                }
            };

            const response = await fetch(url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            });

            const data = await response.json();

            if (data.error) {
                throw new Error(data.error.message);
            }

            const planText = data.candidates[0].content.parts[0].text;
            const plan = JSON.parse(planText);

            console.log("Gemini Plan:", plan);
            await executePlan(plan);

        } catch (e) {
            console.error(e);
            updateStatus(`Error: ${e.message}`, "red");
        }
    }

    async function executePlan(plan) {
        updateStatus(`Executing ${plan.length} actions...`, "#00ff00");

        let submitBtn = null;

        for (const step of plan) {
            const el = elementMap.get(step.id);
            if (!el) continue;

            // Identify submit buttons but do not click immediately
            const text = (el.innerText || el.value || "").toLowerCase();
            if (step.action === 'click' && (text.includes('提交') || text.includes('submit') || text.includes('下一步'))) {
                submitBtn = el;
                continue;
            }

            if (step.action === 'fill') {
                simulateInput(el, step.value);
            } else if (step.action === 'click') {
                simulateClick(el);
            }

            // Small delay to prevent freezing the UI
            await new Promise(r => setTimeout(r, 20));
        }

        finalize(submitBtn);
    }

    function finalize(submitBtn) {
        // Validation: Highlight missing required fields
        const inputs = document.querySelectorAll('input[required], textarea[required]');
        let missing = false;
        inputs.forEach(el => {
            if (!el.value) {
                el.style.boxShadow = "0 0 10px red";
                el.style.border = "2px solid red";
                missing = true;
            }
        });

        if (missing) {
            updateStatus("Found missing fields!", "red");
            return;
        }

        if (submitBtn) {
            if (CONFIG.autoSubmit) {
                updateStatus(`Submitting in ${CONFIG.submitDelay}ms...`, "orange");
                setTimeout(() => submitBtn.click(), CONFIG.submitDelay);
            } else {
                // Highlight submit button for manual confirmation
                submitBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
                submitBtn.style.border = "4px solid #00ff00";
                updateStatus("Ready to Submit", "white");
            }
        } else {
            updateStatus("Finished (No submit btn)", "white");
        }
    }

    // =========================================================================
    // [ENTRY POINT]
    // =========================================================================

    function main() {
        initUI();

        // Pre-warm connection to Google API
        fetch(`https://generativelanguage.googleapis.com/v1beta/models/${CONFIG.modelName}?key=${CONFIG.apiKey}`, {method: 'HEAD'}).catch(()=>{});

        const now = Date.now();
        const target = new Date(CONFIG.targetTime).getTime();

        if (target > now) {
            const waitTime = target - now - CONFIG.preLoadOffset;
            updateStatus(`Wait ${(waitTime/1000).toFixed(1)}s`, "white");

            // Countdown timer
            const timer = setInterval(() => {
                const n = Date.now();
                if (n >= target - CONFIG.preLoadOffset) {
                    clearInterval(timer);
                    runAgent();
                } else {
                    statusDiv.innerText = `🤖 Wait: ${((target - CONFIG.preLoadOffset - n)/1000).toFixed(1)}s`;
                }
            }, 100);
        } else {
            runAgent();
        }
    }

    // Register menu command
    GM_registerMenuCommand("🚀 Run Auto-Filler", main);
    window.addEventListener('load', main);

})();