您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动调用 Qwen-VL + Qwen-Max 答题(Base64 图片 + 实时进度)
// ==UserScript== // @name 芯位答题插件(大语言模型调用LLM Qwen通义千问版本) // @namespace http://tampermonkey.net/ // @version 1.01 // @description 自动调用 Qwen-VL + Qwen-Max 答题(Base64 图片 + 实时进度) // @author You // @match *://*.beeline-ai.com/student/* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // ====================== // 配置模块(apiKey 从 localStorage 读取) // ====================== const Config = { VisionModelSelect: "qwen-vl-plus", LLM_modelSelect: "qwen-max", get apiKey() { return localStorage.getItem('qwen_api_key') || ''; }, set apiKey(val) { if (val) { localStorage.setItem('qwen_api_key', val.trim()); } else { localStorage.removeItem('qwen_api_key'); } } }; // ====================== // 状态管理 // ====================== const State = { answerreturn: [], isDone: false, hasTriggered: false }; // ====================== // UI 模块(新增 API Key 输入面板) // ====================== const UI = (function () { let boardanswer, bottonanswer, displaylevel, textanswer, textlevelnum, textleveltag; let apiKeyPanel = null; function createElement(tag, styles = {}, text = '', children = []) { const el = document.createElement(tag); Object.assign(el.style, styles); if (text) el.textContent = text; children.forEach(child => el.appendChild(child)); return el; } function showApiKeyInput() { if (apiKeyPanel) return; apiKeyPanel = createElement('div', { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', padding: '20px', backgroundColor: '#fff', borderRadius: '10px', boxShadow: '0 0 15px rgba(0,0,0,0.3)', zIndex: '99999', width: '300px', textAlign: 'center' }); const title = createElement('h3', { margin: '0 0 15px 0' }, '请输入 Qwen API Key'); const input = createElement('input', { width: '100%', padding: '8px', marginBottom: '10px', border: '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box' }); input.type = 'password'; // 可改为 'text' 如果希望可见 const submitBtn = createElement('button', { padding: '6px 12px', backgroundColor: '#4CAF50', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }, '保存并开始'); submitBtn.onclick = () => { const key = input.value.trim(); if (key) { Config.apiKey = key; document.body.removeChild(apiKeyPanel); apiKeyPanel = null; // 可选:提示成功 alert('API Key 已保存!插件将在检测到题目后自动运行。'); } else { alert('请输入有效的 API Key!'); } }; const note = createElement('p', { fontSize: '12px', color: '#666', marginTop: '10px' }, 'Key 格式如:sk-xxxxxxxxxxxxxxxxxxxxxxxx'); apiKeyPanel.append(title, input, submitBtn, note); document.body.appendChild(apiKeyPanel); // 聚焦输入框 input.focus(); } function createUI() { if (boardanswer) return; boardanswer = createElement('div', { position: 'fixed', bottom: '20px', right: '20px', width: '190px', height: '230px', backgroundColor: '#F5F5F5', borderRadius: '10px', outline: '2px solid #757575', zIndex: '9999' }); bottonanswer = createElement('div', { position: 'absolute', top: '10px', right: '10px', width: '170px', height: '50px', backgroundColor: '#B4D4FF', outline: '2px solid #86B6F6', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center' }); displaylevel = createElement('div', { position: 'absolute', top: '70px', right: '10px', width: '100px', height: '50px', backgroundColor: '#BFD8AF', outline: '2px solid #99BC85', borderRadius: '10px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }); textanswer = createElement('h3', { color: '#1A365D', margin: '0', fontSize: '14px', textAlign: 'center' }, 'answer: 等待题目...'); const textlevel = createElement('h4', { color: '#12372A', margin: '0', fontSize: '12px' }, '题号:'); textlevelnum = createElement('h2', { color: '#12372A', margin: '0', fontSize: '18px' }); textleveltag = createElement('h5', { color: '#12372A', margin: '0', fontSize: '12px' }); bottonanswer.appendChild(textanswer); displaylevel.append(textlevel, textlevelnum, textleveltag); boardanswer.append(displaylevel, bottonanswer); document.body.appendChild(boardanswer); // 检查是否已有 API Key if (!Config.apiKey) { showApiKeyInput(); } } function updateAnswerText(text) { if (textanswer) textanswer.textContent = text; } function updateLevel(num, tag) { if (textlevelnum) textlevelnum.textContent = num; if (textleveltag) textleveltag.textContent = tag; } return { createUI, updateAnswerText, updateLevel }; })(); // ====================== // 工具函数(支持 Base64 图片) // ====================== const Utils = { isValidURL(str) { return typeof str === 'string' && /<img\s/i.test(str); }, extractImgSrc(htmlStr) { const temp = document.createElement('div'); temp.innerHTML = htmlStr; const img = temp.querySelector('img'); return img ? img.src : null; }, async urlToBase64(url) { if (!url || typeof url !== 'string') return null; try { const cleanUrl = url.replace(/ /g, '%20'); const response = await fetch(cleanUrl); if (!response.ok) { console.warn('Image fetch failed:', response.status, url); return null; } const blob = await response.blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(reader.error); reader.readAsDataURL(blob); }); } catch (e) { console.error('urlToBase64 error:', url, e.message || e); return null; } } }; // ====================== // AI 调用模块(使用 Config.apiKey) // ====================== const AI = (function () { async function callModel(model, messages, timeoutMs = 12000) { const apiKey = Config.apiKey; if (!apiKey) { console.error('API Key 未设置!'); return '[API Key 未设置]'; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { // 🔧 修复:移除 URL 末尾空格! const response = await fetch("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model, messages }), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { const errText = await response.text().catch(() => '[无法读取错误]'); console.error(`API ${response.status}:`, errText); return '[调用失败]'; } const data = await response.json().catch(e => { console.error('JSON parse failed:', e); return null; }); if (!data || !data.choices?.[0]?.message?.content) { console.warn('Invalid API response:', data); return '[无有效内容]'; } return data.choices[0].message.content.trim(); } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { console.error(`请求超时 (${timeoutMs}ms)`); } else { console.error('Fetch error:', error); } return '[请求异常]'; } } async function callVisionModel(imageData) { const messages = [{ role: "user", content: [ { type: "text", text: "请准确提取图片中的所有文字和数学公式,仅返回内容,不要任何解释!" }, { type: "image_url", image_url: { url: imageData } } ] }]; return await callModel(Config.VisionModelSelect, messages, 10000); } async function callLLM(prompt) { const messages = [ { role: "system", content: "你是一个答题助手。你必须只输出一行严格的 JSON 对象,不要任何其他字符、解释、空格、换行、Markdown 代码块或标点。格式必须为:{\"question\":\"...\",\"answer\":\"A\",\"diff\":true}" }, { role: "user", content: prompt.trim() } ]; const result = await callModel(Config.LLM_modelSelect, messages, 12000); return result === '[请求异常]' || result === '[调用失败]' || result === '[无有效内容]' || result === '[API Key 未设置]' ? '{}' : result; } return { callVisionModel, callLLM }; })(); // ====================== // 主逻辑(新增 Key 检查) // ====================== const MainLogic = (function () { const qTypeMap = { 'danxuan': 0, 'duoxuan': 1, 'panduan': 2 }; const prompts = [ `这是一道单选题。请严格按以下要求作答:仅输出一行 JSON,格式为 {"question":"题目内容","answer":"A","diff":true}。不要任何额外字符、解释、换行或空格。题目如下:\n`, `这是一道多选题。请严格按以下要求作答:仅输出一行 JSON,格式为 {"question":"题目内容","answer":"A B","diff":true}。不要任何额外字符、解释、换行或空格。题目如下:\n`, `这是一道判断题。请严格按以下要求作答:仅输出一行 JSON,格式为 {"question":"题目内容","answer":"T","diff":true}。不要任何额外字符、解释、换行或空格。题目如下:\n` ]; async function limitConcurrencyWithProgress(tasks, limit = 2, onProgress = null) { const results = []; const total = tasks.length; let completed = 0; for (let i = 0; i < tasks.length; i += limit) { const batch = tasks.slice(i, i + limit); const batchResults = await Promise.all( batch.map(task => task().catch(err => { console.error('Task failed:', err); return '[处理失败]'; })) ); results.push(...batchResults); completed += batch.length; if (onProgress) onProgress(completed, total); } return results; } async function processTopic(topic) { try { let topicTitle = topic.topicTitle || ''; if (Utils.isValidURL(topicTitle)) { const imgUrl = Utils.extractImgSrc(topicTitle); if (imgUrl) { const base64 = await Utils.urlToBase64(imgUrl); if (base64) { topicTitle = await AI.callVisionModel(base64); } else { topicTitle = '[图片加载失败]'; } } } let optionsText = ''; const questions = topic.topicQuestionCoreDtoList || []; const optionPromises = questions.map(async (q) => { let content = q.content || ''; if (Utils.isValidURL(content)) { const imgUrl = Utils.extractImgSrc(content); if (imgUrl) { const base64 = await Utils.urlToBase64(imgUrl); if (base64) { content = await AI.callVisionModel(base64); } else { content = '[图片加载失败]'; } } } return `${q.index}\t${content}\n`; }); const optionChunks = []; for (let i = 0; i < optionPromises.length; i += 2) { const chunk = optionPromises.slice(i, i + 2); const results = await Promise.all(chunk.map(p => p.catch(e => { console.warn('Option parse failed:', e); return `${questions[i]?.index || '?'}\t[图片解析失败]\n`; }))); optionChunks.push(...results); } optionsText = optionChunks.join(''); const qType = qTypeMap[topic.topicType]; if (qType === undefined) { console.warn('Unknown question type:', topic.topicType); return null; } return prompts[qType] + topicTitle + '\n' + optionsText; } catch (e) { console.error('processTopic error:', e); return null; } } function extractJsonFromString(str) { if (typeof str !== 'string') return null; try { return JSON.parse(str); } catch (e) { const start = str.indexOf('{'); const end = str.lastIndexOf('}'); if (start === -1 || end <= start) return null; let jsonStr = str.slice(start, end + 1); try { return JSON.parse(jsonStr); } catch (e2) { try { const fixed = jsonStr.replace(/\\\\/g, '\\'); return JSON.parse(fixed); } catch (e3) { return null; } } } } async function run(data) { if (!Config.apiKey) { UI.createUI(); // 会自动弹出输入框 return; } if (State.hasTriggered) return; State.hasTriggered = true; UI.createUI(); UI.updateAnswerText('检测到作业数据,正在加载题目...'); const homeworkTopicList = data.data?.homeworkTopicList; if (!homeworkTopicList || !Array.isArray(homeworkTopicList)) { UI.updateAnswerText('无效题目数据'); return; } const topicTasks = homeworkTopicList.map(topic => () => processTopic(topic)); const promptList = (await limitConcurrencyWithProgress(topicTasks, 2)) .filter(prompt => prompt && prompt !== '[处理失败]'); if (promptList.length === 0) { UI.updateAnswerText('未解析到有效题目'); return; } UI.updateAnswerText(`共 ${promptList.length} 题,正在分析...`); const llmTasks = promptList.map(prompt => () => AI.callLLM(prompt)); console.log("正在启动LLM任务"); const results = await limitConcurrencyWithProgress(llmTasks, 2, (completed, total) => { UI.updateAnswerText(`正在分析第 ${completed}/${total} 题...`); }); console.log("LLM任务完成"); State.answerreturn = results.map((res, i) => { const parsed = extractJsonFromString(res); if (parsed && parsed.answer !== undefined) { return parsed.answer; } else { console.error('Failed to extract answer from Q' + (i + 1), res); return 'NaN'; } }); State.isDone = true; console.log("答题结果已生成"); UI.updateAnswerText('答题完成!'); updateCurrentDisplay(); } function updateCurrentDisplay() { const indexEl = document.querySelector('.index'); const tagEl = document.querySelector('.tag'); if (!indexEl || !tagEl || !State.isDone) return; const numMatch = indexEl.textContent.trim().match(/^(\d+)/); const num = numMatch ? parseInt(numMatch[1], 10) : 1; const tag = tagEl.textContent; UI.updateLevel(num, tag); if (State.answerreturn.length >= num) { UI.updateAnswerText(`answer: ${State.answerreturn[num - 1]}`); } } return { run, updateCurrentDisplay }; })(); // ====================== // 网络拦截 // ====================== const Interceptor = (function () { function tryTrigger(data) { if (State.hasTriggered) return; try { const hasField = JSON.stringify(data).includes('"homeworkAssessPoints"'); if (hasField && data.code === 200 && data.data?.homeworkTopicList) { console.log('[答题插件] 检测到 homeworkAssessPoints,启动答题逻辑'); MainLogic.run(data); } } catch (e) { console.warn('[答题插件] 数据检测异常:', e); } } const originalXhrOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function () { this.addEventListener('load', function () { if (this.readyState === 4 && this.status === 200) { const ct = this.getResponseHeader('content-type'); if (ct?.includes('application/json')) { try { const data = JSON.parse(this.responseText); tryTrigger(data); } catch (e) { /* ignore */ } } } }); return originalXhrOpen.apply(this, arguments); }; const originalFetch = window.fetch; window.fetch = function () { const promise = originalFetch.apply(this, arguments); promise.then(response => { if (response.ok && response.headers.get('content-type')?.includes('application/json')) { response.clone().json().then(data => tryTrigger(data)).catch(() => {}); } }).catch(() => {}); return promise; }; })(); // ====================== // DOM 监听(题号变化) // ====================== function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } const examContainer = document.querySelector('.exam-container') || document.querySelector('.question-wrapper') || document.querySelector('[class*="topic"]') || document.querySelector('[class*="question"]') || document.body; const debouncedUpdate = debounce(MainLogic.updateCurrentDisplay, 100); const observer = new MutationObserver(debouncedUpdate); observer.observe(examContainer, { childList: true, subtree: true, characterData: true }); })();