// ==UserScript== // @name 小雅粘粘粘 // @namespace http://tampermonkey.net/ // @author Qy // @version 1.7 // @description 小雅粘粘粘:提取题目、生成 AI 作答模板,并保存作答记录 // @match *://*.ai-augmented.com/* // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @connect *.ai-augmented.com // @connect *.aliyuncs.com // @connect xiaoya-notice-dwafgrs416f1w156r1fasd11jt.qyrun.me // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxNTFhMWEiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMjAyNzI3Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8cmVjdCB4PSI2NCIgeT0iNjQiIHdpZHRoPSI4OTYiIGhlaWdodD0iODk2IiByeD0iMjEwIiBmaWxsPSJ1cmwoI2JnKSIvPgogIDxwYXRoIGZpbGw9IiNmNGYwZTgiIGQ9Ik0xOTAgMTcwaDE0MmwxMjMgMjE0LTk3IDEyNHoiLz4KICA8cGF0aCBmaWxsPSIjZjRmMGU4IiBkPSJNNzE0IDE3MGgxNDBMNDI4IDc5MkgyODZ6Ii8+CiAgPHBhdGggZmlsbD0iIzNkYjg3YyIgZD0iTTUwOCA2NjBoMzA0djEzMkg0MTZ6Ii8+Cjwvc3ZnPg== // @run-at document-start // ==/UserScript== (function() { 'use strict'; const SCRIPT_NAME = "小雅粘粘粘"; const SCRIPT_VERSION = "1.7"; console.log(`[${SCRIPT_NAME} v${SCRIPT_VERSION}] 脚本已启动`); let globalQuestionsData =[]; let globalExtractedText = ""; let globalImageAssets = []; let globalPdfQuestions = []; let globalSubmissionResult = { state: 'waiting', message: '等待题目数据加载...' }; let resultPanelVisible = false; let resultFilter = 'all'; let imageDrawerVisible = false; let activeTaskKey = ""; let panelExpanded = false; let lastTriggerWidth = 170; let uiTransitionToken = 0; let uiTransitionTimer = null; let resultPanelTransitionTimer = null; let noticeState = { content: '公告加载中...', version: '', updatedAt: '', fetchedAt: 0, hasUnread: false, loading: false, error: '' }; let globalGroupId = ""; let globalNodeId = ""; let globalPaperId = ""; let globalRecordId = ""; let globalToken = ""; const UI_POSITION_KEY = "xy_magic_box_position_v16"; const LEGACY_UI_POSITION_KEY = "xy_magic_box_position_v13"; const DEFAULT_TRIGGER_WIDTH = 170; const UI_MARGIN = 8; const NOTICE_API = "https://xiaoya-notice-dwafgrs416f1w156r1fasd11jt.qyrun.me/notice"; const NOTICE_CHANNEL = "zhanzhanzhan"; const NOTICE_CACHE_KEY = "xy_zhanzhanzhan_notice_cache_v1"; const NOTICE_READ_KEY = "xy_zhanzhanzhan_notice_read_v1"; const NOTICE_CACHE_TTL = 6 * 60 * 60 * 1000; const PDF_SAVE_TIP = '请选择浏览器内置的“保存为 PDF / 另存为 PDF”。'; const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; const originalFetch = window.fetch; function capturePaperRequestParams(rawUrl) { try { const urlObj = new URL(rawUrl, window.location.origin); globalGroupId = urlObj.searchParams.get('group_id') || globalGroupId; globalNodeId = urlObj.searchParams.get('node_id') || globalNodeId; globalPaperId = urlObj.searchParams.get('paper_id') || globalPaperId; } catch (e) { console.warn(`[${SCRIPT_NAME}] 无法解析题目数据请求参数`, e); } } async function parseFetchResponseAsJson(response) { try { return await response.clone().json(); } catch (jsonError) { const text = await response.clone().text(); return JSON.parse(text); } } function buildTaskKey(groupId, nodeId, paperId) { return [groupId || '', nodeId || '', paperId || ''].join(':'); } function resetCapturedTaskState(reason = '') { if (!activeTaskKey && globalQuestionsData.length === 0) return; console.log(`[${SCRIPT_NAME}] 已清空当前任务缓存${reason ? `:${reason}` : ''}`); globalQuestionsData = []; globalExtractedText = ""; globalImageAssets = []; globalPdfQuestions = []; globalSubmissionResult = { state: 'waiting', message: '等待题目数据加载...' }; resultPanelVisible = false; resultFilter = 'all'; imageDrawerVisible = false; globalGroupId = ""; globalNodeId = ""; globalPaperId = ""; globalRecordId = ""; activeTaskKey = ""; updateUIPanelData(); } function currentUrlMatchesActiveTask() { if (!activeTaskKey) return true; const href = window.location.href; try { const urlObj = new URL(href, window.location.origin); const groupParam = urlObj.searchParams.get('group_id'); const nodeParam = urlObj.searchParams.get('node_id'); const paperParam = urlObj.searchParams.get('paper_id'); if (groupParam && globalGroupId && groupParam !== globalGroupId) return false; if (nodeParam && globalNodeId && nodeParam !== globalNodeId) return false; if (paperParam && globalPaperId && paperParam !== globalPaperId) return false; } catch (e) { } const requiredIds = [globalGroupId, globalNodeId].filter(Boolean); if (requiredIds.length > 0 && !requiredIds.every(id => href.includes(id))) return false; if (globalPaperId && href.includes('paper_id') && !href.includes(globalPaperId)) return false; return true; } function handleRouteChange() { setTimeout(() => { if (!currentUrlMatchesActiveTask()) { resetCapturedTaskState('页面路由已离开当前作业任务'); } }, 80); } function installRouteWatcher() { if (installRouteWatcher.installed) return; installRouteWatcher.installed = true; const wrapHistoryMethod = methodName => { const original = history[methodName]; if (typeof original !== 'function') return; history[methodName] = function(...args) { const result = original.apply(this, args); handleRouteChange(); return result; }; }; wrapHistoryMethod('pushState'); wrapHistoryMethod('replaceState'); window.addEventListener('popstate', handleRouteChange); window.addEventListener('hashchange', handleRouteChange); } XMLHttpRequest.prototype.open = function(method, url, ...args) { this._requestUrl = typeof url === 'string' ? url : url.toString(); return originalXHROpen.apply(this,[method, url, ...args]); }; XMLHttpRequest.prototype.send = function(...args) { this.addEventListener('load', function() { try { if (this._requestUrl && this._requestUrl.includes("/queryStuPaper/v2")) { console.log(`[${SCRIPT_NAME}] 已捕获题目数据包`); capturePaperRequestParams(this._requestUrl); if (this.responseType === 'blob' && this.response) { this.response.text().then(text => processPaperData(JSON.parse(text))); } else if (this.responseType === 'json' && this.response) { processPaperData(this.response); } else if ((this.responseType === '' || this.responseType === 'text') && this.responseText) { processPaperData(JSON.parse(this.responseText)); } } } catch (e) { console.error("[解包失败]", e); } }); return originalXHRSend.apply(this, args); }; window.fetch = async function(input, init) { const response = await originalFetch.apply(this, arguments); try { const rawUrl = typeof input === 'string' ? input : (input && input.url ? input.url : String(input)); if (rawUrl && rawUrl.includes("/queryStuPaper/v2")) { console.log(`[${SCRIPT_NAME}] 已捕获 fetch 题目数据包`); capturePaperRequestParams(rawUrl); parseFetchResponseAsJson(response) .then(processPaperData) .catch(e => console.error(`[${SCRIPT_NAME}] fetch 数据解包失败`, e)); } } catch (e) { console.error(`[${SCRIPT_NAME}] fetch 拦截处理失败`, e); } return response; }; function normalizeEntityMap(entityMap) { if (!entityMap || typeof entityMap !== 'object') return {}; return entityMap; } function getEntityByKey(entityMap, key) { const normalizedMap = normalizeEntityMap(entityMap); if (Object.prototype.hasOwnProperty.call(normalizedMap, key)) return normalizedMap[key]; const stringKey = String(key); if (Object.prototype.hasOwnProperty.call(normalizedMap, stringKey)) return normalizedMap[stringKey]; return null; } function getDataType(data = {}) { return String(data.type || data.blockType || data.kind || '').toUpperCase(); } function getImageSrcFromData(data = {}) { const type = getDataType(data); return data.src || data.imageUrl || data.image_url || data?.data?.src || data?.data?.imageUrl || data?.data?.image_url || ((type.includes('IMAGE') || type === 'IMG') ? (data.url || data.href || data?.data?.url || data?.data?.href || '') : ''); } function getFormulaFromData(data = {}) { return data.teX || data.tex || data.latex || data.formula || data.value || data.content || data.text || data?.data?.teX || data?.data?.tex || data?.data?.latex || ''; } function isImageData(data = {}) { const type = getDataType(data); return type.includes('IMAGE') || type === 'IMG' || !!data.src || !!data.imageUrl || !!data.image_url || !!data?.data?.src; } function isFormulaData(data = {}) { const type = getDataType(data); return type.includes('TEX') || type.includes('MATH') || type.includes('FORMULA'); } function parseRichContent(rawContent) { if (!rawContent) return { text: "", images: [], segments: [] }; let contentObject = null; if (typeof rawContent === 'string') { try { contentObject = JSON.parse(rawContent); } catch (e) { const cleanText = rawContent.replace(/^"|"$/g, '').trim(); return { text: cleanText, images: [], segments: cleanText ? [{ type: 'text', value: cleanText }] : [] }; } } else if (typeof rawContent === 'object') { contentObject = rawContent; } else { const cleanText = String(rawContent).trim(); return { text: cleanText, images: [], segments: cleanText ? [{ type: 'text', value: cleanText }] : [] }; } if (!contentObject || !Array.isArray(contentObject.blocks)) { const fallbackText = typeof contentObject === 'string' ? contentObject.trim() : (typeof rawContent === 'string' ? rawContent.trim() : JSON.stringify(rawContent)); return { text: fallbackText, images: [], segments: fallbackText ? [{ type: 'text', value: fallbackText }] : [] }; } const parts = []; const images = []; const segments = []; const entityMap = normalizeEntityMap(contentObject.entityMap); const pushText = (text) => { const cleanText = String(text || '').trim(); if (cleanText) { parts.push(cleanText); segments.push({ type: 'text', value: cleanText }); } }; const pushImage = (src) => { if (!src) return; parts.push(`[图片]`); images.push({ src, kind: 'image' }); segments.push({ type: 'image', src }); }; const pushFormula = (formula) => { const cleanFormula = String(formula || '').trim(); if (cleanFormula) { parts.push(`[公式: ${cleanFormula}]`); segments.push({ type: 'formula', value: cleanFormula }); } }; const handleMediaData = (data = {}) => { if (isImageData(data)) { pushImage(getImageSrcFromData(data)); } else if (isFormulaData(data)) { pushFormula(getFormulaFromData(data)); } }; contentObject.blocks.forEach(block => { if (!block) return; if (block.type === 'atomic' && block.data) { handleMediaData(block.data); return; } pushText(block.text); if (Array.isArray(block.entityRanges)) { block.entityRanges.forEach(range => { const entity = getEntityByKey(entityMap, range?.key); if (entity && entity.data) handleMediaData({ ...entity.data, type: entity.type || entity.data.type }); }); } }); return { text: parts.join('\n').trim(), images, segments }; } function extractTextFromRichJSON(rawContent) { return parseRichContent(rawContent).text; } function collectImageAssets(questionIndex, source, optionLetter, parsedContent) { if (!parsedContent || !Array.isArray(parsedContent.images)) return; parsedContent.images.forEach(image => { if (!image.src) return; const duplicated = globalImageAssets.some(asset => asset.questionIndex === questionIndex && asset.source === source && asset.optionLetter === optionLetter && asset.src === image.src ); if (duplicated) return; globalImageAssets.push({ questionIndex, source, optionLetter, src: image.src }); }); } function getQuestionTypeLabel(type) { const labels = { 1: "[单选题]", 2: "[多选题]", 4: "[填空题]", 5: "[判断题]", 6: "[简答题]", 7: "[附件题]", 13: "[匹配题]" }; return labels[type] || "[其他]"; } function parseMaybeJson(value) { if (typeof value !== 'string') return value; const trimmed = value.trim(); if (!trimmed) return value; if (/^\d+$/.test(trimmed)) return value; try { return JSON.parse(trimmed); } catch (e) { return value; } } function extractPlainAnswerText(value) { if (value === null || value === undefined) return ''; const parsed = parseMaybeJson(value); if (parsed && typeof parsed === 'object' && Array.isArray(parsed.blocks)) { return parsed.blocks.map(block => block?.text || '').join('\n').trim(); } if (Array.isArray(parsed)) return parsed.map(extractPlainAnswerText).filter(Boolean).join(';'); if (parsed && typeof parsed === 'object') { return Object.values(parsed).map(extractPlainAnswerText).filter(Boolean).join(';'); } return String(parsed).trim(); } function extractRichAnswerDisplay(value) { if (value === null || value === undefined || value === '') return ''; const parsed = parseRichContent(value); if (parsed.text) return parsed.text; return extractPlainAnswerText(value); } function toOptionalNumber(value) { if (value === null || value === undefined || value === '') return null; const num = Number(value); return Number.isFinite(num) ? num : null; } function normalizeAnswerIds(answer) { if (Array.isArray(answer)) return answer.map(item => String(item).trim()).filter(Boolean); if (answer === null || answer === undefined) return []; const parsed = parseMaybeJson(answer); if (Array.isArray(parsed)) return parsed.map(item => String(item).trim()).filter(Boolean); return String(parsed).split(/[,,、\s]+/).map(item => item.trim()).filter(Boolean); } function formatChoiceAnswer(qData, answer) { const ids = normalizeAnswerIds(answer); if (!ids.length) return '未作答'; return ids.map(id => { const option = qData.options?.find(item => String(item.id) === String(id)); if (!option) return id; const text = option.text ? ` ${option.text}` : ''; return `${option.letter}.${text}`; }).join(';'); } function formatFillAnswer(qData, answer) { const parsed = parseMaybeJson(answer); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { const text = extractPlainAnswerText(answer); return text || '未作答'; } const parts = qData.sortedItems.map((item, index) => { const value = extractPlainAnswerText(parsed[item.id]); return `空${index + 1}:${value || '未填'}`; }); return parts.length ? parts.join(';') : '未作答'; } function formatMatchingAnswer(qData, answer) { const parsed = parseMaybeJson(answer); const leftItems = qData.matchingLeftItems || []; const rightItems = qData.matchingRightItems || []; if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { const text = extractPlainAnswerText(answer); return text || '未作答'; } const rightById = new Map(rightItems.map(item => [String(item.id), item])); let hasAnswer = false; const lines = leftItems.map(left => { const rawValue = parsed[left.id] ?? parsed[String(left.id)]; const rightIds = normalizeAnswerIds(rawValue); if (!rightIds.length) return `${left.letter}. ${left.text || ''} => 未匹配`; hasAnswer = true; const rightText = rightIds.map(id => { const right = rightById.get(String(id)); return right ? `${right.letter}. ${right.text || ''}` : id; }).join('、'); return `${left.letter}. ${left.text || ''} => ${rightText}`; }); return hasAnswer ? lines.join('\n') : '未作答'; } function formatAnswerForDisplay(qData, answer) { if (!qData) return extractPlainAnswerText(answer) || '未作答'; if (qData.type === 1 || qData.type === 2 || qData.type === 5) return formatChoiceAnswer(qData, answer); if (qData.type === 4) return formatFillAnswer(qData, answer); if (qData.type === 6) return extractRichAnswerDisplay(answer) || '未作答'; if (qData.type === 7) return '附件题'; if (qData.type === 13) return formatMatchingAnswer(qData, answer); return extractPlainAnswerText(answer) || '未作答'; } function getStandardAnswerDisplay(qData, canShowStandardAnswer) { if (!canShowStandardAnswer || !qData) return ''; if (qData.type === 1 || qData.type === 2 || qData.type === 5) { const correctOptions = qData.options?.filter(item => item.answerChecked === 2) || []; return correctOptions.map(option => { const text = option.text ? ` ${option.text}` : ''; return `${option.letter}.${text}`; }).join(';'); } if (qData.type === 4) { const parts = qData.sortedItems.map((item, index) => { const value = extractPlainAnswerText(item.answer); return value ? `空${index + 1}:${value}` : ''; }).filter(Boolean); return parts.join(';'); } if (qData.type === 6 && qData.sortedItems[0]) { return extractRichAnswerDisplay(qData.sortedItems[0].answer); } return ''; } function getQuestionResultState(answerRecord, qData) { if (!answerRecord) return { label: '未作答', tone: 'muted' }; const score = toOptionalNumber(answerRecord.score); const correct = toOptionalNumber(answerRecord.correct); const fullScore = toOptionalNumber(qData?.score); const hasScore = score !== null; const hasFullScore = fullScore !== null && fullScore > 0; if (correct === 2 || (hasScore && hasFullScore && score >= fullScore)) { return { label: '正确', tone: 'ok' }; } if (hasScore && score > 0) return { label: '部分得分', tone: 'partial' }; if (correct === 1 || (hasScore && score === 0)) return { label: '错误', tone: 'bad' }; return { label: '待批改', tone: 'pending' }; } function buildSubmissionResult(paperData) { const answerRecord = paperData?.answer_record; const answers = answerRecord?.answers; if (!answerRecord || !Array.isArray(answers) || answers.length === 0) { return { state: globalQuestionsData.length ? 'not_submitted' : 'waiting', message: globalQuestionsData.length ? '未检测到已提交作业记录。' : '等待题目数据加载...' }; } const isSubmitted = Number(answerRecord.status) === 2; if (!isSubmitted) { return { state: 'not_submitted', message: '检测到作答记录,但当前任务尚未提交。' }; } const canShowStandardAnswer = paperData?.publish_record?.is_show_answer === true; const answersByQuestionId = new Map(answers.map(ans => [String(ans.question_id), ans])); const totalScore = toOptionalNumber(paperData?.total_score); const actualScore = toOptionalNumber(answerRecord.actual_score ?? answerRecord.score); const answerNum = toOptionalNumber(answerRecord.answer_num || globalQuestionsData.length); const correctNum = toOptionalNumber(answerRecord.answer_correct_num); const questionResults = globalQuestionsData.map(qData => { const answer = answersByQuestionId.get(String(qData.id)); const resultState = getQuestionResultState(answer, qData); const score = toOptionalNumber(answer?.score); const fullScore = toOptionalNumber(qData.score); const scoreText = score !== null ? `${score} / ${fullScore !== null ? fullScore : '-'} 分` : `- / ${fullScore !== null ? fullScore : '-'} 分`; return { index: qData.index, id: qData.id, type: qData.type, typeLabel: getQuestionTypeLabel(qData.type), title: qData.titleText, stateLabel: resultState.label, tone: resultState.tone, scoreText, userAnswer: formatAnswerForDisplay(qData, answer?.answer), standardAnswer: getStandardAnswerDisplay(qData, canShowStandardAnswer) }; }); return { state: 'submitted', canShowStandardAnswer, totalScore, actualScore, answerNum: answerNum !== null ? answerNum : questionResults.length, correctNum, questionResults }; } function processPaperData(jsonData) { if (!jsonData || !jsonData.data || !jsonData.data.questions) { console.warn(`[${SCRIPT_NAME}] 题目数据结构不完整,已跳过处理`); return; } globalPaperId = globalPaperId || jsonData.data.paper_id || jsonData.data.paperId || jsonData.data.id || ""; if(!globalGroupId) globalGroupId = jsonData.data.group_id; let questionsArray = jsonData.data.questions; globalQuestionsData =[]; globalImageAssets = []; globalPdfQuestions = []; let resultText = `【小雅粘粘粘:AI 作答模板】 请根据以下题目,严格按照指定格式输出答案,不要输出解析、注释或额外说明。 【输出格式要求】 单选/判断题格式:[题号] => [大写字母] (如 1 => A) 多选题格式:[题号] => [大写字母] (多选请用英文逗号分隔,如 2 => A,C) 填空题格式:[题号] => [空1] | [空2] (如 3 => const | let) 简答题格式:[题号] => [完整文字答案] 匹配题格式:[题号] => A:a,d | B:b,c 附件题无需回答。 --- 以下为考试内容 --- `; questionsArray.forEach((q, index) => { const questionIndex = index + 1; const parsedTitle = parseRichContent(q.title); let qTitle = parsedTitle.text; let qTypeStr = getQuestionTypeLabel(q.type); const resultOptions = []; const matchingLeftItems = []; const matchingRightItems = []; collectImageAssets(questionIndex, 'title', null, parsedTitle); resultText += `${questionIndex}. ${qTitle} ${qTypeStr}\n`; let sortedItems = Array.isArray(q.answer_items) ? [...q.answer_items] : []; if (q.answer_items_sort && Array.isArray(q.answer_items)) { const sortIds = String(q.answer_items_sort).split(','); sortedItems = sortIds.map(id => q.answer_items.find(item => String(item.id) === String(id))).filter(Boolean); } const pdfQuestion = { index: questionIndex, type: q.type, typeLabel: qTypeStr, titleSegments: parsedTitle.segments && parsedTitle.segments.length ? parsedTitle.segments : (qTitle ? [{ type: 'text', value: qTitle }] : []), options: [], matchingLeftItems: [], matchingRightItems: [], blankCount: sortedItems.length }; if (q.type === 1 || q.type === 2 || q.type === 5) { let letterCharCode = 65; sortedItems.forEach(opt => { const optionLetter = String.fromCharCode(letterCharCode); const parsedOption = parseRichContent(opt.value); let optText = parsedOption.text; collectImageAssets(questionIndex, 'option', optionLetter, parsedOption); if (q.type === 5 && !optText) { optText = optionLetter === 'A' ? '正确' : '错误'; } pdfQuestion.options.push({ letter: optionLetter, text: optText, segments: parsedOption.segments && parsedOption.segments.length ? parsedOption.segments : (optText ? [{ type: 'text', value: optText }] : []) }); resultOptions.push({ id: opt.id, letter: optionLetter, text: optText, answerChecked: opt.answer_checked }); resultText += ` ${optionLetter}. ${optText}\n`; letterCharCode++; }); } else if (q.type === 4) { resultText += ` (本题共 ${sortedItems.length} 个填空)\n`; } else if (q.type === 7) { resultText += ` 附件题无需回答。\n`; } else if (q.type === 13) { const leftRawItems = sortedItems.filter(item => item && item.is_target_opt !== true); const rightRawItems = sortedItems.filter(item => item && item.is_target_opt === true); resultText += ` 左侧:\n`; leftRawItems.forEach((opt, itemIndex) => { const optionLetter = String.fromCharCode(65 + itemIndex); const parsedOption = parseRichContent(opt.value); const optText = parsedOption.text; const segments = parsedOption.segments && parsedOption.segments.length ? parsedOption.segments : (optText ? [{ type: 'text', value: optText }] : []); collectImageAssets(questionIndex, 'option', optionLetter, parsedOption); const itemData = { id: opt.id, letter: optionLetter, text: optText, segments }; matchingLeftItems.push(itemData); pdfQuestion.matchingLeftItems.push(itemData); resultText += ` ${optionLetter}. ${optText}\n`; }); resultText += `\n 右侧候选:\n`; rightRawItems.forEach((opt, itemIndex) => { const optionLetter = String.fromCharCode(97 + itemIndex); const parsedOption = parseRichContent(opt.value); const optText = parsedOption.text; const segments = parsedOption.segments && parsedOption.segments.length ? parsedOption.segments : (optText ? [{ type: 'text', value: optText }] : []); collectImageAssets(questionIndex, 'option', optionLetter, parsedOption); const itemData = { id: opt.id, letter: optionLetter, text: optText, segments }; matchingRightItems.push(itemData); pdfQuestion.matchingRightItems.push(itemData); resultText += ` ${optionLetter}. ${optText}\n`; }); } resultText += "\n"; globalQuestionsData.push({ index: questionIndex, id: q.id, type: q.type, score: q.score, titleText: qTitle, options: resultOptions, matchingLeftItems, matchingRightItems, sortedItems: sortedItems }); globalPdfQuestions.push(pdfQuestion); }); globalExtractedText = resultText; activeTaskKey = buildTaskKey(globalGroupId, globalNodeId, globalPaperId); globalSubmissionResult = buildSubmissionResult(jsonData.data); resultPanelVisible = globalSubmissionResult.state === 'submitted'; resultFilter = 'all'; imageDrawerVisible = false; console.log("✅ 数据清洗完毕,已生成终极Prompt!"); createUIPanel(); } function getToken() { const cookies = document.cookie.split('; '); for (let cookie of cookies) { const[name, value] = cookie.split('='); if (name.includes('prd-access-token')) return value; } return null; } async function fetchRecordId() { if (!globalToken) globalToken = getToken(); if (!globalToken) throw new Error("未获取到 Token"); if (!globalNodeId || !globalGroupId) throw new Error("未获取到课程或节点参数"); const url = `${window.location.origin}/api/jx-iresource/survey/course/task/flow/v2?node_id=${globalNodeId}&group_id=${globalGroupId}`; const response = await fetch(url, { headers: { 'authorization': `Bearer ${globalToken}`, 'content-type': 'application/json' }, credentials: 'include' }); if (!response.ok) { throw new Error(`Record ID 请求失败:${response.status}`); } const data = await response.json(); if (data.success && data.data) { if (data.data.task_flow_record?.[0]?.answer_record_id) return data.data.task_flow_record[0].answer_record_id; if (data.data.task_flow_template?.[0]?.answer_record_id) return data.data.task_flow_template[0].answer_record_id; } throw new Error("无法获取 Record ID"); } async function submitSingleAnswer(questionId, answerPayload) { if (!globalPaperId) throw new Error("未获取到 paper_id"); const requestBody = { record_id: globalRecordId, question_id: questionId, answer: answerPayload, ext_answer: "", group_id: globalGroupId, paper_id: globalPaperId, is_try: 0 }; const response = await fetch(`${window.location.origin}/api/jx-iresource/survey/answer`, { method: 'POST', headers: { 'accept': '*/*', 'authorization': `Bearer ${globalToken}`, 'content-type': 'application/json; charset=UTF-8' }, credentials: 'include', body: JSON.stringify(requestBody) }); let responseData = null; try { responseData = await response.clone().json(); } catch (e) { responseData = null; } if (!response.ok || responseData?.success === false) { const message = responseData?.message || responseData?.error || response.statusText || "未知错误"; throw new Error(`保存作答失败:${message}`); } return responseData; } function parseAIAnswerBlocks(aiText) { const blocks = []; let current = null; String(aiText || '').split(/\r?\n/).forEach(line => { const match = line.match(/^\s*(\d+)\s*=>\s*(.*)$/); if (match) { if (current) { current.answer = current.lines.join('\n').trim(); blocks.push(current); } current = { index: parseInt(match[1], 10), lines: [match[2].trim()] }; return; } if (current) current.lines.push(line.trimEnd()); }); if (current) { current.answer = current.lines.join('\n').trim(); blocks.push(current); } return blocks.filter(block => Number.isFinite(block.index) && block.answer); } function createRichTextAnswer(text, questionId) { const lines = String(text || '').trim().split(/\r?\n/); return JSON.stringify({ blocks: lines.map((line, index) => ({ key: `ans-${index}`, text: line, type: 'unstyled', depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} })), entityMap: {} }); } function createMatchingAnswerPayload(qData, answerText) { const leftByLetter = new Map((qData.matchingLeftItems || []).map(item => [String(item.letter).toUpperCase(), item])); const rightByLetter = new Map((qData.matchingRightItems || []).map(item => [String(item.letter).toLowerCase(), item])); const payload = {}; const segments = String(answerText || '').split(/[||;\n;]+/).map(item => item.trim()).filter(Boolean); segments.forEach(segment => { const match = segment.match(/^\s*([A-Za-z])\s*(?:=>|->|[::=])\s*(.+?)\s*$/); if (!match) { console.warn(`[${SCRIPT_NAME}] 匹配题答案片段无法识别:${segment}`); return; } const leftLetter = match[1].toUpperCase(); const leftItem = leftByLetter.get(leftLetter); if (!leftItem) { console.warn(`[${SCRIPT_NAME}] 匹配题左侧字母无效:${leftLetter}`); return; } const rightIds = match[2] .split(/[,,、\s]+/) .map(token => token.trim().replace(/[.。]/g, '').toLowerCase()) .filter(Boolean) .map(letter => { const rightItem = rightByLetter.get(letter); if (!rightItem) { console.warn(`[${SCRIPT_NAME}] 匹配题右侧字母无效:${letter}`); return null; } return rightItem.id; }) .filter(Boolean); if (rightIds.length > 0) { payload[leftItem.id] = rightIds.join(','); } }); return payload; } async function executeFill(aiText) { try { globalRecordId = await fetchRecordId(); console.log("✅ 成功获取 Record ID: ", globalRecordId); } catch (e) { alert("初始化提交参数失败,请刷新页面重试!\n" + e.message); return; } const answerBlocks = parseAIAnswerBlocks(aiText); let successCount = 0; let failureCount = 0; for (let answerBlock of answerBlocks) { let qIndex = answerBlock.index; let qAnswerStr = answerBlock.answer.trim(); let qData = globalQuestionsData.find(q => q.index === qIndex); if (!qData) continue; if (qData.type === 7) continue; let answerPayload =[]; if (qData.type === 1 || qData.type === 2 || qData.type === 5) { let letters = qAnswerStr .split(/[,,、\s]+/) .map(s => s.trim().toUpperCase()) .filter(Boolean); letters.forEach(letter => { let targetIndex = letter.charCodeAt(0) - 65; if (qData.sortedItems[targetIndex]) { answerPayload.push(qData.sortedItems[targetIndex].id); } }); } else if (qData.type === 4) { let blanks = qAnswerStr.split(/[||]/).map(s => s.trim()); let fillObject = {}; qData.sortedItems.forEach((item, idx) => { if (blanks[idx]) { fillObject[item.id] = blanks[idx]; } }); answerPayload = [fillObject]; } else if (qData.type === 6 && qAnswerStr) { answerPayload = [createRichTextAnswer(qAnswerStr, qData.id)]; } else if (qData.type === 13) { const matchObject = createMatchingAnswerPayload(qData, qAnswerStr); if (Object.keys(matchObject).length > 0) { answerPayload = [matchObject]; } } if (answerPayload.length > 0) { try { await submitSingleAnswer(qData.id, answerPayload); successCount++; console.log(`[${SCRIPT_NAME}] 第 ${qIndex} 题作答已保存,Payload:`, answerPayload); } catch (error) { failureCount++; console.error(`[${SCRIPT_NAME}] 第 ${qIndex} 题保存失败`, error); } } } if (successCount > 0) { alert(`${SCRIPT_NAME} 已完成:成功保存 ${successCount} 道题,失败 ${failureCount} 道。点击确定后刷新页面查看结果。`); window.location.reload(); } else { alert(`${SCRIPT_NAME} 未保存任何答案,请检查 AI 输出格式或控制台错误。`); } } function setUIStatus(message, isError = false) { const status = document.getElementById('xy-status'); if (status) { status.innerText = message; status.style.color = isError ? '#dc2626' : '#6b7280'; } const floatingStatus = document.getElementById('xy-floating-status'); if (floatingStatus) { floatingStatus.innerText = globalQuestionsData.length > 0 ? `${globalQuestionsData.length} 道题已同步` : '等待同步题目'; } } function safeLocalStorageGet(key) { try { return localStorage.getItem(key); } catch (error) { console.warn(`[${SCRIPT_NAME}] localStorage 读取失败:${key}`, error); return null; } } function safeLocalStorageSet(key, value) { try { localStorage.setItem(key, value); return true; } catch (error) { console.warn(`[${SCRIPT_NAME}] localStorage 写入失败:${key}`, error); return false; } } function isNewerVersion(latest, current) { if (!latest) return false; const latestParts = String(latest).split('.').map(part => Number(part) || 0); const currentParts = String(current).split('.').map(part => Number(part) || 0); const length = Math.max(latestParts.length, currentParts.length); for (let index = 0; index < length; index++) { const latestPart = latestParts[index] || 0; const currentPart = currentParts[index] || 0; if (latestPart > currentPart) return true; if (latestPart < currentPart) return false; } return false; } function getNoticeFingerprint(record = noticeState) { return [record.version || '', record.updatedAt || '', record.content || ''].join('|'); } function readNoticeCache() { const cached = safeLocalStorageGet(NOTICE_CACHE_KEY); if (!cached) return null; try { const parsed = JSON.parse(cached); if (!parsed || typeof parsed !== 'object') return null; return { content: String(parsed.content || '暂无公告'), version: String(parsed.version || ''), updatedAt: String(parsed.updatedAt || ''), fetchedAt: Number(parsed.fetchedAt) || 0 }; } catch (error) { console.warn(`[${SCRIPT_NAME}] 公告缓存解析失败`, error); return null; } } function applyNoticeRecord(record, options = {}) { const normalized = { content: String(record?.content || '暂无公告'), version: String(record?.version || ''), updatedAt: String(record?.updatedAt || ''), fetchedAt: Number(record?.fetchedAt) || Date.now() }; const readFingerprint = safeLocalStorageGet(NOTICE_READ_KEY) || ''; noticeState = { ...noticeState, ...normalized, loading: false, error: options.error || '', hasUnread: options.preventUnread ? false : normalized.content !== '暂无公告' && !!getNoticeFingerprint(normalized) && getNoticeFingerprint(normalized) !== readFingerprint }; updateNoticeUI(); } function updateNoticeUI() { const dot = document.querySelector('.xy-floating-dot'); if (dot) dot.classList.toggle('xy-notice-unread', noticeState.hasUnread); const body = document.getElementById('xy-notice-body'); if (body) body.textContent = noticeState.content || '暂无公告'; const meta = document.getElementById('xy-notice-meta'); if (meta) { const timeText = noticeState.updatedAt ? new Date(noticeState.updatedAt).toLocaleString() : '暂无更新时间'; meta.textContent = noticeState.error ? `${noticeState.error} · ${timeText}` : timeText; } const versionBadge = document.getElementById('xy-notice-version-badge'); if (versionBadge) { const showVersion = isNewerVersion(noticeState.version, SCRIPT_VERSION); versionBadge.style.display = showVersion ? 'inline-flex' : 'none'; versionBadge.textContent = showVersion ? `v${noticeState.version} 可用` : ''; } const toggle = document.getElementById('xy-notice-toggle'); if (toggle) { const needsToggle = (noticeState.content || '').length > 70 || (noticeState.content || '').includes('\n'); toggle.style.display = needsToggle ? 'inline-flex' : 'none'; } const refresh = document.getElementById('xy-notice-refresh'); if (refresh) { refresh.disabled = noticeState.loading; refresh.title = noticeState.loading ? '公告刷新中...' : '刷新公告'; refresh.innerHTML = renderIconSvg('refresh', 12); } } function fetchNoticeWithGM() { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('GM_xmlhttpRequest 不可用')); return; } GM_xmlhttpRequest({ method: 'POST', url: NOTICE_API, data: JSON.stringify({ action: 'get_notice', channel: NOTICE_CHANNEL, client: `xiaoya-zhanzhanzhan-v${SCRIPT_VERSION}` }), headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, timeout: 10000, onload: response => { if (response.status < 200 || response.status >= 300) { reject(new Error(`公告请求失败:${response.status}`)); return; } try { resolve(JSON.parse(response.responseText)); } catch (error) { reject(new Error('公告响应解析失败')); } }, onerror: () => reject(new Error('公告网络请求失败')), ontimeout: () => reject(new Error('公告网络请求超时')) }); }); } async function fetchNoticeRecord() { try { const response = await fetch(NOTICE_API, { method: 'POST', body: JSON.stringify({ action: 'get_notice', channel: NOTICE_CHANNEL, client: `xiaoya-zhanzhanzhan-v${SCRIPT_VERSION}` }) }); if (!response.ok) throw new Error(`公告请求失败:${response.status}`); return await response.json(); } catch (error) { console.warn(`[${SCRIPT_NAME}] 页面 fetch 获取公告失败,尝试 GM_xmlhttpRequest`, error); return fetchNoticeWithGM(); } } async function runNoticeCheck(forceRefresh = false) { const cached = readNoticeCache(); if (cached) applyNoticeRecord(cached); if (!forceRefresh && cached && Date.now() - cached.fetchedAt < NOTICE_CACHE_TTL) return; noticeState.loading = true; updateNoticeUI(); try { const data = await fetchNoticeRecord(); if (!data || data.ok !== true) throw new Error('公告返回数据异常'); if (Number(data.apiVersion) < 2 || data.channel !== NOTICE_CHANNEL) { throw new Error('公告服务尚未完成多频道升级'); } const record = { content: String(data.content || '暂无公告'), version: String(data.version || ''), updatedAt: String(data.updatedAt || ''), fetchedAt: Date.now() }; safeLocalStorageSet(NOTICE_CACHE_KEY, JSON.stringify(record)); applyNoticeRecord(record); } catch (error) { console.warn(`[${SCRIPT_NAME}] 公告加载失败`, error); if (cached) { applyNoticeRecord(cached, { error: '公告刷新失败,正在显示缓存' }); } else { applyNoticeRecord({ content: '公告加载失败', version: '', updatedAt: '', fetchedAt: Date.now() }, { error: '公告加载失败', preventUnread: true }); } } } function markNoticeAsRead() { if (!noticeState.hasUnread) return; safeLocalStorageSet(NOTICE_READ_KEY, getNoticeFingerprint()); noticeState.hasUnread = false; updateNoticeUI(); } function toggleNoticeBody() { const body = document.getElementById('xy-notice-body'); const toggle = document.getElementById('xy-notice-toggle'); if (!body || !toggle) return; const expanded = body.classList.toggle('xy-notice-expanded'); toggle.textContent = expanded ? '收起' : '展开'; } function initializeNoticeSystem() { const cached = readNoticeCache(); if (cached) applyNoticeRecord(cached); runNoticeCheck(false); } function getImageAssetLabel(asset) { if (asset.source === 'option' && asset.optionLetter) { return `第 ${asset.questionIndex} 题选项 ${asset.optionLetter} 图片`; } return `第 ${asset.questionIndex} 题题干图片`; } async function fetchImageBlobWithPageFetch(src) { const response = await fetch(src, { credentials: 'include', redirect: 'follow' }); if (!response.ok) throw new Error(`图片请求失败:${response.status}`); return response.blob(); } function fetchImageBlobWithGM(src) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('GM_xmlhttpRequest 不可用')); return; } GM_xmlhttpRequest({ method: 'GET', url: src, responseType: 'blob', withCredentials: true, timeout: 15000, onload: response => { if (response.status >= 200 && response.status < 300 && response.response) { resolve(response.response); } else { reject(new Error(`GM 图片请求失败:${response.status}`)); } }, onerror: () => reject(new Error('GM 图片请求失败')), ontimeout: () => reject(new Error('GM 图片请求超时')) }); }); } async function getImageBlob(src) { try { return await fetchImageBlobWithPageFetch(src); } catch (fetchError) { console.warn(`[${SCRIPT_NAME}] 页面 fetch 获取图片失败,尝试 GM_xmlhttpRequest`, fetchError); return fetchImageBlobWithGM(src); } } async function mapLimit(list, limit, worker) { const results = new Array(list.length); let cursor = 0; const runners = Array.from({ length: Math.min(limit, list.length) }, async () => { while (cursor < list.length) { const currentIndex = cursor++; results[currentIndex] = await worker(list[currentIndex], currentIndex); } }); await Promise.all(runners); return results; } function blobToDataUrl(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(new Error('图片编码失败')); reader.readAsDataURL(blob); }); } async function hydratePdfImages(assets) { const uniqueSrcs = Array.from(new Set((assets || []).map(asset => asset.src).filter(Boolean))); if (uniqueSrcs.length === 0) return new Map(); const results = await mapLimit(uniqueSrcs, 3, async src => { try { const blob = await getImageBlob(src); const dataUrl = await blobToDataUrl(blob); return { src, ok: true, dataUrl }; } catch (error) { console.warn(`[${SCRIPT_NAME}] PDF 图片读取失败`, error); return { src, ok: false, error: error?.message || String(error) }; } }); const imageMap = new Map(); results.forEach(result => imageMap.set(result.src, result)); return imageMap; } function getPdfFileName() { const now = new Date(); const pad = value => String(value).padStart(2, '0'); const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; return `${SCRIPT_NAME}_AI图文题目_${stamp}.pdf`; } function renderPrintSegments(segments, imageMap) { if (!segments || segments.length === 0) { return '
[内容为空]
'; } return segments.map(segment => { if (segment.type === 'image') { const imageRecord = imageMap.get(segment.src); if (!imageRecord || !imageRecord.ok || !imageRecord.dataUrl) { return '
[图片读取失败]
'; } return `
[图片]
题目图片
`; } if (segment.type === 'formula') { return `
[公式: ${escapeHTML(segment.value || '')}]
`; } return `
${escapeHTML(segment.value || '')}
`; }).join(''); } function renderPrintQuestion(question, imageMap) { let detailHtml = ''; if (question.options && question.options.length > 0) { detailHtml = `
${question.options.map(option => `
${escapeHTML(option.letter)}.
${renderPrintSegments(option.segments, imageMap)}
`).join('')}
`; } else if (question.type === 4) { detailHtml = `
(本题共 ${escapeHTML(question.blankCount)} 个填空)
`; } else if (question.type === 7) { detailHtml = '
附件题无需回答。
'; } else if (question.type === 13) { detailHtml = `
左侧:
${(question.matchingLeftItems || []).map(item => `
${escapeHTML(item.letter)}.
${renderPrintSegments(item.segments, imageMap)}
`).join('')}
右侧候选:
${(question.matchingRightItems || []).map(item => `
${escapeHTML(item.letter)}.
${renderPrintSegments(item.segments, imageMap)}
`).join('')}
`; } return `

${escapeHTML(question.index)}. ${escapeHTML(question.typeLabel)}

${renderPrintSegments(question.titleSegments, imageMap)}
${detailHtml}
`; } function buildPrintDocumentHtml(pdfQuestions, imageMap, title) { const questionsHtml = pdfQuestions.length ? pdfQuestions.map(question => renderPrintQuestion(question, imageMap)).join('') : '
未读取到题目数据。
'; return ` ${escapeHTML(title)}

${escapeHTML(SCRIPT_NAME)} AI 图文题目

浏览器原生打印版:题目文字可选中、可复制、可搜索。
${escapeHTML(PDF_SAVE_TIP)}
请根据以下题目作答,严格按指定格式返回答案,不要输出解析、注释或额外说明。
单选/判断:1 => A  多选:2 => A,C  填空:3 => const | let
简答:21 => 完整文字答案  匹配:10 => A:a,d | B:b,c  附件题无需回答
${questionsHtml}