diff --git a/build.js b/build.js index 1723896..b0b7e73 100644 --- a/build.js +++ b/build.js @@ -110,11 +110,13 @@ const BUILD_CONFIGS = [ 'adapters/haofenshuyize/adapter.js', 'adapters/keewing/selectors.js', 'adapters/keewing/adapter.js', + 'adapters/wuyuetong/selectors.js', + 'adapters/wuyuetong/adapter.js', ], header: { name: 'AI-Marker-Suite', namespace: 'https://aimarking.five-plus-one.com/', - description: 'AI自动批改助手,支持智学网、七天网络、好分数、五岳阅卷、华翰云、光大阅卷、云阅卷、新教育、鑫考、润建、54学霸、九科星、慧阅卷、乐华阅卷、慧学星、粤教翔云、云阅卷(好分数)、科耘等平台。自动识别答案、智能评分、自动提交!', + description: 'AI自动批改助手,支持智学网、七天网络、好分数、五岳阅卷、华翰云、光大阅卷、云阅卷、新教育、鑫考、润建、54学霸、九科星、慧阅卷、乐华阅卷、慧学星、粤教翔云、云阅卷(好分数)、科耘、威科姆等平台。自动识别答案、智能评分、自动提交!', author: '5plus1', match: [ 'https://www.zhixue.com/*', @@ -141,6 +143,7 @@ const BUILD_CONFIGS = [ '*://rrtcp.gdedu.gov.cn/*', 'https://haofenshuyize.com/*', '*://kaoshi.keewing.com/*', + '*://wyna.onlyets.com/*', ], include: [ '/^https?:\/\/\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+\\//', // IP:端口 部署(光大阅卷等) @@ -167,6 +170,7 @@ const BUILD_CONFIGS = [ 'zhengwuyun-1304451002.cos.ap-guangzhou.myqcloud.com', 'pj-sdlc.cn-bj.ufileos.com', 'kyasource.keewing.com', + 'ry-mark.oss-cn-beijing.aliyuncs.com', '*', ], runAt: 'document-idle', diff --git a/src/adapters/wuyuetong/adapter.js b/src/adapters/wuyuetong/adapter.js new file mode 100644 index 0000000..11a51f2 --- /dev/null +++ b/src/adapters/wuyuetong/adapter.js @@ -0,0 +1,314 @@ +// ========== 威科姆(悦卷通)阅卷适配器 ========== +// jQuery 传统页面,域名: wyna.onlyets.com +// 特点:多小题输入框,OSS 图片,键盘/鼠标/步骤三种打分模式 +// 图片使用阿里云 OSS,带裁剪参数和安全 token + +const WuyuetongAdapter = { + name: '威科姆阅卷', + id: 'wuyuetong', + urlPatterns: ['*://wyna.onlyets.com/*'], + iconUrl: '', + + shouldInitialize() { + return window.location.hostname.includes('wyna.onlyets.com'); + }, + + isMarkingPage() { + return /\/mark\/v\d+\/view\//i.test(window.location.pathname); + }, + + async detectMarkingPage() { + if (!this.isMarkingPage()) { + console.log('[威科姆] 当前不在阅卷页面 (pathname:', window.location.pathname, ')'); + return false; + } + + console.log('[威科姆] 开始检测批改页面...'); + try { + const result = await Promise.race([ + waitForElement(WUYUETONG_SELECTORS.PAGE_DETECT_IMAGE).then(() => 'answer-image'), + waitForElement(WUYUETONG_SELECTORS.PAGE_DETECT_INPUT).then(() => 'score-input'), + waitForElement(WUYUETONG_SELECTORS.PAGE_DETECT_SUBMIT).then(() => 'submit-btn'), + ]).catch(() => null); + + if (result) { + console.log(`[威科姆] 检测到批改页面元素: ${result}`); + return true; + } + + // 兜底检测 + await new Promise(resolve => setTimeout(resolve, 3000)); + const hasImage = document.querySelector(WUYUETONG_SELECTORS.ANSWER_IMAGE); + const hasInput = document.querySelector(WUYUETONG_SELECTORS.SCORE_INPUT); + const hasBtn = document.querySelector(WUYUETONG_SELECTORS.SUBMIT_BUTTON); + const detected = !!(hasImage && hasInput && hasBtn); + console.log(`[威科姆] 兜底检测 - 图片: ${!!hasImage}, 输入框: ${!!hasInput}, 提交: ${!!hasBtn}, 最终: ${detected}`); + return detected; + } catch (error) { + console.error('[威科姆] detectMarkingPage 异常:', error); + return false; + } + }, + + getTaskIdentifier() { + return window.location.href; + }, + + async gatherAnswerImages() { + console.log('[威科姆] 开始获取答题卡图片...'); + + const startTime = Date.now(); + const maxWait = 8000; + + // 轮询等待图片加载完成 + while (Date.now() - startTime < maxWait) { + const img = document.querySelector(WUYUETONG_SELECTORS.ANSWER_IMAGE); + if (img && img.src && img.src.startsWith('http')) { + // 检查图片是否已加载完成 + if (img.naturalWidth > 0 || img.complete) { + console.log('[威科姆] 获取到答题卡图片'); + return [img.src]; + } + } + + const elapsed = Date.now() - startTime; + console.log(`[威科姆] 图片尚未加载,等待中... (${elapsed}ms)`); + await new Promise(r => setTimeout(r, 300)); + } + + // 超时兜底 + const fallbackImg = document.querySelector(WUYUETONG_SELECTORS.ANSWER_IMAGE); + if (fallbackImg && fallbackImg.src && fallbackImg.src.startsWith('http')) { + console.log('[威科姆] 超时兜底: 获取到图片'); + return [fallbackImg.src]; + } + + console.warn('[威科姆] 未找到答题卡图片'); + return []; + }, + + async fetchImageAsBase64(url) { + // OSS 图片可能需要移除裁剪参数以获取原图 + // 但当前 URL 已经是裁剪后的,直接使用 + return fetchImageAsBase64(url); + }, + + getScoreInputs() { + const inputs = []; + const inputEls = document.querySelectorAll(WUYUETONG_SELECTORS.SCORE_INPUT); + + inputEls.forEach((el, i) => { + // 从父容器的 font 标签提取最大分数,格式: <=4 + let maxScore = 0; + const container = el.closest(WUYUETONG_SELECTORS.SCORE_INPUT_CONTAINER); + if (container) { + const fontEl = container.querySelector(WUYUETONG_SELECTORS.MAX_SCORE_FONT); + if (fontEl) { + const match = fontEl.textContent.match(/(\d+)/); + if (match) { + maxScore = parseInt(match[1]) || 0; + } + } + } + + // 获取小题标签(从 label.control-label 获取) + const labelEl = container ? container.querySelector(WUYUETONG_SELECTORS.SCORE_LABEL) : null; + const label = labelEl ? labelEl.textContent.trim().replace(/:$/, '') : `第${i + 1}题`; + + inputs.push({ + element: el, + label: label, + index: i, + maxScore: maxScore + }); + }); + + console.log(`[威科姆] 找到 ${inputs.length} 个分数输入框`); + return inputs; + }, + + fillScores(scores) { + const inputs = this.getScoreInputs(); + if (inputs.length === 0) return false; + + const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + let successCount = 0; + + // 确保键盘打分模式激活 + this._ensureKeyboardMode(); + + for (let i = 0; i < Math.min(scores.length, inputs.length); i++) { + if (scores[i] === null || scores[i] === undefined) continue; + + const input = inputs[i].element; + const score = scores[i]; + + // 设置值 + setter.call(input, score); + + // 触发事件(jQuery 页面需要触发多种事件) + input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event('change', { bubbles: true })); + input.dispatchEvent(new Event('blur', { bubbles: true })); + // 触发 jQuery 的 keyup 事件(平台使用 onkeyup 验证) + input.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true })); + + successCount++; + } + + return successCount > 0; + }, + + submitGrade() { + const btn = document.querySelector(WUYUETONG_SELECTORS.SUBMIT_BUTTON); + if (btn) { + btn.click(); + console.log('[威科姆] 点击提交按钮'); + + // 处理0分确认弹窗(异步检测,不阻塞主流程) + setTimeout(() => { + const zeroModal = document.querySelector('#zeroCheckModal'); + if (zeroModal && zeroModal.style.display === 'block') { + const okBtn = document.querySelector('#btn_0_ok'); + if (okBtn) { + console.log('[威科姆] 检测到0分确认弹窗,自动点击确定'); + okBtn.click(); + } + } + }, 200); + + return true; + } + + console.warn('[威科姆] 未找到提交按钮'); + return false; + }, + + async waitForNextPaper(oldImageUrl) { + const timeout = 30000; + const interval = 500; + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + // 检查图片是否变化 + const img = document.querySelector(WUYUETONG_SELECTORS.ANSWER_IMAGE); + const currentUrl = img ? img.src : null; + if (oldImageUrl && currentUrl && currentUrl !== oldImageUrl) { + console.log('[威科姆] 检测到图片变化'); + return true; + } + + // 检查分数输入框是否已清空 + const firstInput = document.querySelector(WUYUETONG_SELECTORS.SCORE_INPUT); + if (firstInput && (firstInput.value === '' || firstInput.value === '0')) { + console.log('[威科姆] 检测到输入框已清空'); + return true; + } + + await new Promise(r => setTimeout(r, interval)); + } + + console.warn('[威科姆] 等待下一份答卷超时'); + return false; + }, + + isRegradeMode() { + // 检查页面是否有回评相关文字 + const bodyText = document.body.innerText || ''; + if (bodyText.includes('回评') || bodyText.includes('复核')) { + return true; + } + return !!window.aiGradingState?.isRegrading; + }, + + detectSubQuestions() { + return []; + }, + + // 确保键盘打分模式激活 + _ensureKeyboardMode() { + const keyboardRadio = document.querySelector(WUYUETONG_SELECTORS.KEYBOARD_MODE); + if (keyboardRadio && !keyboardRadio.checked) { + keyboardRadio.click(); + console.log('[威科姆] 切换到键盘打分模式'); + } + }, + + // 确保自动提交选项状态 + _ensureAutoSubmit(enable = true) { + const checkbox = document.querySelector(WUYUETONG_SELECTORS.AUTO_SUBMIT_CHECKBOX); + if (checkbox && checkbox.checked !== enable) { + checkbox.click(); + console.log(`[威科姆] ${enable ? '启用' : '禁用'}自动提交`); + } + }, + + onPageLoad() { + // 确保键盘打分模式 + this._ensureKeyboardMode(); + + // 注入 z-index 修复样式 + const style = document.createElement('style'); + style.id = 'wuyuetong-zindex-fix'; + style.textContent = ` + /* 提高脚本 UI 的 z-index,避免被威科姆平台元素遮挡 */ + .ai-grade-btn, .ai-history-btn, .ai-settings-btn, + .ai-toast, #ai-history-panel, #ai-grading-settings, + .ai-modal-overlay, #correction-panel, + #auto-submit-dialog, #asd-minimized-bar, + #ai-stream-container, #ai-history-overlay, + #ai-stream-panel { + z-index: 2147483640 !important; + } + `; + document.head.appendChild(style); + console.log('[威科姆] 已注入 z-index 修复样式'); + }, +}; + +// 注册适配器 +if (WuyuetongAdapter.shouldInitialize()) { + window.__AI_MARKER_ADAPTER__ = WuyuetongAdapter; + + // 立即注入 z-index 修复样式 + const style = document.createElement('style'); + style.id = 'wuyuetong-zindex-fix'; + style.textContent = ` + /* 提高脚本 UI 的 z-index,避免被威科姆平台元素遮挡 */ + .ai-grade-btn, .ai-history-btn, .ai-settings-btn, + .ai-toast, #ai-history-panel, #ai-grading-settings, + .ai-modal-overlay, #correction-panel, + #auto-submit-dialog, #asd-minimized-bar, + #ai-stream-container, #ai-history-overlay, + #ai-stream-panel { + z-index: 2147483640 !important; + } + `; + document.head.appendChild(style); + + // 用 JS 强制修改内联样式 + const WUYUETONG_UI_SELECTORS = [ + '.ai-grade-btn', '.ai-history-btn', '.ai-settings-btn', + '#ai-grading-settings', '#ai-history-panel', '#ai-history-overlay', + '#ai-history-detail', '#ai-stream-panel', '#auto-submit-dialog', + '#asd-minimized-bar', '#correction-panel', '.ai-modal-overlay', + '.ai-toast', '#ai-update-dialog' + ]; + + const fixInlineZIndex = () => { + document.querySelectorAll(WUYUETONG_UI_SELECTORS.join(', ')).forEach(el => { + el.style.setProperty('z-index', '2147483640', 'important'); + }); + }; + + // 多次尝试,确保在 UI 创建后执行 + setTimeout(fixInlineZIndex, 3000); + setTimeout(fixInlineZIndex, 5000); + setTimeout(fixInlineZIndex, 8000); + + // 监听 DOM 变化,自动修复新创建的脚本 UI 元素 + const observer = new MutationObserver(() => fixInlineZIndex()); + observer.observe(document.body, { childList: true, subtree: true }); + + console.log('[威科姆] 适配器已注册'); +} diff --git a/src/adapters/wuyuetong/selectors.js b/src/adapters/wuyuetong/selectors.js new file mode 100644 index 0000000..98d784c --- /dev/null +++ b/src/adapters/wuyuetong/selectors.js @@ -0,0 +1,54 @@ +// ========== 威科姆(悦卷通)DOM 选择器常量 ========== +// jQuery 传统页面,域名: wyna.onlyets.com +// 特点:多小题输入框,OSS 图片,键盘/鼠标/步骤三种打分模式 + +const WUYUETONG_SELECTORS = { + // 答题卡图片 + ANSWER_IMAGE: '#imageFullScreen', + IMAGE_CONTAINER: '#imgSpan', + + // 分数输入框(多小题模式) + SCORE_INPUT: '.ipt_score', + SCORE_INPUT_CONTAINER: '.panel-item-input', + SCORE_LABEL: 'label.control-label', + MAX_SCORE_FONT: 'font.text-error span', + + // 快捷按钮 + FULL_SCORE_BTN: '#btnFull', + ZERO_SCORE_BTN: '#btnZero', + + // 提交按钮 + SUBMIT_BUTTON: '#submitMark', + SUBMIT_BUTTON_STEPS: '#submitSteps', + + // 打分模式切换 + KEYBOARD_MODE: '#rdKeyboard', + MOUSE_MODE: '#rdMouse', + STEPS_MODE: '#rdSteps', + + // 选项 + AUTO_SUBMIT_CHECKBOX: '#autoSubmit', + ZERO_CONFIRM_CHECKBOX: '#lbl0OK', + SCALE_CHECKBOX: '#scaleSubmit', + + // 鼠标打分模式下的分数按钮 + MOUSE_SCORE_LIST: '.ul_score', + MOUSE_SCORE_ITEM: '.ul_score li.btn-group', + + // 页面检测 + PAGE_DETECT_IMAGE: '#imageFullScreen', + PAGE_DETECT_INPUT: '.ipt_score', + PAGE_DETECT_SUBMIT: '#submitMark', + + // 主容器 + MAIN_CONTAINER: '#markPanel', + + // 0分确认弹窗 + ZERO_MODAL: '#zeroCheckModal', + ZERO_MODAL_OK: '#btn_0_ok', + ZERO_MODAL_CANCEL: '#btn_0_cancel', + + // 隐藏字段 + HIDDEN_MARK_PIC: '#hdMarkPic', + HIDDEN_MARK_TAG: '#hdMarkTag', +};