// ==UserScript== // @name 问卷星自动答题助手 // @namespace https://wjx.panel.local // @version 0.0.2 // @description 一款功能强大的问卷星辅助工具。支持全题型识别(单选、多选、填空、矩阵、量表、滑块、地区、排序),通过可视化面板自定义各选项权重及设计份数。支持一键破解复制限制,全自动模拟填写与批量提交,内置滑块验证模拟及本地存储清理功能。 // @author x7R2p9 // @match https://*.wjx.top/* // @match https://*.wjx.cn/* // @match https://*.wjx.com/* // @grant none // @run-at document-end // @license MIT // @tag 问卷星 // @tag 问卷星脚本 // @tag 问卷星全自动 // @tag 自动填写 // @tag 批量提交 // @tag 问卷星刷问卷 // @tag 问卷星辅助 // ==/UserScript== (function() { 'use strict'; const PANEL_ID = 'wjx-commercial-panel'; const STORAGE_PREFIX = 'WJX_CN_PANEL_'; const REMAINING_COUNT_KEY = 'WJX_REMAINING_COUNT'; const TOTAL_COUNT_KEY = 'WJX_TOTAL_COUNT'; const SURVEY_URL_KEY = 'WJX_SURVEY_URL'; const PANEL_COLLAPSED_KEY = 'WJX_PANEL_COLLAPSED_BEFORE_AUTOMATION'; const GREASYFORK_URL = 'https://scriptcat.org/zh-CN/script-show-page/6321'; const PROMO_TOAST_KEY = 'WJX_GREASYFORK_PROMO_SHOWN'; const DEFAULT_TEXT_LIBRARY = ['很好', '满意', '支持', '体验不错']; const TYPE_LABELS = { radio: '单选题', checkbox: '多选题', dropdown: '下拉题', text: '填空题', rating: '量表题', matrix: '矩阵题', slide: '滑块题', location: '地区题', sorting: '排序题', unknown: '未知题型' }; const State = { surveyKey: '', config: { targetCount: 1, questions: [] } }; const STYLE_TEXT = ` :root { --wjx-bg: #f5f9ff; --wjx-surface: #ffffff; --wjx-surface-strong: #ffffff; --wjx-primary: #0064ff; --wjx-primary-deep: #0052d9; --wjx-primary-soft: #edf4ff; --wjx-line: #d7e6ff; --wjx-text: #1f2d3d; --wjx-subtle: #5f6f82; --wjx-shadow: 0 10px 28px rgba(23, 80, 179, 0.10); } #${PANEL_ID} { position: fixed; top: 16px; right: 16px; width: 402px; max-height: calc(100vh - 32px); z-index: 2147483647; display: flex; flex-direction: column; overflow: hidden; border-radius: 10px; border: 1px solid var(--wjx-line); background: var(--wjx-surface); color: var(--wjx-text); box-shadow: var(--wjx-shadow); font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif; } #${PANEL_ID}.is-collapsed { width: 240px; } #${PANEL_ID}.is-automation-hidden { display: none !important; } .wjx-head { padding: 16px; background: linear-gradient(180deg, #f8fbff, #edf4ff); color: var(--wjx-text); border-bottom: 1px solid var(--wjx-line); border-top: 3px solid var(--wjx-primary); } .wjx-head-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } .wjx-title { margin: 0; font-size: 18px; font-weight: 700; letter-spacing: 0; } .wjx-toggle { border: 1px solid #b9d2ff; border-radius: 6px; padding: 6px 12px; font-size: 11px; font-weight: 700; color: var(--wjx-primary-deep); background: #ffffff; cursor: pointer; flex-shrink: 0; } .wjx-body { padding: 16px; overflow-y: auto; background: var(--wjx-bg); } .wjx-section { margin-bottom: 14px; padding: 14px; border-radius: 8px; background: var(--wjx-surface); border: 1px solid var(--wjx-line); } .wjx-section-title { margin: 0 0 10px; font-size: 13px; font-weight: 700; color: var(--wjx-primary-deep); } .wjx-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } .wjx-metric { padding: 12px; border-radius: 8px; background: var(--wjx-surface-strong); border: 1px solid var(--wjx-line); } .wjx-metric span { display: block; } .wjx-metric-label { font-size: 11px; color: var(--wjx-subtle); margin-bottom: 4px; } .wjx-metric-value { font-size: 18px; font-weight: 800; } .wjx-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; } .wjx-field label { font-size: 12px; font-weight: 700; } .wjx-input, .wjx-textarea { width: 100%; box-sizing: border-box; border: 1px solid #c9dbff; border-radius: 6px; padding: 10px 12px; font-size: 13px; color: var(--wjx-text); background: #ffffff; outline: none; } .wjx-input:focus, .wjx-textarea:focus { border-color: #7fb0ff; box-shadow: 0 0 0 3px rgba(0,100,255,0.10); } .wjx-textarea { min-height: 88px; resize: vertical; line-height: 1.5; } .wjx-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } .wjx-btn { min-height: 40px; border-radius: 6px; padding: 0 12px; font-size: 13px; font-weight: 700; cursor: pointer; transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease; } .wjx-btn:hover { filter: none; } .wjx-btn-primary { color: #ffffff; background: var(--wjx-primary); border: 1px solid var(--wjx-primary); } .wjx-btn-secondary { color: var(--wjx-text); background: #ffffff; border: 1px solid #c9dbff; } .wjx-list { display: flex; flex-direction: column; gap: 12px; } .wjx-card { padding: 14px; border-radius: 8px; background: var(--wjx-surface-strong); border: 1px solid var(--wjx-line); } .wjx-card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 8px; } .wjx-card-title { font-size: 13px; font-weight: 800; line-height: 1.5; } .wjx-card-type { flex-shrink: 0; padding: 5px 9px; border-radius: 6px; background: var(--wjx-primary-soft); color: var(--wjx-primary-deep); font-size: 11px; font-weight: 700; border: 1px solid #cfe0ff; } .wjx-card-meta { margin-bottom: 10px; color: var(--wjx-subtle); font-size: 11px; line-height: 1.45; } .wjx-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } .wjx-grid-item { padding: 8px; border-radius: 6px; background: #f7fbff; border: 1px solid #deebff; } .wjx-grid-item span { display: block; margin-bottom: 6px; font-size: 11px; color: var(--wjx-subtle); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .wjx-grid-item input { width: 100%; box-sizing: border-box; border: 1px solid #c9dbff; border-radius: 6px; padding: 8px 10px; font-size: 13px; background: #ffffff; } .wjx-tools { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; } .wjx-chip { border: 1px solid #cfe0ff; border-radius: 6px; padding: 6px 10px; font-size: 11px; font-weight: 700; color: var(--wjx-primary-deep); background: var(--wjx-primary-soft); cursor: pointer; } .wjx-empty { text-align: center; padding: 24px 16px; color: var(--wjx-subtle); line-height: 1.7; } .wjx-toast { position: fixed; right: 24px; bottom: 24px; z-index: 2147483647; max-width: 320px; padding: 12px 14px; border-radius: 8px; color: #ffffff; background: rgba(0, 82, 217, 0.94); box-shadow: 0 12px 28px rgba(0, 82, 217, 0.20); font-size: 13px; line-height: 1.5; opacity: 0; transform: translateY(8px); transition: opacity 0.18s ease, transform 0.18s ease; pointer-events: none; } .wjx-toast.is-actionable { max-width: 300px; padding: 10px 12px; font-size: 12px; line-height: 1.6; background: rgba(19, 50, 104, 0.96); box-shadow: 0 10px 24px rgba(19, 50, 104, 0.18); pointer-events: auto; cursor: pointer; } .wjx-toast .wjx-toast-link { color: #ffd36b; font-weight: 700; } .wjx-toast.is-visible { opacity: 1; transform: translateY(0); } .wjx-progress-modal { position: fixed; top: 16px; left: 50%; z-index: 2147483647; min-width: 240px; padding: 14px 18px; border-radius: 10px; border: 1px solid #b9d2ff; background: rgba(255, 255, 255, 0.98); color: var(--wjx-text); box-shadow: 0 12px 32px rgba(0, 82, 217, 0.18); font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif; text-align: center; opacity: 0; transform: translate(-50%, -8px); transition: opacity 0.2s ease, transform 0.2s ease; pointer-events: none; } .wjx-progress-modal.is-visible { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; } .wjx-progress-title { margin: 0 0 8px; font-size: 13px; font-weight: 700; color: var(--wjx-primary-deep); } .wjx-progress-count { margin: 0 0 10px; font-size: 16px; font-weight: 800; color: var(--wjx-text); } .wjx-progress-bar-wrap { height: 8px; margin-bottom: 8px; border-radius: 999px; background: #e8f1ff; overflow: hidden; } .wjx-progress-bar { height: 100%; width: 0; border-radius: 999px; background: linear-gradient(90deg, var(--wjx-primary), #3d8bff); transition: width 0.25s ease; } .wjx-progress-percent { margin: 0; font-size: 12px; font-weight: 700; color: var(--wjx-subtle); } .wjx-progress-stop { margin-top: 12px; min-height: 34px; padding: 0 16px; border-radius: 6px; border: 1px solid #ffb4b4; background: #fff5f5; color: #d4380d; font-size: 13px; font-weight: 700; cursor: pointer; } .wjx-progress-stop:hover { background: #ffece8; } .wjx-hidden { display: none; } @media (max-width: 640px) { #${PANEL_ID} { top: auto; right: 10px; left: 10px; bottom: 10px; width: auto; max-height: 82vh; } .wjx-metrics, .wjx-actions, .wjx-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } `; function init() { // 破解右键、选择和复制限制 try { document.oncontextmenu = () => true; document.onselectstart = () => true; document.oncopy = () => true; document.onpaste = () => true; if (window.$ && window.$.fn) { $('body').css('user-select', 'text'); } } catch (e) {} if (isCompletionPage()) { handleCompletion(); return; } if (document.getElementById(PANEL_ID)) { return; } State.surveyKey = createSurveyKey(); injectStyles(); renderPanel(); refreshQuestions({ silent: true, preserveInputs: false }); updateAllRelationVisibility(); scheduleRescan(); checkAndContinueAutomation(); schedulePromoToast(); } function isCompletionPage() { return /complete\.aspx|join\/complete|done/i.test(location.href); } function handleCompletion() { let remaining = Number(localStorage.getItem(REMAINING_COUNT_KEY) || 0); if (remaining > 0) { remaining -= 1; localStorage.setItem(REMAINING_COUNT_KEY, String(remaining)); if (remaining > 0) { ensureTotalCountForAutomation(); hidePanelForAutomation(); showAutomationProgress(); const url = localStorage.getItem(SURVEY_URL_KEY); if (url) { clearTimeout(automationRedirectTimer); automationRedirectTimer = setTimeout(() => { automationRedirectTimer = null; if (!isAutomationActive()) { return; } location.href = url; }, 2000); } } else { endAutomationSession('所有自动化提交任务已完成!'); } updateAutomationProgress(); } } function checkAndContinueAutomation() { const remaining = Number(localStorage.getItem(REMAINING_COUNT_KEY) || 0); if (remaining > 0) { ensureTotalCountForAutomation(); beginAutomationSession(); setTimeout(() => { executeAutoFillAndSubmit(); }, 2000); } } function createSurveyKey() { return STORAGE_PREFIX + `${location.host}${location.pathname}`.replace(/[^a-zA-Z0-9_-]/g, '_'); } function injectStyles() { const style = document.createElement('style'); style.textContent = STYLE_TEXT; document.head.appendChild(style); } function renderPanel() { const panel = document.createElement('aside'); panel.id = PANEL_ID; panel.innerHTML = `
自动化提交中
第1份/共1份
进度 0%
`; document.body.appendChild(modal); modal.querySelector('#wjx-progress-stop').addEventListener('click', (event) => { event.stopPropagation(); stopAutomation(); }); } return modal; } function updateAutomationProgressUI(progress) { const countEl = document.getElementById('wjx-progress-count'); const percentEl = document.getElementById('wjx-progress-percent'); const barEl = document.getElementById('wjx-progress-bar'); if (countEl) { countEl.textContent = `第${progress.current}份/共${progress.total}份`; } if (percentEl) { percentEl.textContent = `进度 ${progress.percent}%`; } if (barEl) { barEl.style.width = `${progress.percent}%`; } } function showAutomationProgress() { const progress = getAutomationProgress(); if (!progress) { return; } const modal = ensureProgressModal(); updateAutomationProgressUI(progress); modal.classList.add('is-visible'); } function updateAutomationProgress() { const progress = getAutomationProgress(); if (!progress) { return; } const modal = document.querySelector('.wjx-progress-modal') || ensureProgressModal(); updateAutomationProgressUI(progress); modal.classList.add('is-visible'); } function hideAutomationProgress() { const modal = document.querySelector('.wjx-progress-modal'); if (modal) { modal.classList.remove('is-visible'); } } let toastTimer = null; function showToast(message, options) { const settings = Object.assign({ html: false, duration: 2200, actionable: false, href: '' }, options); let toast = document.querySelector('.wjx-toast'); if (!toast) { toast = document.createElement('div'); toast.className = 'wjx-toast'; document.body.appendChild(toast); } toast.classList.toggle('is-actionable', settings.actionable); toast.onclick = null; if (settings.html) { toast.innerHTML = message; } else { toast.textContent = message; } if (settings.actionable && settings.href) { toast.onclick = () => { window.open(settings.href, '_blank', 'noopener,noreferrer'); }; } toast.classList.add('is-visible'); clearTimeout(toastTimer); toastTimer = setTimeout(() => { toast.classList.remove('is-visible'); }, settings.duration); } function schedulePromoToast() { try { const key = `${PROMO_TOAST_KEY}_${location.host}${location.pathname}`; if (sessionStorage.getItem(key)) { return; } sessionStorage.setItem(key, '1'); setTimeout(() => { showToast('脚本已生效!如果觉得好用,可以点击这里在 scriptcat 点赞一下支持作者吗?', { html: true, duration: 5200, actionable: true, href: GREASYFORK_URL }); }, 900); } catch (error) {} } function scheduleRescan() { setTimeout(() => { if (document.getElementById(PANEL_ID)) { refreshQuestions({ silent: true, preserveInputs: true }); } }, 1500); } function startAutomation() { const count = State.config.targetCount; if (count <= 0) { showToast('请输入有效的设计份数。'); return; } localStorage.setItem(REMAINING_COUNT_KEY, String(count)); localStorage.setItem(TOTAL_COUNT_KEY, String(count)); localStorage.setItem(SURVEY_URL_KEY, location.href); beginAutomationSession(); executeAutoFillAndSubmit(); } async function executeAutoFillAndSubmit() { try { if (!isAutomationActive()) { return; } showAutomationProgress(); const started = await ensureSurveyStarted(); if (!isAutomationActive()) { return; } if (!started) { throw new Error('未识别到题目页或封面开始按钮'); } const result = await fillAllQuestions(); if (!isAutomationActive()) { return; } if (result && result.submitted) { return; } // Wait a bit to look more natural await new Promise(resolve => setTimeout(resolve, 1500)); if (!isAutomationActive()) { return; } const submitted = await submitSurvey(); if (!isAutomationActive()) { return; } if (!submitted) { throw new Error('未找到提交按钮'); } } catch (error) { console.error('Automation error:', error); endAutomationSession(`提交出错: ${error.message || '未知错误'}`); } } async function fillAllQuestions() { const filledQuestionIds = new Set(); for (let pageIndex = 0; pageIndex < 12; pageIndex++) { updateAllRelationVisibility(); const visibleNodes = getVisibleQuestionNodes(); if (!visibleNodes.length) { throw new Error('当前页面未识别到可填写题目'); } const pageSignature = visibleNodes.map((node) => extractQuestionId(node, 0)).join('|'); let filledOnPage = false; for (let pass = 0; pass < 8; pass++) { updateAllRelationVisibility(); let filledInPass = false; for (const node of getVisibleQuestionNodes()) { const questionId = extractQuestionId(node, 0); if (filledQuestionIds.has(questionId)) { continue; } const relations = parseRelations(node); if (relations.length && !isRelationSatisfied(relations)) { continue; } const question = State.config.questions.find((item) => String(item.id) === String(questionId)); if (!question) { continue; } await fillQuestion(question, node); filledQuestionIds.add(questionId); updateAllRelationVisibility(); await sleep(200 + Math.random() * 300); filledInPass = true; filledOnPage = true; } if (!filledInPass) { break; } } if (!filledOnPage) { const pendingVisible = getVisibleQuestionNodes().filter((node) => { const questionId = extractQuestionId(node, 0); if (filledQuestionIds.has(questionId)) { return false; } const relations = parseRelations(node); return !relations.length || isRelationSatisfied(relations); }); if (pendingVisible.length) { throw new Error('存在未填写的关联题目,请检查父题选项是否已选中'); } } const action = await advanceSurvey(pageSignature); if (action === 'next') { continue; } if (action === 'submit') { await sleep(800 + Math.random() * 700); const submitted = await submitSurvey(); if (!submitted) { throw new Error('进入提交分支后仍未找到提交按钮'); } return { submitted: true }; } if (action === 'stalled') { throw new Error('检测到下一页按钮,但页面未成功翻页'); } return { submitted: false }; } return { submitted: false }; } async function fillQuestion(question, node) { switch (question.type) { case 'radio': case 'dropdown': case 'rating': const index = pickWeightedIndex(question.weights); clickOption(node, index, question.type); updateAllRelationVisibility(); break; case 'checkbox': const indices = pickMultipleIndices(question.weights, question.minSelections, question.maxSelections); indices.forEach(idx => clickOption(node, idx, question.type)); updateAllRelationVisibility(); break; case 'text': const text = pickRandomText(question.content); fillText(node, text); break; case 'slide': fillSlide(node, question.minScore, question.maxScore); break; case 'matrix': fillMatrix(node, question.weights); break; case 'location': await fillLocation(node); break; case 'sorting': await fillSorting(node); break; } } async function fillLocation(node) { const trigger = node.querySelector('.city-container, .divProvince, .ui-select, textarea, input[type="text"]'); if (trigger) { trigger.click(); await new Promise(resolve => setTimeout(resolve, 1000)); const clickAndPick = async (selector) => { const btn = document.querySelector(selector); if (btn) { btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); await new Promise(r => setTimeout(r, 500)); const options = document.querySelectorAll('[id$=-results] > li:not(:first-child)'); if (options.length > 0) { const randomOpt = options[Math.floor(Math.random() * options.length)]; randomOpt.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); await new Promise(r => setTimeout(r, 500)); return true; } } return false; }; const hasProvince = await clickAndPick('.divProvince, [id^=select2-province]'); if (hasProvince) { await clickAndPick('.divCity, [id^=select2-city]'); await clickAndPick('.divArea, [id^=select2-area]'); const saveBtn = document.querySelector('.layer_save_btn a, .ui-dialog .save_btn'); if (saveBtn) saveBtn.click(); } } } async function fillSorting(node) { const items = Array.from(node.querySelectorAll('.reorder-list > li, .ui-sortable > li, .ui-sortable-handle, .reorder-item, ul > li')) .filter((item) => cleanText(item.textContent)); if (items.length > 0) { const shuffled = items.sort(() => Math.random() - 0.5); for (const item of shuffled) { item.click(); await new Promise(r => setTimeout(r, 300)); } } } function pickWeightedIndex(weights) { const total = weights.reduce((a, b) => a + b, 0); if (total <= 0) return Math.floor(Math.random() * weights.length); let random = Math.random() * total; for (let i = 0; i < weights.length; i++) { if (random < weights[i]) return i; random -= weights[i]; } return weights.length - 1; } function pickMultipleIndices(weights, minSelections, maxSelections) { const totalOptions = weights.length; if (!totalOptions) { return []; } const minCount = Math.max(0, Math.min(Number(minSelections) || 0, totalOptions)); const defaultMax = Math.min(totalOptions, Math.max(minCount || 1, Math.min(3, totalOptions))); const maxCount = Math.max(minCount, Math.min(Number(maxSelections) || defaultMax, totalOptions)); const count = minCount >= maxCount ? Math.max(1, minCount || 1) : randomInt(minCount || 1, maxCount); const pool = weights.map((weight, index) => ({ index, weight: Math.max(1, Number(weight) || 1) })); const results = []; while (results.length < count && pool.length > 0) { const total = pool.reduce((sum, item) => sum + item.weight, 0); let random = Math.random() * total; let selectedIndex = 0; for (let i = 0; i < pool.length; i++) { random -= pool[i].weight; if (random <= 0) { selectedIndex = i; break; } } results.push(pool[selectedIndex].index); pool.splice(selectedIndex, 1); } return results; } function pickRandomText(texts) { if (!texts || texts.length === 0) return DEFAULT_TEXT_LIBRARY[Math.floor(Math.random() * DEFAULT_TEXT_LIBRARY.length)]; return texts[Math.floor(Math.random() * texts.length)]; } function randomInt(min, max) { return min + Math.floor(Math.random() * (max - min + 1)); } function isNodeVisible(node) { if (!(node instanceof HTMLElement)) { return false; } const style = window.getComputedStyle(node); return style.display !== 'none' && style.visibility !== 'hidden' && node.getClientRects().length > 0; } function getVisibleQuestionNodes() { return findQuestionNodes().filter(isNodeVisible); } function getButtonText(button) { if (!(button instanceof HTMLElement)) { return ''; } return cleanText(button.textContent || button.value || button.getAttribute('aria-label') || ''); } function getVisibleActionButtons() { return Array.from(document.querySelectorAll( '#divNext, #ctlNext, #divStart, #submit_button, .submitbutton, .btn-submit, a.button, button, .button.mainBgColor, input[type="button"], input[type="submit"]' )).filter(isNodeVisible); } function getCoverStartButton() { return getVisibleActionButtons().find((button) => { const text = getButtonText(button); if (button.matches('#divStart')) { return true; } if (button.matches('#ctlNext, #divNext') && /开始|进入|继续|下一页|下一步/.test(text)) { return true; } return /开始答题|开始填写|进入问卷|继续答题|继续填写|参与答题|立即开始|马上开始|点击开始/.test(text); }) || null; } function getNextPageButton() { return getVisibleActionButtons().find((button) => { if (button.matches('#ctlNext')) { return false; } const text = getButtonText(button); return button.id === 'divNext' || /下一页|下一步/.test(text); }) || null; } function getSubmitButton() { return getVisibleActionButtons().find((button) => { const text = getButtonText(button); if (button.matches('#submit_button, .submitbutton, .btn-submit')) { return true; } if (button.matches('#ctlNext')) { return !/下一页|下一步|开始答题|开始填写|进入问卷|继续答题|继续填写|立即开始|马上开始/.test(text); } return /提交|交卷|完成|发送|确认提交|确认/.test(text); }) || null; } async function waitForNextPage(previousSignature) { const start = Date.now(); while (Date.now() - start < 5000) { await sleep(250); const currentNodes = getVisibleQuestionNodes(); const currentSignature = currentNodes.map((node) => extractQuestionId(node, 0)).join('|'); if (currentSignature && currentSignature !== previousSignature) { return true; } } return false; } async function waitForQuestionPage() { const start = Date.now(); while (Date.now() - start < 8000) { await sleep(250); if (getVisibleQuestionNodes().length > 0) { return true; } } return false; } async function ensureSurveyStarted() { if (getVisibleQuestionNodes().length > 0) { return true; } const startButton = getCoverStartButton(); if (!startButton) { return false; } startButton.click(); return waitForQuestionPage(); } async function advanceSurvey(previousSignature) { const nextPageButton = getNextPageButton(); if (nextPageButton) { nextPageButton.click(); const pageChanged = await waitForNextPage(previousSignature); return pageChanged ? 'next' : 'stalled'; } return getSubmitButton() ? 'submit' : null; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getChoiceElements(node, type) { const controlGroup = node.querySelector('.ui-controlgroup'); if (type === 'checkbox' && controlGroup) { const directOptions = Array.from(controlGroup.children).filter((item) => item.matches('.ui-checkbox, li, .option-item')); if (directOptions.length) { return directOptions; } } if ((type === 'radio' || type === 'rating') && controlGroup) { const directOptions = Array.from(controlGroup.children).filter((item) => item.matches('.ui-radio, li, .option-item')); if (directOptions.length) { return directOptions; } } if (type === 'checkbox') { const options = Array.from(node.querySelectorAll('.ui-controlgroup .ui-checkbox, .ulradiocheck > li, .option-item')); return options.length ? options : Array.from(node.querySelectorAll('input[type="checkbox"]')); } if (type === 'radio' || type === 'rating') { const options = Array.from(node.querySelectorAll('.ui-controlgroup .ui-radio, .ulradiocheck > li, .scale-div li, .rating-star, .onscore, .starlevel, .option-item')); return options.length ? options : Array.from(node.querySelectorAll('input[type="radio"]')); } return []; } function isChoiceSelected(option, type) { if (!option) { return false; } const inputSelector = type === 'checkbox' ? 'input[type="checkbox"]' : 'input[type="radio"]'; const input = option.matches(inputSelector) ? option : option.querySelector(inputSelector); if (input) { return !!input.checked; } return option.classList.contains('checked') || option.classList.contains('active') || option.classList.contains('on') || option.querySelector('.jqchecked, .checked, .active, a.checked, a.jqchecked') !== null; } function clickChoiceElement(option, type) { if (!option) { return; } const targets = [ option, option.querySelector('.label'), option.querySelector('.jqcheck, .jqradio, a.jqcheck, a.jqradio'), option.querySelector('label'), option.querySelector('input') ].filter(Boolean); for (const target of targets) { ['mousedown', 'mouseup'].forEach((eventName) => { target.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true })); }); if (typeof target.click === 'function') { target.click(); } if (isChoiceSelected(option, type)) { break; } } const otherInput = option.querySelector('input.OtherText, input.OtherRadioText, .OtherText, .OtherRadioText, .ui-text input[type="text"]'); if (otherInput && !otherInput.value) { otherInput.value = '其他'; otherInput.dispatchEvent(new Event('input', { bubbles: true })); otherInput.dispatchEvent(new Event('change', { bubbles: true })); } } function clickOption(node, index, type) { if (type === 'dropdown') { const select = node.querySelector('select'); if (select && select.options.length > index) { let realIndex = index; // If the first option is "Please select", we skip it if (select.options[0].text.includes('请选择')) { realIndex += 1; } if (select.options[realIndex]) { select.selectedIndex = realIndex; select.dispatchEvent(new Event('change', { bubbles: true })); } } return; } const options = getChoiceElements(node, type); if (options[index]) { const option = options[index]; clickChoiceElement(option, type); if (!isChoiceSelected(option, type)) { const fallback = option.querySelector(type === 'checkbox' ? 'input[type="checkbox"]' : 'input[type="radio"]'); if (fallback && typeof fallback.click === 'function') { fallback.click(); } } } } function fillText(node, text) { const inputs = Array.from(node.querySelectorAll('textarea, input[type="text"], input[type="tel"], input[type="email"], input[type="number"]')) .filter((input) => input.offsetParent !== null && !input.classList.contains('OtherText') && !input.classList.contains('OtherRadioText')); inputs.forEach((input) => { input.value = text; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); }); } function fillSlide(node, minScore, maxScore) { const lower = clampScore(minScore, 1); const upper = Math.max(lower, clampScore(maxScore, 100)); const value = randomInt(lower, upper); const input = node.querySelector('input[type="range"], input[type="hidden"], input[type="number"], input[type="text"][id^="q"], input[name^="q"]'); if (!input) { return; } input.value = String(value); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); const visibleDisplay = node.querySelector('.slider-value, .scale-value, .slider_num, .slider-value-text'); if (visibleDisplay) { visibleDisplay.textContent = String(value); } } function getMatrixRowOptions(row) { const directControls = Array.from(row.querySelectorAll('.ui-radio, .ui-checkbox, .jqRadio, .jqCheckbox')); if (directControls.length) { return directControls; } const rateAnchors = Array.from(row.querySelectorAll('a[dval], .rate-off, .rate-on, .rate-offlarge, .rate-onlarge')); if (rateAnchors.length) { return rateAnchors; } const cells = Array.from(row.querySelectorAll('td, th')).slice(1); return cells.filter((cell) => cell.querySelector('input[type="radio"], input[type="checkbox"], .ui-radio, .ui-checkbox, a[dval], .rate-off, .rate-on') || cleanText(cell.textContent) ); } function fillMatrix(node, weights) { const rows = node.querySelectorAll('tr[rowindex], tbody tr'); rows.forEach(row => { if (!cleanText(row.textContent)) { return; } const index = pickWeightedIndex(weights); const options = getMatrixRowOptions(row); if (options[index]) { const option = options[index]; if (option.matches('a[dval], .rate-off, .rate-on, .rate-offlarge, .rate-onlarge')) { option.click(); return; } if (option.matches('td, th')) { const clickable = option.querySelector('a[dval], .rate-off, .rate-on, .rate-offlarge, .rate-onlarge, .ui-radio, .ui-checkbox, input[type="radio"], input[type="checkbox"]'); if (clickable) { clickable.click(); } else { option.click(); } } else { clickChoiceElement(option, option.querySelector('input[type="checkbox"]') ? 'checkbox' : 'radio'); } } }); } function isElementVisible(element) { if (!(element instanceof HTMLElement)) { return false; } const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) { return false; } return element.getClientRects().length > 0; } async function waitForCondition(checker, timeoutMs, intervalMs) { const start = Date.now(); while (Date.now() - start < timeoutMs) { if (checker()) { return true; } await sleep(intervalMs); } return false; } const SECURITY_DIALOG_POLL_MS = 40; const SECURITY_DIALOG_CONFIRM_DELAY_MIN_MS = 800; const SECURITY_DIALOG_CONFIRM_DELAY_MAX_MS = 2000; const SECURITY_DIALOG_POST_CLICK_MS = 350; function getSecurityCheckDialog() { const layers = document.querySelectorAll('.layui-layer-dialog'); for (let i = 0; i < layers.length; i++) { const layer = layers[i]; if (!isElementVisible(layer)) { continue; } const content = layer.querySelector('.layui-layer-content'); if (content && /需要安全校验/.test(cleanText(content.textContent))) { return layer; } } return null; } function getSecurityCheckConfirmButton(layer) { const root = layer || getSecurityCheckDialog(); if (!root) { return null; } const directBtn = root.querySelector('.layui-layer-btn0'); if (directBtn && isElementVisible(directBtn)) { return directBtn; } return Array.from(root.querySelectorAll('.layui-layer-btn a, .layui-layer-btn button')).find((btn) => { return isElementVisible(btn) && /确认|确定|知道了/.test(getButtonText(btn)); }) || null; } function clickConfirmFast(element) { if (!(element instanceof HTMLElement)) { return false; } const rect = element.getBoundingClientRect(); const clientX = rect.left + rect.width / 2; const clientY = rect.top + rect.height / 2; dispatchMouseLikeEvent(element, 'pointerdown', clientX, clientY, { buttons: 1, pressure: 0.5 }); dispatchMouseLikeEvent(element, 'mousedown', clientX, clientY, { buttons: 1 }); dispatchMouseLikeEvent(element, 'pointerup', clientX, clientY, { buttons: 0, pressure: 0 }); dispatchMouseLikeEvent(element, 'mouseup', clientX, clientY, { buttons: 0 }); dispatchMouseLikeEvent(element, 'click', clientX, clientY, { buttons: 0 }); if (typeof element.click === 'function') { element.click(); } return true; } function getAliyunCaptchaPopup() { const popup = document.querySelector('#aliyunCaptcha-window-popup'); return popup && isElementVisible(popup) ? popup : null; } function getAliyunCaptchaCheckboxIcon() { const icon = document.querySelector('#aliyunCaptcha-checkbox-icon.aliyunCaptcha-checkbox-icon') || document.getElementById('aliyunCaptcha-checkbox-icon'); return icon && isElementVisible(icon) ? icon : null; } function randomFloat(min, max) { return min + Math.random() * (max - min); } function getRandomPointInElement(element, paddingRatio) { const ratio = Number.isFinite(paddingRatio) ? paddingRatio : 0.22; const rect = element.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) { return { clientX: rect.left, clientY: rect.top }; } const padX = Math.min(rect.width * ratio, rect.width * 0.42); const padY = Math.min(rect.height * ratio, rect.height * 0.42); const minX = rect.left + padX; const maxX = Math.max(minX, rect.right - padX); const minY = rect.top + padY; const maxY = Math.max(minY, rect.bottom - padY); return { clientX: randomFloat(minX, maxX), clientY: randomFloat(minY, maxY) }; } function buildPointerInit(clientX, clientY, extra) { return Object.assign({ bubbles: true, cancelable: true, view: window, clientX, clientY, screenX: window.screenX + clientX, screenY: window.screenY + clientY, button: 0, buttons: 0, detail: 1, pointerId: 1, pointerType: 'mouse', isPrimary: true, width: 1, height: 1, pressure: 0.5 }, extra || {}); } function dispatchMouseLikeEvent(target, type, clientX, clientY, extra) { if (!(target instanceof HTMLElement)) { return; } const mouseInit = buildPointerInit(clientX, clientY, extra); if (type.startsWith('pointer') && typeof PointerEvent !== 'undefined') { const pointerInit = Object.assign({}, mouseInit, { pressure: type === 'pointerdown' ? 0.5 : 0 }); target.dispatchEvent(new PointerEvent(type, pointerInit)); } if (/^mouse|click|dblclick$/.test(type)) { target.dispatchEvent(new MouseEvent(type, mouseInit)); } } function getEventTargetAt(clientX, clientY, fallback) { const hit = document.elementFromPoint(clientX, clientY); return hit instanceof HTMLElement ? hit : fallback; } function collectEventTargets(hitTarget, rootElement) { const chain = []; let node = hitTarget; while (node instanceof HTMLElement) { chain.push(node); if (node === rootElement) { break; } node = node.parentElement; } if (rootElement instanceof HTMLElement && !chain.includes(rootElement)) { chain.push(rootElement); } return chain; } async function simulateMouseMovePath(endX, endY, startX, startY) { const steps = randomInt(10, 22); for (let i = 1; i <= steps; i++) { const progress = i / steps; const eased = progress * progress * (3 - 2 * progress); const clientX = startX + (endX - startX) * eased + randomFloat(-1.8, 1.8); const clientY = startY + (endY - startY) * eased + randomFloat(-1.8, 1.8); const target = getEventTargetAt(clientX, clientY, document.body); dispatchMouseLikeEvent(target, 'mousemove', clientX, clientY, { buttons: 0 }); if (typeof PointerEvent !== 'undefined') { dispatchMouseLikeEvent(target, 'pointermove', clientX, clientY, { buttons: 0 }); } await sleep(randomInt(14, 42)); } } async function simulateHumanClick(element, options) { if (!(element instanceof HTMLElement)) { return false; } const settings = Object.assign({ paddingRatio: 0.22, preDelay: [120, 380], hoverDelay: [60, 180], pressDelay: [55, 140], postDelay: [80, 200], approachDistance: [45, 110] }, options || {}); await sleep(randomInt(settings.preDelay[0], settings.preDelay[1])); const point = getRandomPointInElement(element, settings.paddingRatio); const clientX = point.clientX; const clientY = point.clientY; const approach = settings.approachDistance; const startX = clientX + randomFloat(-approach[1], approach[1]); const startY = clientY + randomFloat(-approach[0], approach[0]); await simulateMouseMovePath(clientX, clientY, startX, startY); await sleep(randomInt(settings.hoverDelay[0], settings.hoverDelay[1])); const hitTarget = getEventTargetAt(clientX, clientY, element); const eventTargets = collectEventTargets(hitTarget, element); eventTargets.forEach((target) => { dispatchMouseLikeEvent(target, 'mouseover', clientX, clientY, { buttons: 0 }); dispatchMouseLikeEvent(target, 'mouseenter', clientX, clientY, { buttons: 0 }); if (typeof PointerEvent !== 'undefined') { dispatchMouseLikeEvent(target, 'pointerover', clientX, clientY, { buttons: 0 }); dispatchMouseLikeEvent(target, 'pointerenter', clientX, clientY, { buttons: 0 }); } }); await sleep(randomInt(settings.pressDelay[0], settings.pressDelay[1])); const clickTarget = hitTarget.closest('#aliyunCaptcha-checkbox-icon, #aliyunCaptcha-checkbox-body, #aliyunCaptcha-checkbox-left, #aliyunCaptcha-checkbox-wrapper') || element; dispatchMouseLikeEvent(clickTarget, 'pointerdown', clientX, clientY, { buttons: 1, pressure: 0.5 }); dispatchMouseLikeEvent(clickTarget, 'mousedown', clientX, clientY, { buttons: 1 }); await sleep(randomInt(45, 125)); dispatchMouseLikeEvent(clickTarget, 'pointerup', clientX, clientY, { buttons: 0, pressure: 0 }); dispatchMouseLikeEvent(clickTarget, 'mouseup', clientX, clientY, { buttons: 0 }); dispatchMouseLikeEvent(clickTarget, 'click', clientX, clientY, { buttons: 0 }); await sleep(randomInt(settings.postDelay[0], settings.postDelay[1])); return true; } async function handleAliyunCaptchaCheckbox(timeoutMs) { const appeared = await waitForCondition(() => getAliyunCaptchaPopup(), timeoutMs, SECURITY_DIALOG_POLL_MS); if (!appeared) { return false; } return performAliyunCaptchaClick(timeoutMs); } async function performAliyunCaptchaClick(timeoutMs) { const icon = getAliyunCaptchaCheckboxIcon() || document.querySelector('#aliyunCaptcha-checkbox-body') || document.querySelector('#aliyunCaptcha-checkbox-left'); if (!icon) { return false; } await sleep(randomInt(450, 1100)); await simulateHumanClick(icon, { paddingRatio: 0.18, preDelay: [180, 520], hoverDelay: [90, 240], pressDelay: [70, 160], postDelay: [120, 280], approachDistance: [55, 130] }); await sleep(randomInt(600, 1200)); const loading = document.querySelector('#aliyunCaptcha-loading'); if (loading && isElementVisible(loading)) { await waitForCondition(() => { return !isElementVisible(loading) || !!document.querySelector('.aliyunCaptcha-checkbox-icon-checked'); }, Math.min(timeoutMs, 12000), 400); } await waitForCondition(() => !getAliyunCaptchaPopup(), Math.min(timeoutMs, 15000), 400); return true; } async function handleVerification(timeoutMs) { const deadline = Date.now() + (timeoutMs || 12000); let handled = false; let securityConfirmed = false; let aliyunClicked = false; let sliderHandled = false; let idleSince = Date.now(); while (Date.now() < deadline) { let activeThisTick = false; if (!securityConfirmed) { const confirmBtn = getSecurityCheckConfirmButton(getSecurityCheckDialog()); if (confirmBtn) { await sleep(randomInt(SECURITY_DIALOG_CONFIRM_DELAY_MIN_MS, SECURITY_DIALOG_CONFIRM_DELAY_MAX_MS)); clickConfirmFast(confirmBtn); await sleep(SECURITY_DIALOG_POST_CLICK_MS); securityConfirmed = true; handled = true; activeThisTick = true; idleSince = Date.now(); } } if (!aliyunClicked && getAliyunCaptchaPopup()) { if (await performAliyunCaptchaClick(deadline - Date.now())) { aliyunClicked = true; handled = true; activeThisTick = true; idleSince = Date.now(); } } if (!sliderHandled) { const rectMask = document.querySelector('#rectMask'); if (rectMask && isElementVisible(rectMask)) { rectMask.click(); await sleep(2000); await simulateSlider(); sliderHandled = true; handled = true; activeThisTick = true; idleSince = Date.now(); } } if (activeThisTick) { await sleep(SECURITY_DIALOG_POLL_MS); continue; } if (Date.now() - idleSince >= 1200) { break; } await sleep(SECURITY_DIALOG_POLL_MS); } return handled; } async function submitSurvey() { const submitBtn = getSubmitButton(); if (!submitBtn) { return false; } submitBtn.click(); const verificationHandled = await handleVerification(); if (verificationHandled) { await sleep(600); const retrySubmit = getSubmitButton(); if (retrySubmit) { retrySubmit.click(); await handleVerification(); } } else { await sleep(800); } return true; } async function simulateSlider() { const slider = document.querySelector('#nc_1__scale_text > span') || document.querySelector('.nc_iconfont.btn_slide'); if (slider) { const rect = slider.getBoundingClientRect(); const startX = rect.left + rect.width / 2; const startY = rect.top + rect.height / 2; const mouseDown = new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: startX, clientY: startY }); slider.dispatchEvent(mouseDown); const steps = 15; const totalWidth = 300; for (let i = 0; i <= steps; i++) { await new Promise(r => setTimeout(r, 50 + Math.random() * 100)); const mouseMove = new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: startX + (totalWidth / steps) * i + (Math.random() * 6 - 3), clientY: startY + (Math.random() * 6 - 3) }); window.dispatchEvent(mouseMove); } const mouseUp = new MouseEvent('mouseup', { bubbles: true, cancelable: true }); window.dispatchEvent(mouseUp); } } init(); })();