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).
// ==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);
})();