您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
One-click AI autofill for Google Forms using Gemini with optional one-time instruction override + quick cleaner
// ==UserScript== // @name Google Forms: AI Form Filler (Gemini) // @namespace https://www.fiverr.com/web_coder_nsd // @version 1.1.2 // @icon https://cdn-icons-png.flaticon.com/512/720/720311.png // @description One-click AI autofill for Google Forms using Gemini with optional one-time instruction override + quick cleaner // @match https://docs.google.com/forms/*/viewform // @grant none // @license MIT // ==/UserScript== (() => { // UI buttons const btn = document.createElement('button'); // Run const btnEdit = document.createElement('button'); // One-time prompt const btnClean = document.createElement('button'); // Clean all Object.assign(btn.style, { position: 'fixed', right: '16px', top: '25px', zIndex: 2147483647, width: '44px', height: '44px', borderRadius: '50%', border: '0', background: '#111', color: '#fff', fontSize: '14px', lineHeight: '44px', textAlign: 'center', cursor: 'pointer', boxShadow: '0 6px 16px rgba(0,0,0,.28)', userSelect: 'none' }); btn.textContent = '▶'; document.documentElement.appendChild(btn); Object.assign(btnEdit.style, { position: 'fixed', right: '9px', top: '19px', zIndex: 2147483647, width: '24px', height: '24px', borderRadius: '50%', border: '0', background: '#444', color: '#fff', fontSize: '17px', lineHeight: '14px', textAlign: 'center', cursor: 'pointer', boxShadow: '0 6px 16px rgba(0,0,0,.28)', userSelect: 'none' }); btnEdit.title = 'Add one-time instructions'; btnEdit.textContent = '✎'; document.documentElement.appendChild(btnEdit); Object.assign(btnClean.style, { position: 'fixed', right: '16px', top: '72px', zIndex: 2147483647, width: '44px', height: '44px', borderRadius: '50%', border: '0', background: '#7a1d1d', color: '#fff', fontSize: '18px', lineHeight: '44px', textAlign: 'center', cursor: 'pointer', boxShadow: '0 6px 16px rgba(0,0,0,.28)', userSelect: 'none' }); btnClean.title = 'Clear all inputs'; btnClean.textContent = '🧹'; document.documentElement.appendChild(btnClean); // one-time extra instructions, cleared after each run let sessionExtraInstructions = ''; btnEdit.addEventListener('click', () => { const next = prompt('One-time instructions for this run only.\nExample: "email should be [email protected]"'); if (next == null) return; sessionExtraInstructions = (next || '').trim(); btnEdit.style.background = sessionExtraInstructions ? '#0a7' : '#444'; }); // drag all three as a group (drag using main ▶ button) let drag = { x: 0, y: 0, sx: 0, sy: 0, down: false }; const onDown = (e) => { drag.down = true; drag.sx = e.clientX; drag.sy = e.clientY; const r = btn.getBoundingClientRect(); drag.x = r.left; drag.y = r.top; e.preventDefault(); }; const onMove = (e) => { if (!drag.down) return; const nx = drag.x + (e.clientX - drag.sx); const ny = drag.y + (e.clientY - drag.sy); [ [btn, 0], [btnClean, 47] ].forEach(([el, dy]) => { el.style.left = nx + 'px'; el.style.top = (ny + dy) + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; }); // the small ✎ button sits near the ▶ button btnEdit.style.left = (nx + 28) + 'px'; btnEdit.style.top = (ny - 6) + 'px'; btnEdit.style.right = 'auto'; btnEdit.style.bottom = 'auto'; }; const onUp = () => { drag.down = false; }; btn.addEventListener('mousedown', onDown); window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); const sleep = (ms) => new Promise(r => setTimeout(r, ms)); // Gemini client class GeminiClient { constructor() { this.baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models'; this.primaryModel = 'gemini-2.0-flash'; this.fallbackModel = 'gemini-1.5-flash'; this.apiKey = null; } async init() { if (!this.apiKey) { let k = localStorage.getItem('gemini_api_key'); if (!k) { k = prompt('Enter Gemini API key'); if (!k) throw new Error('API key is required'); localStorage.setItem('gemini_api_key', k.trim()); } this.apiKey = k.trim(); } } async fetchFromModel(model, prompt) { const res = await fetch(`${this.baseUrl}/${model}:generateContent?key=${this.apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) }); return res; } async generateContent(prompt) { if (!this.apiKey) await this.init(); let response = await this.fetchFromModel(this.primaryModel, prompt); if (response.status === 429) response = await this.fetchFromModel(this.fallbackModel, prompt); if (!response.ok) throw new Error(`HTTP error ${response.status}`); const data = await response.json(); return data.candidates?.[0]?.content?.parts?.[0]?.text || ''; } } // DOM helpers const getClosestListItem = (el) => el.closest('[role="listitem"]') || el.closest('.Qr7Oae') || el.closest('.geS5n'); const getHeadingText = (container) => { let t = ''; if (!container) return t; const heading = container.querySelector('[role="heading"] .M7eMe') || container.querySelector('[role="heading"]') || container.querySelector('.HoXoMd .M7eMe'); if (heading) t = heading.textContent.trim(); return t || (container.querySelector('label, .aDTYNe, .OIC90c')?.textContent?.trim() || ''); }; const plain = (s) => (s || '').replace(/\s+/g, ' ').trim(); const uid = () => Math.random().toString(36).slice(2, 10); const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n)); const isVisible = (el) => !!(el && el.offsetParent !== null); // prompt builder function buildPrompt(schema, oneTimeExtra) { const now = new Date().toISOString(); const extraBlock = oneTimeExtra ? `\nUser instructions for this run only. If they conflict, override defaults:\n${oneTimeExtra}\n` : ''; return ` You are a data faker for autofilling Google Forms. Decide realistic but safe dummy values from the provided schema. Rules: - Only use provided options for radio, checkbox, and select. - For "date" fields, return ISO YYYY-MM-DD. Respect "min" and "max" if given, otherwise pick within ±365 days from today. - For "time" fields, return {"hour":1-12,"minute":0-59,"meridiem":"AM|PM"}. - Keep text answers short and plausible. Email looks like a real address. - Return STRICT JSON only. No markdown. ${extraBlock} Schema (JSON): ${JSON.stringify(schema, null, 2)} Output JSON array of objects, each: { "id": "<schema.id>", "value": <string | string[] | {"hour":number,"minute":number,"meridiem":string}> } Now generate values. Current time: ${now}. `; } // safe JSON function safeJson(txt) { const cleaned = txt.trim().replace(/^```(?:json)?/i, '').replace(/```$/, '').trim(); try { return JSON.parse(cleaned); } catch (e) { const m = cleaned.match(/\[[\s\S]*\]|\{[\s\S]*\}/); if (m) return JSON.parse(m[0]); throw e; } } // setters function setNativeValue(el, value) { let proto = el; let setter = null; while ((proto = Object.getPrototypeOf(proto)) && !setter) { const d = Object.getOwnPropertyDescriptor(proto, 'value'); if (d && typeof d.set === 'function') setter = d.set; } if (setter) setter.call(el, value); else el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } // Select helpers function clickOptionInListbox(listbox, wanted) { const norm = (s) => (s || '').replace(/\s+/g, ' ').trim().toLowerCase(); const open = () => { if (listbox.getAttribute('aria-expanded') === 'false') listbox.click(); }; const close = () => { if (listbox.getAttribute('aria-expanded') === 'true') listbox.click(); }; open(); let popup = Array.from(document.querySelectorAll('.OA0qNb[jsname="V68bde"]')).find(m => isVisible(m)); let options = popup ? Array.from(popup.querySelectorAll('[role="option"]')) : Array.from(listbox.querySelectorAll('[role="option"]')); let target = options.find(o => norm(o.getAttribute('data-value')) === norm(wanted)) || options.find(o => norm(o.textContent) === norm(wanted)) || options.find(o => { const txt = norm(o.getAttribute('data-value') || o.textContent); return txt && txt !== 'choose'; }); if (target) target.click(); close(); } function resetListboxToChooseOrFirst(listbox) { const open = () => { if (listbox.getAttribute('aria-expanded') === 'false') listbox.click(); }; const close = () => { if (listbox.getAttribute('aria-expanded') === 'true') listbox.click(); }; open(); let popup = Array.from(document.querySelectorAll('.OA0qNb[jsname="V68bde"]')).find(m => isVisible(m)); let options = popup ? Array.from(popup.querySelectorAll('[role="option"]')) : Array.from(listbox.querySelectorAll('[role="option"]')); // Prefer "Choose" / empty value let target = options.find(o => /choose/i.test(o.textContent || '') || (o.getAttribute('data-value') || '') === ''); if (!target && options.length) target = options[0]; if (target) target.click(); close(); } // CLEARING LOGIC function clearCheckboxesOnly() { // Uncheck all checked checkboxes (requested: auto-remove previously set checkmarks before run) Array.from(document.querySelectorAll('[role="checkbox"][aria-checked="true"]')).forEach(cb => cb.click()); } function clearAllInputs() { // Text-like inputs document.querySelectorAll('input.whsOnd[type="text"], input.whsOnd[type="email"], input.whsOnd[type="number"]').forEach(el => setNativeValue(el, '')); // Textareas document.querySelectorAll('textarea').forEach(el => setNativeValue(el, '')); // Dates document.querySelectorAll('input[type="date"].whsOnd').forEach(el => setNativeValue(el, '')); // Time (Hour/Minute) document.querySelectorAll('.PfQ8Lb input[aria-label="Hour"]').forEach(el => setNativeValue(el, '')); document.querySelectorAll('.PfQ8Lb input[aria-label="Minute"]').forEach(el => setNativeValue(el, '')); // Time AM/PM reset to first option (usually AM) document.querySelectorAll('.PfQ8Lb [role="listbox"][aria-label="AM or PM"]').forEach(lb => resetListboxToChooseOrFirst(lb)); // Checkboxes Array.from(document.querySelectorAll('[role="checkbox"][aria-checked="true"]')).forEach(cb => cb.click()); // Radios (try to deselect active; Forms may keep one selected if required) Array.from(document.querySelectorAll('[role="radio"][aria-checked="true"]')).forEach(r => r.click()); // Select listboxes reset to default document.querySelectorAll('[role="listbox"].jgvuAb, [role="listbox"].cGN2le, [role="listbox"].t9kgXb').forEach(lb => resetListboxToChooseOrFirst(lb)); } btnClean.addEventListener('click', () => { clearAllInputs(); }); // fill from plan async function fillFromPlan(plan) { document.querySelectorAll('[data-ai-field-id^="text_"]').forEach(input => { const id = input.dataset.aiFieldId; const p = plan.find(x => x.id === id); if (p && typeof p.value === 'string') setNativeValue(input, p.value); }); document.querySelectorAll('[data-ai-field-id^="textarea_"]').forEach(ta => { const id = ta.dataset.aiFieldId; const p = plan.find(x => x.id === id); if (p && typeof p.value === 'string') setNativeValue(ta, p.value); }); document.querySelectorAll('[role="radiogroup"][data-ai-field-id]').forEach(group => { const id = group.dataset.aiFieldId; const p = plan.find(x => x.id === id); if (!p || typeof p.value !== 'string') return; const target = Array.from(group.querySelectorAll('[role="radio"]')).find(r => { const v = r.getAttribute('data-value') || r.getAttribute('aria-label') || r.textContent; return plain(v).toLowerCase() === plain(p.value).toLowerCase(); }) || Array.from(group.querySelectorAll('[role="radio"]'))[0]; if (target) target.click(); }); const groupIds = new Set(plan.filter(p => Array.isArray(p.value) || typeof p.value === 'string').map(p => p.id)); groupIds.forEach(id => { const planned = plan.find(p => p.id === id); const values = Array.isArray(planned?.value) ? planned.value : [planned?.value].filter(Boolean); const cbs = Array.from(document.querySelectorAll(`[role="checkbox"][data-ai-field-id="${id}"]`)); cbs.forEach(cb => { if (cb.getAttribute('aria-checked') === 'true') cb.click(); }); values.forEach(val => { const target = cbs.find(cb => { const v = cb.getAttribute('data-answer-value') || cb.getAttribute('aria-label') || cb.textContent; return plain(v).toLowerCase() === plain(val).toLowerCase(); }) || null; if (target) target.click(); }); }); document.querySelectorAll('[role="listbox"][data-ai-field-id^="select_"]').forEach(lb => { const id = lb.dataset.aiFieldId; const p = plan.find(x => x.id === id); if (!p || typeof p.value !== 'string') return; clickOptionInListbox(lb, p.value); }); document.querySelectorAll('input[type="date"][data-ai-field-id^="date_"]').forEach(dateEl => { const id = dateEl.dataset.aiFieldId; const p = plan.find(x => x.id === id); const min = dateEl.getAttribute('min'); const max = dateEl.getAttribute('max'); const value = (p && typeof p.value === 'string') ? p.value : randDate(min, max); setNativeValue(dateEl, value); }); const timeGroups = {}; document.querySelectorAll('[data-ai-field-id^="time_"]').forEach(el => { const id = el.dataset.aiFieldId.replace(/_(hour|minute|ampm)$/, ''); if (!timeGroups[id]) timeGroups[id] = {}; if (/_hour$/.test(el.dataset.aiFieldId)) timeGroups[id].hourEl = el; if (/_minute$/.test(el.dataset.aiFieldId)) timeGroups[id].minuteEl = el; if (/_ampm$/.test(el.dataset.aiFieldId)) timeGroups[id].ampmEl = el; }); Object.keys(timeGroups).forEach(id => { const grp = timeGroups[id]; const p = plan.find(x => x.id === id); let obj = null; if (p && typeof p.value === 'object' && p.value) { obj = { hour: clamp(parseInt(p.value.hour, 10) || 9, 1, 12), minute: clamp(parseInt(p.value.minute, 10) || 0, 0, 59), meridiem: String(p.value.meridiem || 'AM').toUpperCase() }; } else if (p && typeof p.value === 'string') { obj = parseTimeLike(p.value); } if (!obj) obj = { hour: 10, minute: 30, meridiem: 'AM' }; if (grp.hourEl) setNativeValue(grp.hourEl, String(obj.hour).padStart(2, '0')); if (grp.minuteEl) setNativeValue(grp.minuteEl, String(obj.minute).padStart(2, '0')); if (grp.ampmEl) clickOptionInListbox(grp.ampmEl, obj.meridiem); }); } // schema scraper async function scrapeSchema() { const schema = []; document.querySelectorAll('input[type="text"].whsOnd, input[type="email"].whsOnd, input[type="number"].whsOnd').forEach((input, idx) => { const wrap = getClosestListItem(input); const label = getHeadingText(wrap); const id = `text_${idx}_${uid()}`; input.dataset.aiFieldId = id; schema.push({ id, type: 'text', label: plain(label), required: input.required || /[*]$/.test(label) }); }); document.querySelectorAll('textarea.KHxj8b, textarea').forEach((ta, idx) => { const wrap = getClosestListItem(ta); const label = getHeadingText(wrap); const id = `textarea_${idx}_${uid()}`; ta.dataset.aiFieldId = id; schema.push({ id, type: 'textarea', label: plain(label), required: ta.required || /[*]$/.test(label) }); }); document.querySelectorAll('[role="radiogroup"]').forEach((rg, idx) => { const wrap = getClosestListItem(rg); const label = getHeadingText(wrap); const options = Array.from(rg.querySelectorAll('[role="radio"]')).map(r => { const v = r.getAttribute('data-value') || r.getAttribute('aria-label') || r.textContent; return plain(v); }).filter(Boolean); if (!options.length) return; const id = `radio_${idx}_${uid()}`; rg.dataset.aiFieldId = id; schema.push({ id, type: 'radio', label: plain(label), options: Array.from(new Set(options)) }); }); const checkboxEls = Array.from(document.querySelectorAll('[role="checkbox"][data-answer-value], [role="checkbox"][aria-label]')); const groups = new Map(); checkboxEls.forEach((cb) => { const wrap = getClosestListItem(cb); const groupLabel = getHeadingText(wrap) || 'Checkbox Group'; const fieldId = cb.getAttribute('data-field-id') || groupLabel; const key = `${groupLabel}::${fieldId}`; if (!groups.has(key)) groups.set(key, []); const v = cb.getAttribute('data-answer-value') || cb.getAttribute('aria-label') || cb.textContent; groups.get(key).push({ el: cb, value: plain(v) }); }); let cidx = 0; for (const [key, items] of groups.entries()) { const [groupLabel] = key.split('::'); const id = `checkbox_${cidx++}_${uid()}`; items.forEach(i => { i.el.dataset.aiFieldId = id; }); const options = Array.from(new Set(items.map(i => i.value))).filter(Boolean); if (options.length) schema.push({ id, type: 'checkbox', label: plain(groupLabel), options }); } // Select listboxes and floating popup const listboxes = Array.from(document.querySelectorAll('[role="listbox"].jgvuAb, [role="listbox"].cGN2le, [role="listbox"].t9kgXb')); for (let idx = 0; idx < listboxes.length; idx++) { const lb = listboxes[idx]; const wrap = getClosestListItem(lb); const label = getHeadingText(wrap) || 'Select'; const id = `select_${idx}_${uid()}`; lb.dataset.aiFieldId = id; if (lb.getAttribute('aria-expanded') === 'false') lb.click(); await sleep(10); let popup = Array.from(document.querySelectorAll('.OA0qNb[jsname="V68bde"]')).find(m => isVisible(m)); let opts = popup ? Array.from(popup.querySelectorAll('[role="option"]')).map(o => plain(o.getAttribute('data-value') || o.textContent)) : Array.from(lb.querySelectorAll('[role="option"]')).map(o => plain(o.getAttribute('data-value') || o.textContent)); if (lb.getAttribute('aria-expanded') === 'true') lb.click(); opts = opts.filter(v => v && !/^choose$/i.test(v)); if (opts.length) schema.push({ id, type: 'select', label: plain(label), options: Array.from(new Set(opts)) }); } document.querySelectorAll('input[type="date"].whsOnd').forEach((dateEl, idx) => { const wrap = getClosestListItem(dateEl); const label = getHeadingText(wrap) || 'Date'; const id = `date_${idx}_${uid()}`; dateEl.dataset.aiFieldId = id; schema.push({ id, type: 'date', label: plain(label), min: dateEl.getAttribute('min') || null, max: dateEl.getAttribute('max') || null }); }); document.querySelectorAll('.PfQ8Lb').forEach((timeWrap, idx) => { const wrap = getClosestListItem(timeWrap); const label = getHeadingText(wrap) || 'Time'; const hour = timeWrap.querySelector('input[aria-label="Hour"]'); const minute = timeWrap.querySelector('input[aria-label="Minute"]'); const meridianBox = timeWrap.querySelector('[role="listbox"][aria-label="AM or PM"]'); if (hour && minute && meridianBox) { const id = `time_${idx}_${uid()}`; hour.dataset.aiFieldId = `${id}_hour`; minute.dataset.aiFieldId = `${id}_minute`; meridianBox.dataset.aiFieldId = `${id}_ampm`; const options = Array.from(meridianBox.querySelectorAll('[role="option"]')).map(o => plain(o.getAttribute('data-value') || o.textContent)); schema.push({ id, type: 'time', label: plain(label), options: Array.from(new Set(options)) }); } }); return schema; } // main async function run() { const original = btn.textContent; btn.disabled = true; btn.textContent = '...'; try { // Auto-remove previously set checkmarks (checkboxes) before running fill clearCheckboxesOnly(); const schema = await scrapeSchema(); if (!schema.length) throw new Error('No fields detected'); const prompt = buildPrompt(schema, sessionExtraInstructions); // clear one-time instructions after use sessionExtraInstructions = ''; btnEdit.style.background = '#444'; const gemini = new GeminiClient(); const txt = await gemini.generateContent(prompt); const plan = safeJson(txt); await fillFromPlan(plan); btn.textContent = '✓'; await sleep(1200); } catch (e) { console.error(e); alert(e.message || 'AI fill failed'); } finally { btn.disabled = false; btn.textContent = original; } } btn.addEventListener('click', run); })();