// ==UserScript== // @name 问卷星一键刷问卷|星球问卷助手|简单易操作,傻瓜式刷问卷!后期将会更新信效度、ai填写、相关、回归、因子分析、中介、调节、SEM、Kano等各种题型,以及模拟微信来源,自动切换IP地址等! // @namespace http://tampermonkey.net/ // @version 2.0 // @description 自动解析问卷,支持设置选项百分比,一键随机填写,QQ群:1029241274 后期将会更新信效度、ai填写、相关、回归、因子分析、中介、调节、SEM、Kano等各种题型,以及模拟微信来源,自动切换IP地址等!可以通过问卷星ai检测,页面完全模拟手机提交! // @description 我只需要略微出手,便是这个段位的极限,不好用来打死我 // @author NewB666 // @match *://www.wjx.cn/* // @match *://v.wjx.cn/* // @match *://ks.wjx.top/* // @match *://*.wjx.cn/* // @match *://*.wjx.top/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @run-at document-end // @icon https://vitejs.dev/logo.svg // @icon https://image.wjx.com/images/wlogo.png // ==/UserScript== (function() { 'use strict'; console.log('[星球问卷助手] 脚本开始加载...'); function isInIframe() { try { return window.self !== window.top; } catch (e) { return true; } } // ========== 计数与自动循环模块 ========== const COUNT_KEY = 'wjx_submit_count'; const AUTO_MODE_KEY = 'wjx_auto_mode'; const TARGET_URL_KEY = 'wjx_target_url'; const TARGET_COUNT_KEY = 'wjx_target_count'; // GM存储兼容层(如果GM API不可用则降级到localStorage) const storage = { get: (key, def) => { // 优先读 localStorage(同步且无缓存),再 fallback 到 GM try { const v = localStorage.getItem(key); if (v !== null) return JSON.parse(v); } catch (e) {} try { if (typeof GM_getValue === 'function') return GM_getValue(key, def); } catch (e) {} return def; }, set: (key, val) => { // 双写:localStorage + GM try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {} try { if (typeof GM_setValue === 'function') { GM_setValue(key, val); } } catch (e) {} }, del: (key) => { // 双删:localStorage + GM try { localStorage.removeItem(key); } catch (e) {} try { if (typeof GM_deleteValue === 'function') { GM_deleteValue(key); } } catch (e) {} } }; function getSubmitCount() { return storage.get(COUNT_KEY, 0); } function setSubmitCount(n) { storage.set(COUNT_KEY, n); } function incrementCount() { const c = getSubmitCount() + 1; setSubmitCount(c); return c; } function resetCount() { storage.del(COUNT_KEY); setSubmitCount(0); } function isAutoMode() { return !!storage.get(AUTO_MODE_KEY, false); } function setAutoMode(on) { storage.set(AUTO_MODE_KEY, !!on); } function getTargetUrl() { return storage.get(TARGET_URL_KEY, ''); } function setTargetUrl(url) { storage.set(TARGET_URL_KEY, url); } function getTargetCount() { return storage.get(TARGET_COUNT_KEY, 0); } function setTargetCount(n) { storage.set(TARGET_COUNT_KEY, n); } // 检测是否在成功页(提交成功后的页面) function isSuccessPage() { const url = location.href.toLowerCase(); const successUrlPatterns = ['/vm/', '/complete', 'completemobile', 'finish', 'success']; const urlMatch = successUrlPatterns.some(p => url.includes(p)); const bodyText = document.body ? document.body.innerText : ''; const successTextPatterns = ['感谢', '提交成功', '答题完成', '问卷已提交', '提交完成', '谢谢参与', '感谢您的参与', '已成功提交']; const textMatch = successTextPatterns.some(p => bodyText.includes(p)); // 提交后“领取/福利”落地页(常见:提交表单 → 提取福利/去领取) const rewardTextPatterns = ['提交表单', '提取福利', '去领取', '领取福利', '抽奖', '领取', '小礼物', '为您准备了']; const rewardTextMatch = rewardTextPatterns.some(p => bodyText.includes(p)); const stepLike = !!document.querySelector('.step, .steps, .step-item, [class*="step"], [class*="Steps"], [class*="steps"]'); const rewardBtn = Array.from(document.querySelectorAll('a, button, div')).some(el => { const t = (el.textContent || '').trim(); return t === '去领取' || t === '领取' || t.includes('领取') || t.includes('抽奖'); }); const hasSuccessIcon = !!document.querySelector('.icon-success, .success-icon, .complete-icon, .finish-icon, [class*="success"], [class*="complete"]'); const rewardLandingMatch = rewardTextMatch && (stepLike || rewardBtn); return (urlMatch && textMatch) || (textMatch && hasSuccessIcon) || (urlMatch && hasSuccessIcon) || rewardLandingMatch; } // 检测并自动点击阿里云验证码 let captchaToastAt = 0; let captchaManualHintAt = 0; let captchaFailHintAt = 0; function isCaptchaFailedState() { const popup = document.querySelector('#aliyunCaptcha-window-popup.window-show'); if (!popup) return false; const t = (popup.textContent || '').trim(); return t.includes('验证失败') && (t.includes('刷新') || t.includes('重试')); } function isCaptchaVisible() { const popup = document.querySelector('#aliyunCaptcha-window-popup.window-show'); const mask = document.querySelector('#aliyunCaptcha-mask.mask-show'); if (!popup && !mask) return false; const isShown = (el) => { if (!el) return false; const style = el.style || {}; if (style.display && style.display.toLowerCase() === 'none') return false; return true; }; return isShown(popup) || isShown(mask); } let captchaClicking = false; function scheduleHumanClick(el) { if (!el) return; if (captchaClicking) return; captchaClicking = true; (async () => { try { await humanClick(el); } finally { captchaClicking = false; } })(); } function checkAndClickCaptcha() { // 检测阿里云验证码弹窗 const captchaPopup = document.querySelector('#aliyunCaptcha-window-popup.window-show'); const captchaMask = document.querySelector('#aliyunCaptcha-mask.mask-show'); if ((!captchaPopup || captchaPopup.style.display === 'none') && (!captchaMask || captchaMask.style.display === 'none')) return false; if (isCaptchaFailedState()) { const now = Date.now(); if (now - captchaFailHintAt > 6000) { captchaFailHintAt = now; showToastGlobal('❌ 验证码提示“验证失败,请刷新重试”。请手动刷新页面后再继续。'); } return true; } const now = Date.now(); if (now - captchaToastAt > 3500) { captchaToastAt = now; showToastGlobal('⚠️ 检测到验证码,正在尝试自动点击验证...'); } // 查找验证码点击区域(尽量点击“开始智能验证”的可交互区域) const captchaIcon = document.querySelector('#aliyunCaptcha-checkbox-icon'); const captchaBody = document.querySelector('#aliyunCaptcha-checkbox-body'); const captchaLeft = document.querySelector('#aliyunCaptcha-checkbox-left'); const captchaWrapper = document.querySelector('#aliyunCaptcha-checkbox-wrapper'); const clickTarget = captchaIcon || captchaLeft || captchaBody || captchaWrapper; if (clickTarget && clickTarget.offsetParent !== null) { console.log('[星球问卷助手] 检测到阿里云验证码,自动点击...'); // 模拟真实鼠标点击 const rect = clickTarget.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; scheduleHumanClick(clickTarget); return true; } return true; } async function waitForCaptchaClear(maxWaitMs) { const startedAt = Date.now(); const limit = typeof maxWaitMs === 'number' ? maxWaitMs : 20000; if (isCaptchaFailedState()) { showToastGlobal('❌ 验证码验证失败:请先手动刷新页面后重试(脚本无法模拟可信手势绕过)。'); return false; } // A类:通常需要真人点一次“开始智能验证”,脚本事件可能不被接受 if (isCaptchaVisible()) { const now = Date.now(); if (now - captchaManualHintAt > 6000) { captchaManualHintAt = now; showToastGlobal('🧩 需要你手动点击一次验证码里的“开始智能验证”,通过后脚本会继续…'); } } while (Date.now() - startedAt < limit) { if (!isCaptchaVisible()) return true; if (isCaptchaFailedState()) { showToastGlobal('❌ 验证码验证失败:请手动刷新页面后重试。'); return false; } checkAndClickCaptcha(); await new Promise(r => setTimeout(r, 500)); } if (isCaptchaVisible()) { showToastGlobal('🧩 验证码还未通过:请先手动点一次“开始智能验证”。'); return false; } return true; } // 定时检测验证码(每秒检查一次) function startCaptchaWatcher() { setInterval(() => { checkAndClickCaptcha(); }, 1000); } let securityResubmitAttempts = 0; function isVisible(el) { if (!el) return false; return el.offsetParent !== null; } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function getElementCenter(el) { const rect = el.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; } function dispatchMouseMove(x, y) { try { const ev = new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: x, clientY: y }); document.dispatchEvent(ev); } catch (e) {} try { if (typeof PointerEvent === 'function') { const pev = new PointerEvent('pointermove', { bubbles: true, cancelable: true, clientX: x, clientY: y, pointerType: 'mouse' }); document.dispatchEvent(pev); } } catch (e) {} } async function humanMoveTo(el) { if (!el) return; let target; try { el.scrollIntoView({ block: 'center', inline: 'center' }); await sleep(80 + Math.random() * 120); target = getElementCenter(el); } catch (e) { return; } const startX = target.x + (Math.random() * 30 - 15); const startY = target.y + (Math.random() * 30 - 15); const endX = target.x + (Math.random() * 10 - 5); const endY = target.y + (Math.random() * 10 - 5); const steps = 8 + Math.floor(Math.random() * 8); for (let i = 0; i <= steps; i++) { const t = i / steps; const x = startX + (endX - startX) * t + Math.sin(t * Math.PI) * (Math.random() * 6 - 3); const y = startY + (endY - startY) * t + Math.cos(t * Math.PI) * (Math.random() * 6 - 3); dispatchMouseMove(x, y); await sleep(18 + Math.random() * 35); } } async function humanClick(el) { if (!el) return false; await humanMoveTo(el); const { x, y } = getElementCenter(el); const mkMouse = (type) => { try { return new MouseEvent(type, { bubbles: true, cancelable: true, clientX: x, clientY: y }); } catch (e) { return null; } }; const md = mkMouse('mousedown'); const mu = mkMouse('mouseup'); const ck = mkMouse('click'); try { el.focus && el.focus(); } catch (e) {} try { if (md) el.dispatchEvent(md); } catch (e) {} await sleep(40 + Math.random() * 90); try { if (mu) el.dispatchEvent(mu); } catch (e) {} try { if (ck) el.dispatchEvent(ck); } catch (e) {} try { el.click(); } catch (e) {} return true; } function clickPrimarySubmitButton() { // 以用户提供的真实提交按钮为准,优先点击 #SubmitBtnGroup #ctlNext const btn = document.querySelector('#SubmitBtnGroup #ctlNext') || document.querySelector('#ctlNext'); if (btn && isVisible(btn)) { console.log('[星球问卷助手] 点击提交按钮(#ctlNext)'); btn.click(); return true; } return false; } function startSecurityDialogWatcher() { const startedAt = Date.now(); const timer = setInterval(() => { if (Date.now() - startedAt > 15000) { clearInterval(timer); return; } const dialog = document.querySelector('.layui-layer.layui-layer-dialog'); if (!dialog || dialog.style.display === 'none') return; const content = dialog.querySelector('.layui-layer-content'); const text = content ? (content.textContent || '') : ''; if (!text.includes('需要安全校验') && !text.includes('重新提交')) return; if (securityResubmitAttempts >= 3) { clearInterval(timer); return; } securityResubmitAttempts += 1; console.log('[星球问卷助手] 检测到安全校验弹窗,自动确认并重提,第', securityResubmitAttempts, '次'); const okBtn = dialog.querySelector('.layui-layer-btn0'); if (okBtn && isVisible(okBtn)) { okBtn.click(); } clearInterval(timer); const delay = 600 + Math.random() * 600; setTimeout(() => { const clicked = clickPrimarySubmitButton(); if (clicked) { // 继续监听,避免再次弹出 startSecurityDialogWatcher(); } }, delay); }, 400); } function startResumeAnswerDialogWatcher() { const startedAt = Date.now(); const timer = setInterval(() => { if (Date.now() - startedAt > 12000) { clearInterval(timer); return; } const dialog = document.querySelector('.layui-layer.layui-layer-dialog'); if (!dialog || dialog.style.display === 'none') return; const content = dialog.querySelector('.layui-layer-content'); const text = content ? (content.textContent || '') : ''; if (!text.includes('已经回答了部分题目') || !text.includes('是否继续')) return; const cancelBtn = dialog.querySelector('.layui-layer-btn1'); if (cancelBtn && isVisible(cancelBtn)) { console.log('[星球问卷助手] 检测到继续上次回答提示,自动取消'); humanClick(cancelBtn).catch(() => { try { cancelBtn.click(); } catch (e) {} }); clearInterval(timer); } }, 400); } const IFRAME_ACTION_TYPE = 'WJX_HELPER_ACTION'; const IFRAME_SUCCESS_TYPE = 'WJX_HELPER_SUCCESS'; const IFRAME_READY_TYPE = 'WJX_HELPER_READY'; async function runOnceFillAndMaybeSubmit(opts) { if (!isSurveyQuestionPage()) return; startResumeAnswerDialogWatcher(); const captchaOk = await waitForCaptchaClear(25000); if (!captchaOk) return; parseSurvey(); await fillSurveyHuman(); if (opts && opts.submit) { const submitDelay = 900 + Math.random() * 900; setTimeout(() => { autoSubmit(); }, submitDelay); } } function setupIframeMessageListener() { window.addEventListener('message', (evt) => { const data = evt && evt.data; if (!data || typeof data !== 'object') return; if (data.type === IFRAME_ACTION_TYPE) { const action = data.action; if (action === 'RUN_ONCE') { runOnceFillAndMaybeSubmit(data.options || {}).catch(() => {}); } } }); if (isInIframe()) { try { window.parent && window.parent.postMessage({ type: IFRAME_READY_TYPE }, '*'); } catch (e) {} } } function openSurveyModal(url, options) { const existing = document.getElementById('wjx-survey-modal-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.id = 'wjx-survey-modal-overlay'; overlay.style.cssText = ` position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 1000000; display: flex; align-items: center; justify-content: center; padding: 16px; pointer-events: none; `; const modal = document.createElement('div'); modal.id = 'wjx-survey-modal'; modal.style.cssText = ` width: min(960px, 96vw); height: min(88vh, 900px); background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,0.28); display: flex; flex-direction: column; pointer-events: auto; `; const header = document.createElement('div'); header.style.cssText = ` background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; padding: 10px 12px; display: flex; align-items: center; justify-content: space-between; font-size: 14px; font-weight: 600; `; header.innerHTML = `星球问卷助手 - 弹窗填写`; const closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.textContent = '×'; closeBtn.style.cssText = ` background: transparent; border: none; color: #fff; font-size: 22px; line-height: 1; cursor: pointer; opacity: 0.9; `; let sendTimer = null; const cleanup = () => { try { if (sendTimer) clearInterval(sendTimer); } catch (e) {} try { window.removeEventListener('message', onMsg); } catch (e) {} try { overlay.remove(); } catch (e) {} }; closeBtn.onclick = cleanup; header.appendChild(closeBtn); const iframe = document.createElement('iframe'); iframe.style.cssText = ` width: 100%; height: 100%; border: none; background: #fff; `; iframe.referrerPolicy = 'no-referrer-when-downgrade'; iframe.src = url; const hint = document.createElement('div'); hint.style.cssText = ` padding: 8px 12px; background: #fff3cd; color: #856404; font-size: 12px; border-bottom: 1px solid rgba(0,0,0,0.06); `; hint.textContent = '如果弹窗里显示空白/被拦截,说明该问卷站点禁止被iframe嵌入(CSP/X-Frame-Options限制)。'; modal.appendChild(header); modal.appendChild(hint); modal.appendChild(iframe); overlay.appendChild(modal); document.body.appendChild(overlay); const opts = options || {}; const modalUrl = url; const delayMs = Number(opts.delayMs) || 0; const startAt = Date.now() + Math.max(0, delayMs); const trySendAction = () => { try { if (Date.now() < startAt) return false; if (!iframe.contentWindow) return false; iframe.contentWindow.postMessage({ type: IFRAME_ACTION_TYPE, action: 'RUN_ONCE', options: { submit: !!opts.submit } }, '*'); return true; } catch (e) { return false; } }; let attempts = 0; sendTimer = setInterval(() => { attempts += 1; trySendAction(); if (attempts >= 12) clearInterval(sendTimer); }, 500); iframe.addEventListener('load', () => { attempts = 0; }); function onMsg(evt) { const data = evt && evt.data; if (!data || typeof data !== 'object') return; if (data.type === IFRAME_SUCCESS_TYPE) { const countDisplay = document.getElementById('wjx-count-display'); if (countDisplay) countDisplay.textContent = String(getSubmitCount()); if (opts.autoCloseOnSuccess) { cleanup(); } } } window.addEventListener('message', onMsg); closeBtn.onclick = () => { cleanup(); }; } // 自动点击提交按钮 function autoSubmit() { const submitSelectors = [ '#SubmitBtnGroup #ctlNext', '#submit_button', '#ctlNext', '.submitbtn', 'a.submitbtn', 'input[type="submit"]', 'button[type="submit"]', '.btn-submit', '#btnNext', '.button-submit', 'a[onclick*="submit"]', 'a[onclick*="Submit"]', '.mainbtn', '#divSubmit a', '#divSubmit input' ]; for (const sel of submitSelectors) { const btn = document.querySelector(sel); if (btn && btn.offsetParent !== null) { console.log('[星球问卷助手] 找到提交按钮,点击提交...'); humanClick(btn).then(() => { startSecurityDialogWatcher(); }).catch(() => { try { btn.click(); } catch (e) {} startSecurityDialogWatcher(); }); return true; } } const allBtns = document.querySelectorAll('a, button, input[type="button"], input[type="submit"]'); for (const btn of allBtns) { const text = btn.textContent || btn.value || ''; if (text.includes('提交') && btn.offsetParent !== null) { console.log('[星球问卷助手] 找到提交按钮(文字匹配),点击提交...'); humanClick(btn).then(() => { startSecurityDialogWatcher(); }).catch(() => { try { btn.click(); } catch (e) {} startSecurityDialogWatcher(); }); return true; } } console.warn('[星球问卷助手] 未找到提交按钮'); return false; } // 自动循环:成功页处理 function handleSuccessPage() { if (!isAutoMode()) return; const targetUrl = getTargetUrl(); const targetCount = getTargetCount(); const currentCount = incrementCount(); console.log(`[星球问卷助手] 检测到成功页,已完成 ${currentCount} 份`); showToastGlobal(`✅ 第 ${currentCount} 份提交成功!`); if (targetCount > 0 && currentCount >= targetCount) { setAutoMode(false); showToastGlobal(`🎉 已完成目标 ${targetCount} 份!自动模式已停止`); return; } if (targetUrl) { const delay = 2000 + Math.random() * 2000; setTimeout(() => { console.log('[星球问卷助手] 自动打开下一份问卷...'); location.href = targetUrl; }, delay); } } // 简易toast(在成功页也能用,不依赖面板) function showToastGlobal(message) { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #333; color: white; padding: 12px 24px; border-radius: 8px; font-size: 14px; z-index: 999999; `; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s'; setTimeout(() => toast.remove(), 300); }, 2000); } const nameList = `梁金 褚满兴 刘微 田水梅 石月 李阳成 郭小小 赵立敏 潘艺心 程明 林萌 刘圣斌 汪大妍 徐士龙 吴小燕 祝玉华 郑海英 杨芯 程瑞 徐玉茹 丁咕噜 郭丹 董育灵 杜玲 赵垚屹 王乐乐 高璐 李允记 吴龙山 杨明军 张娟娟 梁小清 宋子翔 陈伊伊 郭雪丽 王霏霏 张慧 张琳 迟晓燕 孔超 向峙臻 程小姐 罗曦 张玉梅 蒋冰冰 黄淇淇 石冬冬 于微 王旭升 李怡蓉 王佳乐 哈生 马女士 姜雨旋 马婧 金忠强 康康 胡霖 吴佳 焦焦 单璐 陈晓星 陈盼 邹承娟 马梦珊 吴丽丽 张明亮 连云开 谢玲玲 顾顾 杨苗苗 丁亮 孙婕 杨朵朵 王静思 王小青 曹豆豆 王亚娟 戚鑫 沈兰 杨扬 任静 刘小兔 卓小洁 刘翠梅 于浩洋 董帅芝 朱丽丽 罗懿 孙天宇 陈麟 黄生 杨信红 俞志峰 王劲 蔡秀英 刘新杰 涂梅琴 郭小香 蒋朵朵 陈学艳 木易杨 叶海英 冯卫平 郭清清 叶丹 张丹丽 田丽 杨璃儿 韩娟 朱红娟 郭思蔓 王秀兰 柳颖 马宁 余霞 曹丽娜 吴志红 丁清霞 冷家君 周娜 陈苗苗 李亚雄 张留建 郑佩华 黄新萍 吕赛赛 张芳楼 龚婷 金玫 陈爱军 胡小英 臧雪琳 鲁丹 李晶辉 董佳慧 宗文娟 邱女士 樊媛 杜清 叶春来 郑燕 纪雯 徐女士 史绘霞 沈伟琴 张玮 曹兵 蒙秋梅 舒瓜 史晓娟 丁女士 罗琼 单粉芳 古海伦 贾霞 许莉 郭美丽 胡利娅 叶先生 周文博 韩懿 张霆 李国强 叶群 吴心如 赵然 李兰 王乃玉 余伟锋 杨桃平 孟姣 杨春 凌小姐 吴成斌 陈思佳 张欢 钟家才 苏英歌 徐林华 黄捷 张露 钟小东 吴敏玉 刘薇 程娟娟 李鹏 胡馨誉 刘茜 詹美双 黄忠梅 曹诚贤 陈亚君 吴旭东 王杉杉 郭倩倩 黄成 李莉 琴姐 林麟 金蕊 易先生 林龙英 顾丽 郑婉娟 吴梅霞 张桂婷 罗曼姗 郑粉兰 刘波波 吴传捷 袁德辉 和东琴 李海润 张素霞 宋平 陆平 刘蕊 吴宁莉 张海梅 周玲芳 陈兰 袁新菊 蔡女士 刘则君 仉女士 徐会玲 刘宏珍 吕小凡 严中爱 堵雄风 花朵 闫玉兰 雷雷 蒋琳 郝婧 王俊棋 李银花 王凤云 金茗 李点 许女士 程颖 王天 赵文华 张根鱼 胡桂梅 杨岭 徐琳琳 王兰霞 陈美玉 杨波 吴艳 王强 胡颖 周晓敏 范福珍 岳秀云 李若唯 张明玉 李军超 黄海霞 向锦华 吴丽娜 张雅妮 段平凤 宋倩楠 娄伟波 楚翎 张卫 符湘莲 吴桂英 苏莉 寇秀梅 王闽立 李佳麟 秦依依 吕瑞兰 耿惠 陆淑光 龙梅芳 李科慧 王俊萍 王琛 石永凤 贾蕊 马苗英 王铁利 杨中华 田玉星 国影 倪玉如 张翠萍 曹圣燕 蒋丽芸 贾民丽 文武 张利男 向阳花 黄宏玉 周玉稳 石头 王艺红 李晓娟 陈亚茹 郑晓春 庄小芳 谢蕴韬 李丽萍 余红 王素菊 王立新 金卫红 李万鹏 郭秀芬 巫建韫 罗文霞 杨曼玉 徐丽 吴小建 郑勤勤 胡彩玉 王明艳 张思思 江华 花小c 李珂 杨保青 曹海林 汪艾林 霍贝贝 韩艳萍 黄丽 韩静 李亚云 王志静 廖雅姿 宗娟 王梅 陈昱静 李铁辛 宋瑞平 从涛 蔡苏银 高克梅 吴秀红 郑敏杰 高小利 高艳 黄哲涵 罗源华 赵鑫华 鱼儿 王珍珠 徐爱枝 熊军侠 王守滨 苏育玲 刘春丽 张丽仙 马庆 王毅 姚小慧 叶小新 李桂玲 徐玲玲 陈腾飞 李毓晨 杜春妹 张宇翔 卢艳丽 周慧利 马小抖 高振龙 王明烨 尹彩云 王超 高芳云 朱艳 田春琴 王美玲 米成荣 郑夏梅 关俊峰 卢静 刘耿龙 吴春城 王尔俊 徐海敏 李微 周杰 罗红霞 王春晴 岳琳琳 邢添慈 彭玉红 韩燕妮 李春辉 王均玲 王亚琳 罗林燕 毕灿 张彦芬 胡楠 汤晓青 罗大大 曾云龙 崔华 杨卫 常艳 郑伟 郑乾鹏 姜倩 高源泽 方婷婷 赖丽君 王红琴 丁健 钟琪 吴庆花 齐文雅 颜珍馨 周玉梅 温雅 崔海鹏 梁靖译 邢雅楠 刘远 郭素珍 季小平 丁云飞 许雪兆 于丽敏 王成 任进玉 `; const namePool = nameList.split(/\r?\n/).map(s => s.trim()).filter(Boolean); // 存储解析后的问卷数据 let surveyData = { questions: [] }; const STORAGE_PREFIX = 'wjx_helper_v1:'; const DEFAULT_SETTINGS = { fixedUrl: '', timeMinSec: 8, timeMaxSec: 20 }; const PSYCHO_FIXED = { alpha: 0.8, validityR: 0.5, criterionQ: 13 }; const PSYCHO_BIAS = { left: 0, right: 0 }; let helperSettings = loadSettings(); function getSurveyKey() { const base = (location.origin || '') + (location.pathname || ''); const qs = location.search || ''; return base + qs; } function getRatioStorageKey() { return STORAGE_PREFIX + 'ratio:' + getSurveyKey(); } function getSettingsStorageKey() { return STORAGE_PREFIX + 'settings'; } function getPendingParseKey() { return STORAGE_PREFIX + 'pending_parse'; } function loadSettings() { try { const raw = localStorage.getItem(getSettingsStorageKey()); if (!raw) return { ...DEFAULT_SETTINGS }; const parsed = JSON.parse(raw); return { ...DEFAULT_SETTINGS, ...(parsed || {}) }; } catch (e) { return { ...DEFAULT_SETTINGS }; } } function saveSettings(next) { helperSettings = { ...helperSettings, ...(next || {}) }; try { localStorage.setItem(getSettingsStorageKey(), JSON.stringify(helperSettings)); } catch (e) { } } function normalRandom() { let u = 0; let v = 0; while (u === 0) u = Math.random(); while (v === 0) v = Math.random(); return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); } function normalInv(p) { const a1 = -39.69683028665376; const a2 = 220.9460984245205; const a3 = -275.9285104469687; const a4 = 138.3577518672690; const a5 = -30.66479806614716; const a6 = 2.506628277459239; const b1 = -54.47609879822406; const b2 = 161.5858368580409; const b3 = -155.6989798598866; const b4 = 66.80131188771972; const b5 = -13.28068155288572; const c1 = -0.007784894002430293; const c2 = -0.3223964580411365; const c3 = -2.400758277161838; const c4 = -2.549732539343734; const c5 = 4.374664141464968; const c6 = 2.938163982698783; const d1 = 0.007784695709041462; const d2 = 0.3224671290700398; const d3 = 2.445134137142996; const d4 = 3.754408661907416; const plow = 0.02425; const phigh = 1 - plow; if (p <= 0) return -Infinity; if (p >= 1) return Infinity; let q; let r; if (p < plow) { q = Math.sqrt(-2 * Math.log(p)); return (((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) / ((((d1 * q + d2) * q + d3) * q + d4) * q + 1); } if (phigh < p) { q = Math.sqrt(-2 * Math.log(1 - p)); return -(((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) / ((((d1 * q + d2) * q + d3) * q + d4) * q + 1); } q = p - 0.5; r = q * q; return (((((a1 * r + a2) * r + a3) * r + a4) * r + a5) * r + a6) * q / (((((b1 * r + b2) * r + b3) * r + b4) * r + b5) * r + 1); } function zToCategoryIndex(z, m) { const mm = Math.max(2, Math.min(50, Math.floor(m || 5))); for (let j = 1; j < mm; j++) { const t = normalInv(j / mm); if (z <= t) return j - 1; } return mm - 1; } function computeRhoFromAlpha(alpha, k) { if (!(alpha > 0 && alpha < 1)) return 0.2; if (!(k >= 2)) return 0.2; const denom = (k - alpha * (k - 1)); if (denom <= 0) return 0.2; const rho = alpha / denom; return Math.max(1e-6, Math.min(0.999999, rho)); } function variance(xs) { if (!Array.isArray(xs) || xs.length < 2) return 0; const m = xs.reduce((a, b) => a + b, 0) / xs.length; let s = 0; for (const x of xs) s += (x - m) * (x - m); return s / (xs.length - 1); } function cronbachAlpha(matrix) { if (!Array.isArray(matrix) || matrix.length === 0) return 0; const k = matrix[0].length; if (k < 2) return 0; const totals = matrix.map(r => r.reduce((a, b) => a + b, 0)); const varTotal = variance(totals); if (varTotal === 0) return 0; let sumItemVar = 0; for (let j = 0; j < k; j++) { sumItemVar += variance(matrix.map(r => r[j])); } return (k / (k - 1)) * (1 - (sumItemVar / varTotal)); } function correlation(xs, ys) { if (!Array.isArray(xs) || !Array.isArray(ys) || xs.length !== ys.length || xs.length < 2) return 0; const mx = xs.reduce((a, b) => a + b, 0) / xs.length; const my = ys.reduce((a, b) => a + b, 0) / ys.length; let num = 0; let dx = 0; let dy = 0; for (let i = 0; i < xs.length; i++) { const a = xs[i] - mx; const b = ys[i] - my; num += a * b; dx += a * a; dy += b * b; } const den = Math.sqrt(dx * dy); if (den === 0) return 0; return num / den; } function buildPsychometricPlan(questions) { const enabled = (questions || []).some(q => q && q.psycho); if (!enabled) return null; const targetAlpha = Number(PSYCHO_FIXED.alpha); const validityR = Math.max(0, Math.min(0.999, Number(PSYCHO_FIXED.validityR))); const criterionQInput = parseInt(PSYCHO_FIXED.criterionQ, 10) || 0; let criterionQ = criterionQInput > 0 ? (criterionQInput - 1) : -1; if (criterionQ >= 0) { const cq = (questions || [])[criterionQ]; if (!cq || !cq.psycho) criterionQ = -1; } const items = []; const itemBias = new Map(); (questions || []).forEach(q => { if (!q || !q.psycho) return; if ((q.type === 'radio' || q.type === 'scale' || q.type === 'select') && Array.isArray(q.options) && q.options.length >= 2) { items.push({ kind: 'q', qIndex: q.index, m: q.options.length }); itemBias.set(`q:${q.index}`, q.bias); } else if (q.type === 'matrix' && Array.isArray(q.matrixRows)) { (q.matrixRows || []).forEach((row, rIdx) => { if (row && Array.isArray(row.options) && row.options.length >= 2) { items.push({ kind: 'm', qIndex: q.index, rIndex: rIdx, m: row.options.length }); itemBias.set(`m:${q.index}:${rIdx}`, q.bias); } }); } }); const k = items.length; if (k < 2) return null; const rho = computeRhoFromAlpha(targetAlpha, k); const sigmaE = Math.sqrt((1 / rho) - 1); const theta = normalRandom(); const eta = validityR > 0 ? (validityR * theta + Math.sqrt(1 - validityR * validityR) * normalRandom()) : theta; const choices = new Map(); const criterionValues = []; const totalValues = []; for (const item of items) { const useEta = (criterionQ >= 0) && (item.kind === 'q') && (item.qIndex === criterionQ); const base = useEta ? eta : theta; const key = item.kind === 'q' ? `q:${item.qIndex}` : `m:${item.qIndex}:${item.rIndex}`; const bias = itemBias.get(key); const biasShift = (bias === 'left') ? -1.0 : (bias === 'right') ? 1.0 : 0; const z = base + biasShift + sigmaE * normalRandom(); const idx = zToCategoryIndex(z, item.m); choices.set(key, idx); } try { const nSim = 40; const simMatrix = []; for (let i = 0; i < nSim; i++) { const th = normalRandom(); const et = validityR > 0 ? (validityR * th + Math.sqrt(1 - validityR * validityR) * normalRandom()) : th; const rowVals = []; let total = 0; let crit = null; for (const item of items) { const useEta = (criterionQ >= 0) && (item.kind === 'q') && (item.qIndex === criterionQ); const base = useEta ? et : th; const key = item.kind === 'q' ? `q:${item.qIndex}` : `m:${item.qIndex}:${item.rIndex}`; const bias = itemBias.get(key); const biasShift = (bias === 'left') ? -1.0 : (bias === 'right') ? 1.0 : 0; const z = base + biasShift + sigmaE * normalRandom(); const idx = zToCategoryIndex(z, item.m); const v = idx + 1; rowVals.push(v); if (useEta) crit = v; else total += v; } simMatrix.push(rowVals); if (crit != null) { totalValues.push(total); criterionValues.push(crit); } } const approxAlpha = cronbachAlpha(simMatrix); const approxR = (criterionQ >= 0 && totalValues.length > 2) ? correlation(totalValues, criterionValues) : 0; console.log('[星球问卷助手] 信效度模式启用 | 目标α=', targetAlpha, '题数=', k, '估计α≈', approxAlpha.toFixed(3), '目标r=', validityR, '估计r≈', approxR.toFixed(3)); } catch (e) { } return { choices }; } function loadRatioConfig() { try { const raw = localStorage.getItem(getRatioStorageKey()); if (!raw) return null; return JSON.parse(raw); } catch (e) { return null; } } function saveRatioConfig() { const payload = { version: 1, savedAt: Date.now(), surveyKey: getSurveyKey(), questions: (surveyData.questions || []).map(q => { const base = { type: q.type, fillText: q.fillText, psycho: q.psycho, bias: q.bias, isNumeric: q.isNumeric, minNum: q.minNum, maxNum: q.maxNum }; if (q.type === 'radio' || q.type === 'checkbox' || q.type === 'select' || q.type === 'scale') { base.options = (q.options || []).map(o => ({ percent: o.percent })); } if (q.type === 'matrix') { base.matrixRows = (q.matrixRows || []).map(r => ({ options: (r.options || []).map(o => ({ percent: o.percent })) })); } return base; }) }; try { localStorage.setItem(getRatioStorageKey(), JSON.stringify(payload)); } catch (e) { } } function applyRatioConfigToSurveyData() { const cfg = loadRatioConfig(); if (!cfg || !Array.isArray(cfg.questions)) return; const qs = cfg.questions; (surveyData.questions || []).forEach((q, i) => { const saved = qs[i]; if (!saved || saved.type !== q.type) return; if (typeof saved.fillText === 'string') q.fillText = saved.fillText; if (typeof saved.psycho === 'boolean') q.psycho = saved.psycho; if (typeof saved.bias === 'string') q.bias = saved.bias; if (typeof saved.isNumeric === 'boolean') q.isNumeric = saved.isNumeric; if (typeof saved.minNum === 'number') q.minNum = saved.minNum; if (typeof saved.maxNum === 'number') q.maxNum = saved.maxNum; if ((q.type === 'radio' || q.type === 'checkbox' || q.type === 'select' || q.type === 'scale') && Array.isArray(saved.options) && Array.isArray(q.options) && saved.options.length === q.options.length) { q.options.forEach((opt, idx) => { const v = saved.options[idx] && saved.options[idx].percent; if (typeof v === 'number' && v >= 0 && v <= 100) opt.percent = v; }); } if (q.type === 'matrix' && Array.isArray(saved.matrixRows) && Array.isArray(q.matrixRows) && saved.matrixRows.length === q.matrixRows.length) { q.matrixRows.forEach((row, rIdx) => { const savedRow = saved.matrixRows[rIdx]; if (!savedRow || !Array.isArray(savedRow.options) || !Array.isArray(row.options) || savedRow.options.length !== row.options.length) return; row.options.forEach((opt, oIdx) => { const v = savedRow.options[oIdx] && savedRow.options[oIdx].percent; if (typeof v === 'number' && v >= 0 && v <= 100) opt.percent = v; }); }); } }); } // 添加样式 - 使用原生方式 function addStyle(css) { const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); } addStyle(` #wjx-helper-panel { position: fixed; top: 10px; right: 10px; width: 400px; max-height: 80vh; background: #fff; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 99999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; overflow: hidden; } #wjx-helper-header { background: #1677ff; color: #fff; padding: 12px 15px; font-size: 15px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; cursor: move; } #wjx-helper-header .close-btn { cursor: pointer; font-size: 20px; opacity: 0.8; } #wjx-helper-header .close-btn:hover { opacity: 1; } #wjx-helper-body { padding: 15px; max-height: calc(80vh - 120px); overflow-y: auto; } #wjx-helper-footer { padding: 10px 15px; background: #f8f9fa; border-top: 1px solid #eee; display: flex; gap: 10px; } .wjx-btn { flex: 1; padding: 10px 15px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; } .wjx-btn-primary { background: #1677ff; color: white; } .wjx-btn-primary:hover { background: #4096ff; } .wjx-btn-secondary { background: #e9ecef; color: #495057; } .wjx-btn-secondary:hover { background: #dee2e6; } .wjx-question-item { margin-bottom: 15px; padding: 12px; background: #fafafa; border-radius: 6px; border-left: 3px solid #1677ff; } .wjx-question-title { font-weight: 600; margin-bottom: 10px; color: #333; font-size: 13px; } .wjx-question-type { display: inline-block; padding: 2px 8px; background: #1677ff; color: white; border-radius: 4px; font-size: 11px; margin-left: 8px; } .wjx-psy-chip { display: inline-flex; align-items: center; gap: 6px; margin-left: 8px; padding: 4px 10px; background: #f1f5f9; color: #64748b; border-radius: 6px; font-size: 11px; font-weight: 500; cursor: pointer; user-select: none; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); border: 1px solid transparent; } .wjx-psy-chip:hover { background: #e2e8f0; color: #475569; } .wjx-psy-chip.active { background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); color: #fff; box-shadow: 0 2px 4px rgba(109, 40, 217, 0.2); } .wjx-psy-chip input { display: none; } .wjx-psy-mark { display: none; font-size: 12px; } .wjx-psy-chip.active .wjx-psy-mark { display: inline-block; } .wjx-bias-group { display: flex; align-items: center; gap: 4px; margin-top: 8px; background: #f1f5f9; padding: 4px; border-radius: 8px; } .wjx-bias-btn { flex: 1; padding: 6px 0; border-radius: 6px; border: none; background: transparent; color: #64748b; font-size: 12px; font-weight: 500; cursor: pointer; user-select: none; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .wjx-bias-btn:hover:not(.active) { background: rgba(0,0,0,0.04); color: #475569; } .wjx-bias-btn.active { background: #ffffff; color: #1677ff; font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06); } .wjx-option-item { display: flex; align-items: center; margin: 8px 0; padding: 6px 10px; background: white; border-radius: 4px; } .wjx-option-label { flex: 1; font-size: 12px; color: #555; margin-right: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .wjx-option-input { width: 60px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; text-align: center; font-size: 12px; } .wjx-option-input:focus { outline: none; border-color: #1677ff; } .wjx-percent-label { font-size: 12px; color: #888; margin-left: 4px; } .wjx-fill-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; margin-top: 8px; } .wjx-toggle-btn { position: fixed; top: 10px; right: 10px; width: 50px; height: 50px; border-radius: 50%; background: #1677ff; color: white; border: none; cursor: pointer; font-size: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 99998; display: flex; align-items: center; justify-content: center; } .wjx-toggle-btn:hover { transform: scale(1.1); } .wjx-stats { padding: 10px; background: #e8f4fd; border-radius: 6px; margin-bottom: 15px; font-size: 13px; color: #0066cc; } .wjx-randomize-btn { padding: 4px 10px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px; margin-left: 10px; } .wjx-tabs { display: flex; flex-direction: row; border-bottom: 1px solid #eee; background: #fff; } .wjx-tab { flex: 1; text-align: center; padding: 12px 0; cursor: pointer; font-size: 14px; font-weight: 500; color: #666; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s; } .wjx-tab:hover { color: #1677ff; } .wjx-tab.active { color: #1677ff; border-bottom-color: #1677ff; } .wjx-tab-content { display: none; } .wjx-tab-content.active { display: block; } .wjx-settings-group { margin-bottom: 15px; padding: 12px; background: #f8f9fa; border-radius: 6px; } .wjx-settings-label { display: block; font-size: 13px; font-weight: 600; color: #333; margin-bottom: 8px; } .wjx-settings-input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; box-sizing: border-box; } .wjx-settings-input:focus { outline: none; border-color: #1677ff; } .wjx-settings-row { display: flex; gap: 10px; align-items: center; } .wjx-settings-row .wjx-settings-input { flex: 1; } .wjx-settings-hint { font-size: 11px; color: #888; margin-top: 4px; } .wjx-batch-info { padding: 10px; background: #e8f4fd; border-radius: 6px; margin-bottom: 15px; font-size: 13px; color: #0066cc; } .wjx-batch-progress { margin-top: 10px; padding: 10px; background: #fff3cd; border-radius: 6px; font-size: 13px; color: #856404; display: none; } .wjx-batch-progress.active { display: block; } `); // 解析问卷 - 针对问卷星的DOM结构 function parseSurvey() { surveyData.questions = []; // 问卷星移动端页面:题目容器就是 #divQuestion 内的 .field // 之前用 fieldset/div[id^=q] 会把很多无关节点也当题目,导致解析结果“乱” const questionDivs = document.querySelectorAll('#divQuestion .field.ui-field-contain, #divQuestion .field'); let questionIndex = 0; questionDivs.forEach((div) => { const question = parseQuestion(div, questionIndex); if (question) { surveyData.questions.push(question); questionIndex++; } }); console.log('[星球问卷助手] 解析到', surveyData.questions.length, '道题目'); applyRatioConfigToSurveyData(); return surveyData; } function isSurveyQuestionPage() { return !!document.querySelector('#divQuestion'); } function normalizeUrl(u) { try { const url = new URL(u, location.href); url.hash = ''; return url.toString(); } catch (e) { return (u || '').trim(); } } function consumePendingParseIfMatch() { try { const raw = sessionStorage.getItem(getPendingParseKey()); if (!raw) return false; const payload = JSON.parse(raw); const target = payload && payload.url ? normalizeUrl(payload.url) : ''; const current = normalizeUrl(location.href); if (!target || target !== current) return false; if (!isSurveyQuestionPage()) return false; parseSurvey(); renderQuestions(); const stat = document.querySelector('.wjx-stats strong'); if (stat) stat.textContent = surveyData.questions.length; saveRatioConfig(); sessionStorage.removeItem(getPendingParseKey()); showToast('✅ 解析完成并已保存配比'); return true; } catch (e) { try { sessionStorage.removeItem(getPendingParseKey()); } catch (e2) {} return false; } } // 解析单个题目 function parseQuestion(div, index) { // 问卷星移动端:标题在 .field-label > .topichtml // 示例:
1. 解析: 输入问卷链接,点击解析以加载题目。
2. 配比: 设置每题的选项比例。勾选 ✨ 信效度 后,该题将按算法自动生成,忽略百分比。
3. 设置: 配置自动循环、答题速度等全局参数。
4. 填空: 在“配比”里找到填空题,支持一次输入多个候选答案,用 ;(或 ;)分隔;自动填写时会随机取其中一个填入。若勾选了“随机数字”,则优先按数字范围随机。另外点击ai填空按钮,会把这个题目复制并跳转到kimi,你直接control+v粘贴到kimi对话框即可。
勾选信效度后,可设置偏向:
* 适用于量表题、单选题等。
Q: 怎么自动提交?
A: 在“设置”里开启“自动循环模式”,设置目标份数即可。
Q: 为什么信效度没效果?
A: 确保已勾选题目上的“信效度”开关。