// ==UserScript== // @name 自动填写助手 // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description 自动填写助手-个人使用 // @author Edison // @match https://learnfront.cyjy.net/task/* // @icon https://www.google.com/s2/favicons?sz=64&domain=cyjy.net // @require https://fastly.jsdelivr.net/npm/zh-address-parse@1.3.16/dist/zh-address-parse.min.js // @require https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js // @grant none // @license MIT // ==/UserScript== /** * 科目管理器 */ const SubjectManager = { KEY: "auto_fill_subject", getSubject: function () { const saved = localStorage.getItem(this.KEY); return saved || "物理"; // 默认物理 }, setSubject: function (subject) { localStorage.setItem(this.KEY, subject); this.updateDisplay(); UI.notification(`已切换到 ${subject} 模式`); }, toggleSubject: function () { const current = this.getSubject(); const next = current === "物理" ? "化学" : "物理"; this.setSubject(next); }, getOffset: function () { return this.getSubject() === "物理" ? 0 : 1; }, updateDisplay: function () { const btn = document.getElementById("subject-toggle-btn"); if (btn) { const subject = this.getSubject(); btn.innerText = `当前: ${subject}`; btn.style.backgroundColor = subject === "物理" ? "#1890ff" : "#52c41a"; btn.style.borderColor = subject === "物理" ? "#1890ff" : "#52c41a"; } }, }; /** * 选择器配置 - 使用getter动态获取offset */ const SELECTORS = { editor: '[id^="w-e-textarea-"]', scoreInput: 'input[role="spinbutton"]', // 动态路径 - 使用getter函数 get abilitySelect() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 2 + offset }) > div > div.ant-col.ant-form-item-control > div > div > span`; }, get yearSelect() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 5 + offset }) > div > div.ant-col.ant-form-item-control > div > div > div > div.ant-select-selector`; }, get yearSelectedTag() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 5 + offset }) > div > div.ant-col.ant-form-item-control > div > div > div > div.ant-select-selector > div`; }, get sceneContainer() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 6 + offset }) > div > div.ant-col.ant-form-item-control > div > div`; }, get categorySelect() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 7 + offset }) > div > div.ant-col.ant-form-item-control > div > div > div > div.ant-select-selector`; }, get sourceBtn() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 8 + offset }) > div > div.ant-col.ant-form-item-control > div > div > div > div.ant-row.ant-form-item.Item > div > div > div > button`; }, get sourceInput() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 8 + offset }) > div > div.ant-col.ant-form-item-control > div > div > div > div.ant-row.ant-form-item.Item > div > div > div > div > div > div > div > div.ant-popover-inner > div.ant-popover-inner-content > input`; }, get sourceConfirm() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 8 + offset }) > div > div.ant-col.ant-form-item-control > div > div > div > div.ant-row.ant-form-item.Item > div > div > div > div > div > div > div > div.ant-popover-inner > div.ant-popover-inner-content > div > button.ant-btn.ant-btn-primary.ant-btn-sm.ml10`; }, get sourceDisplay() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 9 + offset }) > div > div.ant-col.ant-form-item-control > div > div > span`; }, get addressTrigger() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 10 + offset }) > div > div.ant-col.ant-form-item-control > div > div > div`; }, get questionTypeContainer() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 11 + offset }) > div > div.ant-col.ant-form-item-control > div > div`; }, get groupSelect() { const offset = SubjectManager.getOffset(); return `#app > div > div.resizable-layout > div.right-panel > div > form > div > div:nth-child(${ 13 + offset }) > div > div.ant-col.ant-form-item-control > div > div > div > div.ant-select-selector`; }, }; /** * UI 工具类 */ const UI = { // 等待元素,超时会打印具体的 selector wait: function (selectorOrElement, timeout = 10000) { return new Promise((resolve, reject) => { // 1. 定义一个通用的检查逻辑 const check = () => { try { // 情况A: 传入的是函数(这是解决复杂路径的关键) if (typeof selectorOrElement === "function") { const result = selectorOrElement(); if (result instanceof Element) return result; } // 情况B: 传入的是字符串选择器 else if (typeof selectorOrElement === "string") { const el = document.querySelector(selectorOrElement); if (el) return el; } // 情况C: 传入的已经是元素(通常不走这里,除非逻辑特殊) else if (selectorOrElement instanceof Element) { return selectorOrElement; } // eslint-disable-next-line no-unused-vars } catch (e) { // 忽略查找过程中的报错(比如父节点暂时还没出来导致的报错) return null; } return null; }; // 2. 第一次立即检查 const immediateResult = check(); if (immediateResult) return resolve(immediateResult); // 3. 开始轮询 const startTime = Date.now(); const timer = setInterval(() => { const el = check(); if (el) { clearInterval(timer); resolve(el); } if (Date.now() - startTime > timeout) { clearInterval(timer); console.error( `❌ [UI.wait] 超时!找不到元素:\n${selectorOrElement.toString()}` ); reject(new Error(`Timeout: 找不到元素`)); } }, 200); }); }, sleep: (ms) => new Promise((r) => setTimeout(r, ms)), click: async function (selectorOrElement) { try { const el = await this.wait(selectorOrElement); el.focus && el.focus(); ["mousedown", "mouseup"].forEach((evt) => { el.dispatchEvent( new MouseEvent(evt, { bubbles: true, cancelable: true, view: window }) ); }); el.click && el.click(); return el; } catch (e) { // 捕获 wait 抛出的错误,避免红屏,只在控制台显示 console.warn(`⚠️ [UI.click] 跳过操作,原因: ${e.message}`); } }, type: async function (selectorOrElement, value) { try { const el = await this.wait(selectorOrElement); el.value = ""; const nativeSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, "value" ).set; if (nativeSetter) { nativeSetter.call(el, value); } else { el.value = value; } ["input", "change"].forEach((evt) => el.dispatchEvent(new Event(evt, { bubbles: true })) ); } catch (e) { console.warn(`⚠️ [UI.type] 输入失败: ${e.message}`); } }, // 精简了日志,去掉冗余的 Step 信息 clickText: async function (selector, text) { try { await this.wait(selector); let retries = 10; while (retries > 0) { const elements = document.querySelectorAll(selector); for (let el of elements) { if (el.innerText.trim() === text) { el.click(); return; } } await this.sleep(200); retries--; } console.warn(`⚠️ [UI.clickText] 在 ${selector} 中未找到文本 "${text}"`); } catch (e) { console.error(e.message); } }, selectTreeBySearch: async function (triggerDiv, searchText) { const input = triggerDiv.querySelector("input") || triggerDiv.querySelector(".ant-select-selection-search-input"); if (input) { await UI.type(input, searchText); await UI.sleep(500); await UI.clickText(".ant-select-tree-node-content-wrapper", searchText); } else { console.error("❌ [TreeSearch] 树形选择器中找不到输入框 input"); } }, selectAntOption: async function (targetText) { await UI.sleep(300); let dropdown = null; let maxRetries = 10; while (maxRetries > 0) { const allDropdowns = document.querySelectorAll( ".ant-select-dropdown:not(.ant-select-dropdown-hidden):not([style*='display: none'])" ); if (allDropdowns.length > 0) { dropdown = allDropdowns[allDropdowns.length - 1]; if (dropdown.innerText.trim() !== "") break; } await UI.sleep(200); maxRetries--; } if (!dropdown) { console.error( "❌ [SelectOption] 未找到任何可见的下拉菜单 (ant-select-dropdown)" ); return; } const listHolder = dropdown.querySelector(".rc-virtual-list-holder-inner"); const options = listHolder ? listHolder.querySelectorAll(".ant-select-item-option") : dropdown.querySelectorAll(".ant-select-item-option"); let found = false; for (let opt of options) { const content = opt.querySelector(".ant-select-item-option-content"); const text = content ? content.innerText : opt.innerText; if (text.trim() === targetText) { opt.click(); found = true; return; } } if (!found) { console.warn(`⚠️ [SelectOption] 下拉菜单中未找到选项: "${targetText}"`); } }, // 通知容器数组,用于管理多个通知 _notifications: [], notification: function (text) { const div = document.createElement("div"); div.innerText = text; div.style.cssText = "position: fixed; left: 70%; transform: translateX(-50%); background: #bfff94ff; padding: 10px 20px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 10000; font-size: 14px; transition: top 0.3s ease;"; // 添加到通知数组 this._notifications.push(div); // 计算位置(从上往下堆叠,起始位置 20px) this._updateNotificationPositions(); document.body.appendChild(div); // 3秒后移除 setTimeout(() => { div.style.opacity = "0"; div.style.transition = "opacity 0.3s ease, top 0.3s ease"; setTimeout(() => { div.remove(); // 从数组中移除 const index = this._notifications.indexOf(div); if (index > -1) { this._notifications.splice(index, 1); // 更新剩余通知的位置 this._updateNotificationPositions(); } }, 300); }, 3000); }, _updateNotificationPositions: function () { let topOffset = 20; // 起始位置 this._notifications.forEach((notif) => { notif.style.top = topOffset + "px"; topOffset += notif.offsetHeight + 10; // 每个通知之间间隔10px }); }, simulateBackspace: function (editor) { editor.focus(); const keyEventConfig = { key: "Backspace", code: "Backspace", keyCode: 8, which: 8, bubbles: true, cancelable: true, view: window, }; // 1. KeyDown const down = new KeyboardEvent("keydown", keyEventConfig); editor.dispatchEvent(down); const beforeInput = new InputEvent("beforeinput", { inputType: "deleteContentBackward", data: null, bubbles: true, cancelable: true, view: window, }); const beforeInputResult = editor.dispatchEvent(beforeInput); if (beforeInputResult) { document.execCommand("delete", false, null); } else { document.execCommand("delete", false, null); } editor.dispatchEvent( new InputEvent("input", { inputType: "deleteContentBackward", bubbles: true, cancelable: false, view: window, }) ); // 5. KeyUp const up = new KeyboardEvent("keyup", keyEventConfig); editor.dispatchEvent(up); // 6. Extra Events editor.dispatchEvent(new Event("change", { bubbles: true })); editor.dispatchEvent(new Event("compositionend", { bubbles: true })); }, }; /** * 配置管理器 */ const ConfigManager = { KEY: "auto_fill_config", data: { titleRules: [], sourceRules: [] }, init: function () { const saved = localStorage.getItem(this.KEY); if (saved) { try { this.data = JSON.parse(saved); this.updateStatus(); // eslint-disable-next-line no-unused-vars } catch (e) { /* empty */ } } }, save: function () { localStorage.setItem(this.KEY, JSON.stringify(this.data)); UI.notification("配置已保存"); }, importFromExcel: function (file) { if (!window.XLSX) { UI.notification("XLSX 库尚未加载完成,请稍后..."); return; } const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target.result); // eslint-disable-next-line no-undef const workbook = XLSX.read(data, { type: "array" }); const parseSheet = (sheetName) => { if (!workbook.Sheets[sheetName]) return []; // eslint-disable-next-line no-undef return XLSX.utils .sheet_to_json(workbook.Sheets[sheetName], { header: 1 }) .slice(1) .filter((row) => row[0] && row[1]) .map((row) => ({ key: String(row[0]).trim(), value: String(row[1]).trim(), })); }; if (workbook.SheetNames.length >= 1) this.data.titleRules = parseSheet(workbook.SheetNames[0]); if (workbook.SheetNames.length >= 2) this.data.sourceRules = parseSheet(workbook.SheetNames[1]); this.save(); this.updateStatus(); } catch (err) { console.error("❌ Excel 解析失败", err); UI.notification("Excel 解析失败"); } }; reader.readAsArrayBuffer(file); }, updateStatus: function () { const statusEl = document.getElementById("config-status-display"); if (statusEl) { statusEl.innerText = `(题干:${this.data.titleRules.length}, 来源:${this.data.sourceRules.length})`; statusEl.style.color = "#265c0cff"; } }, matchRule: function (text, ruleKey) { if (!text || !ruleKey) return false; const conditions = ruleKey.trim().split(/\s+/); for (let condition of conditions) { if (!condition) continue; if (condition.startsWith("&")) { if (!text.includes(condition.substring(1))) return false; } else if (condition.startsWith("!")) { if (text.includes(condition.substring(1))) return false; } else { if (!text.includes(condition)) return false; } } return true; }, getMatchedScenes: function (title, source) { const scenes = new Set(); const check = (text, rules) => { if (text) rules.forEach( (rule) => this.matchRule(text, rule.key) && scenes.add(rule.value) ); }; if (title) { check(title, this.data.titleRules); } if (source) { check(source, this.data.sourceRules); } return Array.from(scenes); }, }; /** * 业务逻辑集合 */ const App = { getFullContent: function (walker) { let fullText = ""; let nodeMap = []; let currentNode = walker.nextNode(); while (currentNode) { const textContent = currentNode.textContent; const length = textContent.length; nodeMap.push({ node: currentNode, start: fullText.length, // 该节点在全文中的起始索引 end: fullText.length + length, // 该节点在全文中的结束索引 }); fullText += textContent; currentNode = walker.nextNode(); } // 只取第一个逗号前的内容 (Why? User logic, keeping it but it seems risky for general purpose) // fullText = fullText.split(',')[0]; // Commenting out split because it breaks exact mapping if we filter text but keep nodes. // If we split text, `nodeMap` indices will be out of sync with truncated text. // BETTER: Don't split here. Let the caller decide what to match. return { fullText, nodeMap }; }, removeContent: async function (fullText, editor, nodeMap, customPattern) { let targetStartIndex = -1; let targetEndIndex = -1; let textToRemove = ""; // 情况 A: 根据传入的 pattern 查找 // 这里的 fullText 包含了所有节点的内容,跨节点也能匹配到 targetStartIndex = fullText.indexOf(customPattern); if (targetStartIndex === -1) { console.log(`⚠️ 未找到匹配内容: "${customPattern}"`); return null; } targetEndIndex = targetStartIndex + customPattern.length; textToRemove = customPattern; // 3. 将全局索引转换回 DOM Range (Mapping back to Nodes) let startContainer = null; let startOffset = 0; let endContainer = null; let endOffset = 0; for (const item of nodeMap) { // 寻找开始节点 // 如果目标起始点落在当前节点范围内 (包含 start,不包含 end,除非是最后一个字符) if ( !startContainer && targetStartIndex >= item.start && targetStartIndex < item.end ) { startContainer = item.node; startOffset = targetStartIndex - item.start; } // 寻找结束节点 // 结束点通常是开区间,所以可以是 item.end if ( !endContainer && targetEndIndex > item.start && targetEndIndex <= item.end ) { endContainer = item.node; endOffset = targetEndIndex - item.start; } // 如果两头都找到了,提前结束循环 if (startContainer && endContainer) break; } // 兜底:如果结束点正好是全文末尾,且因为边界问题没匹配到(很少见,但为了稳健) if ( !endContainer && nodeMap.length > 0 && targetEndIndex === fullText.length ) { const lastItem = nodeMap[nodeMap.length - 1]; endContainer = lastItem.node; endOffset = lastItem.node.textContent.length; } if (!startContainer || !endContainer) { console.error("❌ 计算 DOM 选区失败,索引越界或节点丢失"); return null; } // 4. 执行选区和删除 let hasChange = false; try { editor.focus(); const sel = window.getSelection(); const range = document.createRange(); // 核心:设置跨节点的 Range range.setStart(startContainer, startOffset); range.setEnd(endContainer, endOffset); sel.removeAllRanges(); sel.addRange(range); await UI.sleep(500); UI.simulateBackspace(editor); await UI.sleep(500); console.log(`✅ 已自动清除: "${textToRemove}" (跨节点处理完成)`); hasChange = true; } catch (e) { console.error("❌ 删除操作执行出错:", e); } return hasChange; }, clearContent: async function (customPattern = null) { const editor = document.querySelector(SELECTORS.editor); if (!editor) { console.error( `❌ [clearContent] 找不到题干编辑器,Selector: ${SELECTORS.editor}` ); return null; } const walker = document.createTreeWalker( editor, NodeFilter.SHOW_TEXT, null, false ); const { fullText, nodeMap } = this.getFullContent(walker); if (customPattern) { await this.removeContent(fullText, editor, nodeMap, customPattern); } }, getAndCleanTitle: async function (remove = false) { const editor = document.querySelector(SELECTORS.editor); if (!editor) { console.error( `❌ [getAndCleanTitle] 找不到题干编辑器,Selector: ${SELECTORS.editor}` ); return null; } const walker = document.createTreeWalker( editor, NodeFilter.SHOW_TEXT, null, false ); const { fullText, nodeMap } = this.getFullContent(walker); const regex = /(?:([^)]*)|\([^)]*\))/g; let match; while ((match = regex.exec(fullText)) !== null) { const matchedStr = match[0]; // 例如 "(2023北京)" const extracted = matchedStr.trim().replace(/^[((]|[))]$/g, ""); // 例如 "2023北京" // --- 校验逻辑 --- const hasYear = /(?:19|20)\d{2}/.test(extracted); let hasRegion = false; if (typeof App !== "undefined" && App.paraseAddr) { const inner = extracted.trim(); const addr = App.paraseAddr(inner); if (addr && (addr.province || addr.city || addr.area)) { hasRegion = true; } } if (hasYear || hasRegion) { // 1. 计算结束位置 const matchEndIndex = match.index + matchedStr.length; // 2. 截取前缀:从节点文本开头(0) 到 括号结束的位置 // 如果原文是 "1. (2023北京) 题目...",这里 prefix 就是 "1. (2023北京)" const prefixContent = fullText.substring(0, matchEndIndex); // 准备返回的数据对象 const resultData = { title: extracted, // "2023北京" prefix: prefixContent, // "1. (2023北京)" }; if (remove) { await this.removeContent(fullText, editor, nodeMap, prefixContent); console.log( `✅ [匹配成功] 提取到: "${extracted}", 前缀: "${prefixContent}"` ); } return resultData; } } if (remove) UI.notification("未找到包含年份或地区的题干标签"); return null; }, getScore: async function () { const el = document.querySelector(SELECTORS.scoreInput); if (!el) { console.error( `❌ [getScore] 找不到星级输入框,Selector: ${SELECTORS.scoreInput}` ); return 0; } const val = parseFloat(el.value); if (!val) { UI.notification("⚠️ 请先填写题目难度星级"); throw new Error("无难度星级"); // 中断流程 } return val; }, selectCategory: async function (score) { let category = "思维拓展题"; if (score <= 0.4) category = "基础题"; else if (score <= 0.6) category = "综合运用题"; await UI.click(SELECTORS.categorySelect); await UI.selectAntOption(category); }, selectYear: async function (title, sourceFrom) { // 含义:匹配 (19xx 或 20xx) 或者 (23, 24, 25, 26) const yearRegex = /\b((19|20)\d{2}|2[3-6])\b/g; let availableYears = [ ...(title.match(yearRegex) || []), ...(sourceFrom.match(yearRegex) || []), ]; // 标准化年份 (将 2位 转换为 4位) // 如果是 23, 24 这种,自动补全为 2023, 2024 availableYears = availableYears.map((year) => { if (year.length === 2) { return "20" + year; } return year; }); // 去重 (此时都是4位年份了) availableYears = [...new Set(availableYears)]; // 检查已选 const selectedTag = document.querySelector(SELECTORS.yearSelectedTag); if (selectedTag) { // 提取已选中的年份(假设已选标签中显示的肯定是4位年份) const existing = selectedTag.textContent.match(/\b(19|20)\d{2}\b/); if (existing) // 过滤掉已经选中的年份 availableYears = availableYears.filter((y) => y !== existing[0]); } // 再次去重 (保险起见) availableYears = [...new Set(availableYears)]; if (availableYears.length > 0) { await UI.click(SELECTORS.yearSelect); for (const year of availableYears) { await UI.sleep(500); // 这里传入的 year 已经是 4 位数,例如 "2024",组合后为 "2024年" await UI.selectAntOption(year + "年"); } } }, paraseAddr: function (title) { if (typeof ZhAddressParse === "undefined") { console.error("❌ ZhAddressParse 库未定义,无法解析地址"); return; } // 清洗字符串 去掉数字和特殊字符 title = title.replace(/[^\p{L}\s]/gu, ""); // eslint-disable-next-line no-undef const addr = ZhAddressParse(title); return addr; }, selectAddress: async function (title, sourceFrom) { let addrSet = new Set(); for (let item of [title, sourceFrom]) { // 出现改编就跳过 if (item.indexOf("改编") != -1) { continue; } const addr = App.paraseAddr(item); if (addr.province) { if (addr.province === "北京市") addr.province = "北京"; if (addr.province === "上海市") addr.province = "上海"; if (addr.province === "天津市") addr.province = "天津"; if (addr.province === "重庆市") addr.province = "重庆"; if (addr.province && !addrSet.has(addr.province)) { addrSet.add(addr.province); const trigger = await UI.click(SELECTORS.addressTrigger); await UI.selectTreeBySearch(trigger, addr.province); if (addr.city && !addrSet.has(addr.city)) { addrSet.add(addr.city); await UI.selectTreeBySearch(trigger, addr.city); if (addr.area && !addrSet.has(addr.area)) { addrSet.add(addr.area); await UI.selectTreeBySearch(trigger, addr.area); } } } } } }, formatSmartTitle: function (text) { if (!text) return ""; // 1. 正则捕获:前缀、年份1、年份2(可选)、后缀 // ^(.*?) -> 前缀 (Group 1) // (\d{4}) -> 年份1 (Group 2) // (?:-(\d{4}))? -> 年份2 (Group 3, 可选) // \s*学年\s* -> 过滤掉"学年" // (.*)$ -> 后缀 (Group 4, 包含学期信息) const regex = /^(.*?)(\d{4})(?:[-—](\d{4}))?\s*学年\s*(.*)$/; const match = text.match(regex); // 没有匹配忽略类容 if (!match) return ""; const [, prefix, year1, year2, suffix] = match; // 关键词检测:下学期、第二学期、下册 const isSecondSemester = /下学期|第二学期|下册/.test(text); // 如果是下学期且存在第二个年份,则取year2,否则默认year1 let finalYear = isSecondSemester && year2 ? year2 : year1; let otherText = prefix.trim() + suffix.trim(); const addr = App.paraseAddr(otherText); const addrStr = addr.province + addr.city + addr.area; // 没有地区忽略类容 if (!addrStr) return ""; let final = ""; for (let item of ["期中", "期末"]) { if (otherText.indexOf(item) != -1) { final = item; break; } } // 5. 返回结果 return `${finalYear}·${addrStr}${final}`; }, addSource: async function (title, sourceFrom) { let result; if (title && title.indexOf("改编") == -1) { result = title; } else if (sourceFrom && sourceFrom.indexOf("改编") == -1) { result = sourceFrom; } else { return; } await UI.click(SELECTORS.sourceBtn); await UI.type(SELECTORS.sourceInput, result); await UI.sleep(100); await UI.click(SELECTORS.sourceConfirm); }, selectQuestionType: async function () { const types = [ "基础题", "压轴题", "典型题", "易错易混题", "同步题", "教材改编题", "原创题", "教材原题", "跨学科", "传统文化题", "竞赛题", "新考法", "新情境", "方法模型", ]; const editor = document.querySelector(SELECTORS.editor); if (!editor) { console.error( `❌ [getAndCleanTitle] 找不到题干编辑器,Selector: ${SELECTORS.editor}` ); return null; } const walker = document.createTreeWalker( editor, NodeFilter.SHOW_TEXT, null, false ); let forDeleteKeyWord = []; const { fullText } = this.getFullContent(walker); for (let i = 0; i < types.length; i++) { if (fullText.indexOf(types[i]) > -1) { forDeleteKeyWord.push(types[i]); await UI.click( `${SELECTORS.questionTypeContainer} > span:nth-child(${i + 1})` ); await UI.sleep(100); } } for (let deleteItem of forDeleteKeyWord) { await App.clearContent(deleteItem); } }, selectGroup: async function (score) { let group = "提升"; if (score <= 0.4) group = "基础"; else if (score <= 0.6) group = "巩固"; await UI.click(SELECTORS.groupSelect); await UI.selectAntOption(group); }, selectScene: async function (title, sourceFrom) { try { let matchedScenes; let titleCopy = ""; let sourceFromCopy = ""; if (title.indexOf("改编") == -1) { titleCopy = title; } if (sourceFrom.indexOf("改编") == -1) { sourceFromCopy = sourceFrom; } matchedScenes = ConfigManager.getMatchedScenes(titleCopy, sourceFromCopy); const sceneOptions = [ "课前预习", "同步练习", "课后作业", "单元测试", "阶段练习", "月考", "期中", "期末", "专题练习", "开学考试", "假期作业", "统考、联考题", "高考真题", "高考模拟", ]; for (let scene of matchedScenes) { const idx = sceneOptions.indexOf(scene); if (idx > -1) { const el = await UI.wait( `${SELECTORS.sceneContainer} > span:nth-child(${idx + 1})` ); el.click(); } await UI.sleep(200); } return sourceFrom; // eslint-disable-next-line no-unused-vars } catch (e) { console.warn( "⚠️ 无法获取当前【来源】字段,可能该元素不存在或 Selector 错误" ); return ""; } }, validateTitle: function (title) { const addr = this.paraseAddr(title); const hasLocation = addr.province || addr.city || addr.area; const hasYear = /\d{4}/.test(title); if (!hasLocation || !hasYear) { const missing = []; if (!hasLocation) missing.push("地区信息"); if (!hasYear) missing.push("年份信息"); UI.notification(`⚠️ 题目缺少${missing.join("和")},无法自动填写`); throw new Error(`⚠️ 题目缺少${missing.join("和")},无法自动填写`); // 中断流程 } return true; }, // 选择能力 selectAbility: async function (score) { let ability = "基础知识理解与运用能力"; if (score > 0.4) ability = "信息获取与加工能力"; await UI.clickText(SELECTORS.abilitySelect, ability); }, startTask: async function () { const btn = document.getElementById("my-smart-auto-btn"); const originalText = btn ? btn.innerText : ""; if (btn) { btn.innerText = "执行中..."; btn.disabled = true; btn.style.cursor = "not-allowed"; btn.style.opacity = "0.6"; } try { console.log("=== 开始执行自动填写 ==="); let stepCount = 0; // 如果这里报错(例如没有星级),会自动跳到 catch const score = await App.getScore(); UI.notification(`自动填写开始`); // 能力选择 if (SubjectManager.getSubject() == "化学") { await App.selectAbility(score); } // 题类 await App.selectQuestionType(); // 自动删除题干并获取内容 const resultData = await App.getAndCleanTitle(true); let title = ""; if (!resultData) { await App.clearContent("(多选)"); await App.clearContent("(母题)"); await App.clearContent("(变式)"); await App.clearContent("(多选)"); await App.clearContent("(母题)"); await App.clearContent("(变式)"); await App.clearContent("多选"); await App.clearContent("母题"); await App.clearContent("变式"); } else { title = resultData.title || ""; } // 更新显示 const titleDisplay = document.getElementById("extracted-title-display"); if (titleDisplay && title) { titleDisplay.innerText = `题干: ${title}`; } else if (titleDisplay) { titleDisplay.innerText = "题干: 未找到"; } // 录入源 const sourceEl = await UI.wait(SELECTORS.sourceDisplay, 3000); const rawSourceFrom = sourceEl.textContent; const sourceFrom = App.formatSmartTitle(rawSourceFrom); // 判断内容中是否有年份或者地区,如果没有的话提示 if (title) { const addr = App.paraseAddr(title); const hasLocation = addr.province || addr.city || addr.area; if (hasLocation) { // 地区 await App.selectAddress(title, sourceFrom); stepCount++; } else { console.error("⚠️ 题目缺少地区信息,跳过[地区]"); UI.notification("⚠️ 题目缺少地区信息,跳过[地区]"); } stepCount++; } // 来源 await App.addSource(title, sourceFrom); stepCount++; // 场景 await App.selectScene(title, rawSourceFrom); stepCount++; // 题目分类 await App.selectCategory(score); stepCount++; // 试题年份 await App.selectYear(title, sourceFrom); stepCount++; // 分组 if (sourceFrom && sourceFrom.indexOf("实验班") !== -1) { await App.selectGroup(score); stepCount++; } console.log(`✅ 自动填写完成,共 ${stepCount} 步`); UI.notification(`自动填写完成,执行 ${stepCount} 步`); } catch (e) { if (e.message !== "无难度星级") { console.error("❌ 任务中断:", e); UI.notification(`自动填写失败,${e.message}`); } } finally { if (btn) { btn.innerText = originalText; btn.disabled = false; btn.style.cursor = "pointer"; btn.style.opacity = "1"; } } }, }; /** * 主程序入口 */ (function () { const BTN_ID = "my-smart-auto-btn"; function createButton(parent, text, color, onClick, isPrimary = true) { const btn = document.createElement("button"); btn.innerText = text; btn.className = isPrimary ? "ant-btn ant-btn-primary" : "ant-btn"; btn.onclick = onClick; Object.assign(btn.style, { fontWeight: "bold", ...(isPrimary ? { backgroundColor: color, borderColor: color } : {}), }); btn.style.marginLeft = "15px"; if (isPrimary) { btn.id = BTN_ID; } parent.appendChild(btn); } function createSubjectToggle(header) { const btn = document.createElement("button"); btn.id = "subject-toggle-btn"; btn.className = "ant-btn ant-btn-primary"; btn.onclick = () => SubjectManager.toggleSubject(); Object.assign(btn.style, { marginLeft: "15px", fontWeight: "bold", cursor: "pointer", }); header.appendChild(btn); SubjectManager.updateDisplay(); } function createImportControl(header) { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = ".xlsx, .xls, .csv"; fileInput.style.display = "none"; fileInput.onchange = (e) => { if (e.target.files.length) ConfigManager.importFromExcel(e.target.files[0]); fileInput.value = ""; }; header.appendChild(fileInput); createButton(header, "导入配置", "#52c41a", () => fileInput.click(), false); const status = document.createElement("span"); status.id = "config-status-display"; Object.assign(status.style, { marginLeft: "10px", fontSize: "12px", color: "#999", }); status.innerText = "(配置未加载)"; header.appendChild(status); } function checkAndInject() { // 这里的 scoreInput 只是用来判断当前页面是否是目标页面 const target = document.querySelector(SELECTORS.scoreInput); const header = document.querySelector("header"); const existingBtn = document.getElementById(BTN_ID); if (target && header) { if (!existingBtn) { console.log("✅ 检测到目标输入框,注入控制面板..."); // 恢复直接创建按钮 createButton(header, "自动完成", "#52c41a", App.startTask, true); // 创建题号显示区域 let titleDisplay = document.getElementById("extracted-title-display"); if (!titleDisplay) { titleDisplay = document.createElement("span"); titleDisplay.id = "extracted-title-display"; titleDisplay.style.fontSize = "14px"; titleDisplay.style.color = "#1890ff"; titleDisplay.style.fontWeight = "bold"; titleDisplay.style.marginLeft = "auto"; titleDisplay.innerText = "题干: -"; } const allButtons = Array.from(document.querySelectorAll("button")); const submitResultBtn = allButtons.find((b) => b.innerText.includes("提交结果") ); submitResultBtn.style.marginLeft = "15px"; if (submitResultBtn && submitResultBtn.parentNode) { submitResultBtn.parentNode.insertBefore( titleDisplay, submitResultBtn ); } else { header.appendChild(titleDisplay); } createSubjectToggle(header); createImportControl(header); ConfigManager.init(); } } else if (existingBtn) { console.log("⚠️ 目标输入框消失,移除面板..."); existingBtn.remove(); // 同时也移除显示区域 const display = document.getElementById("extracted-title-display"); if (display) display.remove(); } } const observer = new MutationObserver(checkAndInject); observer.observe(document.body, { childList: true, subtree: true }); checkAndInject(); })();