您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
IXL 解题脚本:Display-Only 模式使用 OpenAI SSE 流式实时渲染;Auto-Fill 保留自动填入。面板可拖拽+最小化,进度条、回滚、日志、令牌计数、默认折叠设置区。预置 gpt-4.1-nano。
当前为
// ==UserScript== // @name IXL Auto Answer (OpenAI API Required) // @namespace http://tampermonkey.net/ // @version 9.1 // @license GPL-3.0 // @description IXL 解题脚本:Display-Only 模式使用 OpenAI SSE 流式实时渲染;Auto-Fill 保留自动填入。面板可拖拽+最小化,进度条、回滚、日志、令牌计数、默认折叠设置区。预置 gpt-4.1-nano。 // @match https://*.ixl.com/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @require https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js // @require https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js // ==/UserScript== (function () { 'use strict'; /*─────────────────────────────────────────────────────────────────────── 0. LaTeX 包装 & 反转义 ───────────────────────────────────────────────────────────────────────*/ function wrapLatex(s) { // 修复 (-$\frac{a}{b}$) → $-\frac{a}{b}$,并给裸 \frac 补 $$ s = s.replace(/\(-\$\\frac\{([^}]+)\}\{([^}]+)\}\$\)/g, (_, a, b) => `$-\\frac{${a}}{${b}}$`); return s.replace(/\\frac\{[^}]+\}\{[^}]+\}/g, m => `$${m}$`); } function unescapeDollar(s) { return s.replace(/\\\$/g, '$'); } /*─────────────────────────────────────────────────────────────────────── 1. 配置存储与迁移 ───────────────────────────────────────────────────────────────────────*/ const OLD1 = localStorage.getItem('gpt4o-modelConfigs'); const OLD2 = localStorage.getItem('ixlAutoAnswerConfigs'); if (!localStorage.getItem('myNewIxLStorage')) { if (OLD1) { localStorage.setItem('myNewIxLStorage', OLD1); localStorage.removeItem('gpt4o-modelConfigs'); } if (OLD2) { localStorage.setItem('myNewIxLStorage', OLD2); localStorage.removeItem('ixlAutoAnswerConfigs'); } } const modelConfigs = JSON.parse(localStorage.getItem('myNewIxLStorage') || '{}'); if (!modelConfigs['gpt-4.1']) { modelConfigs['gpt-4.1'] = { apiKey: '', apiBase: 'https://api.openai.com/v1/chat/completions', discovered: false, modelList: [] }; } const config = { selectedModel: 'gpt-4.1', language: localStorage.getItem('myIxLLang') || 'en', mode: 'displayOnly', // "autoFill" | "displayOnly" autoSubmit: false, totalTokens: 0, lastState: null }; function saveConfig() { localStorage.setItem('myNewIxLStorage', JSON.stringify(modelConfigs)); localStorage.setItem('myIxLLang', config.language); } /*─────────────────────────────────────────────────────────────────────── 2. 多语言文案 ───────────────────────────────────────────────────────────────────────*/ const langText = { en: { panelTitle: "IXL Auto Answer (OpenAI API Required)", modeLabel: "Mode", modeAuto: "Auto Fill (Unstable)", modeDisp: "Display Answer Only (stream)", startButton: "Start Answering", rollbackButton: "Rollback", configAssistant: "Config Assistant", closeButton: "Close", logsButton: "Logs", logsHide: "Hide Logs", tokensLabel: "Tokens: ", statusIdle: "Status: Idle", statusWaiting: "Streaming...", statusDone: "Done.", requestError: "Request error: ", finalAnswerTitle: "Final Answer", stepsTitle: "Solution Steps", missingAnswerTag: "Missing <answer> tag", modelSelectLabel: "Model", modelDescLabel: "Model Description", customModelPlaceholder: "Custom model name", languageLabel: "Language", autoSubmitLabel: "Auto Submit", rentKeyButton: "Rent Key (Support Me!)", settingsKeyButton: "Toggle Settings", apiKeyLabel: "API Key", saveButton: "Save", testKeyButton: "Test Key", testKeyMsg: "Testing key...", keyOK: "API key valid.", keyBad: "API key invalid (missing 'test success').", placeKey: "Enter your API key", placeBase: "Enter your API base URL", apiBaseLabel: "API Base", refreshModels: "Refresh Models", getKeyLinkLabel: "Get API Key", disclaimAutoFill: "Warning: Auto Fill unstable.", minButton: "Min", shortAI: "Ask" }, zh: { panelTitle: "IXL自动解题 (OpenAI)", modeLabel: "模式", modeAuto: "自动填入(不稳定)", modeDisp: "仅展示答案(流式)", startButton: "开始答题", rollbackButton: "撤回", configAssistant: "配置助手", closeButton: "关闭", logsButton: "日志", logsHide: "隐藏日志", tokensLabel: "用量: ", statusIdle: "状态:空闲", statusWaiting: "流式等待GPT...", statusDone: "完成。", requestError: "请求错误:", finalAnswerTitle: "最终答案", stepsTitle: "解题过程", missingAnswerTag: "缺少<answer>标签", modelSelectLabel: "模型", modelDescLabel: "模型介绍", customModelPlaceholder: "自定义模型名称", languageLabel: "语言", autoSubmitLabel: "自动提交", rentKeyButton: "租用Key (支持我!)", settingsKeyButton: "开关设置", apiKeyLabel: "API密钥", saveButton: "保存", testKeyButton: "测试密钥", testKeyMsg: "正在测试...", keyOK: "API密钥有效。", keyBad: "API密钥无效(缺'test success')", placeKey: "输入API密钥", placeBase: "输入API基础地址", apiBaseLabel: "API基础地址", refreshModels: "刷新模型列表", getKeyLinkLabel: "获取API Key", disclaimAutoFill: "警告:自动填入模式可能不稳定,请慎用。", minButton: "最小化", shortAI: "提问" } }; /*─────────────────────────────────────────────────────────────────────── 3. 模型描述 ───────────────────────────────────────────────────────────────────────*/ const modelDescDB = { "gpt-4.1": "New Model, cheaper and a lot better than 4o", "gpt-4.1-mini": "New Model, cheaper and a little bit better than 4o", "gpt-4.1-nano": "Ultra-fast text-only.", "gpt-4o": "Solves images, cost-effective.", "gpt-4o-mini": "Text-only, cheaper.", "o1": "Best for images but slow & expensive.", "o3-mini": "Text-only, cheaper than o1.", "deepseek-reasoner": "No images, cheaper than o1.", "deepseek-chat": "No images, cheap & fast as 4o.", "o3": "Advanced multi-step reasoning model.", "o4-mini": "Compact variant of o4 architecture.", "chatgpt-4o-least": "RLHF version, can be error-prone.", "custom": "User-defined model" }; /*─────────────────────────────────────────────────────────────────────── 4. 构建 UI ───────────────────────────────────────────────────────────────────────*/ const panel = document.createElement("div"); panel.id = "ixl-auto-panel"; panel.innerHTML = ` <div class="ixl-header"> <span id="panel-title">${langText[config.language].panelTitle}</span> <span id="token-count">${langText[config.language].tokensLabel}0</span> <button id="btn-min" title="${langText[config.language].minButton}">—</button> <button id="btn-logs">${langText[config.language].logsButton}</button> <button id="btn-close">${langText[config.language].closeButton}</button> </div> <div class="ixl-content" id="ixl-body"> <div class="row"> <label>${langText[config.language].modeLabel}:</label> <select id="sel-mode" style="width:100%;"> <option value="autoFill">${langText[config.language].modeAuto}</option> <option value="displayOnly">${langText[config.language].modeDisp}</option> </select> </div> <div class="row" style="margin-top:8px; display:flex; gap:8px;"> <button id="btn-start" class="btn-accent" style="flex:1;">${langText[config.language].startButton}</button> <button id="btn-rollback" class="btn-normal" style="flex:1;">${langText[config.language].rollbackButton}</button> <button id="btn-config-assist" class="btn-mini" style="flex:0;">${langText[config.language].configAssistant}</button> </div> <div id="answer-box" style="display:none; border:1px solid #999; padding:6px; background:#fff; margin-top:6px;"> <h4 id="answer-title">${langText[config.language].finalAnswerTitle}</h4> <div id="answer-content" style="font-size:15px; font-weight:bold; color:#080;"></div> <hr/> <h5 id="steps-title">${langText[config.language].stepsTitle}</h5> <div id="steps-content" style="font-size:13px; color:#666;"></div> </div> <div id="progress-area" style="display:none; margin-top:8px;"> <progress id="progress-bar" max="100" value="0" style="width:100%;"></progress> <span id="progress-label">${langText[config.language].statusWaiting}</span> </div> <p id="status-line" style="font-weight:bold; margin-top:6px;">${langText[config.language].statusIdle}</p> <div id="log-area" style="display:none; max-height:120px; overflow-y:auto; background:#fff; border:1px solid #888; margin-top:6px; padding:4px; font-family:monospace;"></div> <div class="row" style="margin-top:10px;"> <button id="btn-rent" class="btn-normal" style="width:100%; font-weight:bold;">${langText[config.language].rentKeyButton}</button> <button id="btn-settings" class="btn-normal" style="width:100%; font-weight:bold; margin-top:6px;">${langText[config.language].settingsKeyButton}</button> </div> <div id="settings-area"> <label>${langText[config.language].modelSelectLabel}:</label> <select id="sel-model" style="width:100%;"></select> <p id="model-desc" style="font-size:12px; color:#666; margin:4px 0;"></p> <div id="custom-model-area" style="display:none;"><input type="text" id="custom-model-input" style="width:100%;" placeholder="${langText[config.language].customModelPlaceholder}"/></div> <div class="row" style="margin-top:8px;"> <label>${langText[config.language].languageLabel}:</label> <select id="sel-lang" style="width:100%;"> <option value="en">English</option> <option value="zh">中文</option> </select> </div> <div id="auto-submit-row" style="margin-top:8px;"><label>${langText[config.language].autoSubmitLabel}:</label><input type="checkbox" id="chk-auto-submit"/></div> <div class="row" style="margin-top:10px;"> <label>${langText[config.language].apiKeyLabel}:</label> <div style="display:flex; gap:4px; margin-top:4px;"> <input type="password" id="txt-apikey" style="flex:1;" placeholder="${langText[config.language].placeKey}"/> <button id="btn-save-key">${langText[config.language].saveButton}</button> <button id="btn-test-key">${langText[config.language].testKeyButton}</button> </div> </div> <div class="row" style="margin-top:8px;"> <label>${langText[config.language].apiBaseLabel}:</label> <div style="display:flex; gap:4px; margin-top:4px;"> <input type="text" id="txt-apibase" style="flex:1;" placeholder="${langText[config.language].placeBase}"/> <button id="btn-save-base">${langText[config.language].saveButton}</button> </div> </div> <label style="display:block; margin-top:6px;">${langText[config.language].getKeyLinkLabel}:</label> <div style="display:flex; gap:4px; margin-top:4px;"> <a id="link-getkey" href="#" target="_blank" class="link-btn" style="flex:1;">Link</a> <button id="btn-refresh" class="btn-normal" style="flex:1;">${langText[config.language].refreshModels}</button> </div> </div> </div>`; document.body.appendChild(panel); GM_addStyle(` #ixl-auto-panel{position:fixed;top:20px;right:20px;width:460px;max-height:500px;background:#fff;border-radius:6px;box-shadow:0 2px 10px rgba(0,0,0,.3);font-family:"Segoe UI",Arial,sans-serif;font-size:14px;overflow-y:auto;z-index:99999999;} .ixl-header{background:#4caf50;color:#fff;display:flex;align-items:center;gap:6px;padding:6px;cursor:move;user-select:none;} .ixl-header button{background:#fff;color:#333;border:none;border-radius:3px;padding:0 6px;font-weight:bold;cursor:pointer;} .ixl-header button:hover{background:#eee;} .ixl-content{padding:10px;} #settings-area{display:none;} .btn-accent{background:#f0ad4e;color:#fff;border:none;border-radius:4px;font-weight:bold;} .btn-accent:hover{background:#ec971f;} .btn-normal{background:#ddd;color:#333;border:none;border-radius:4px;} .btn-normal:hover{background:#ccc;} .btn-mini{background:#bbb;color:#333;border:none;border-radius:4px;font-size:12px;padding:4px 6px;} .btn-mini:hover{background:#aaa;} .link-btn{background:#2f8ee0;color:#fff;text-align:center;padding:6px;border-radius:4px;text-decoration:none;} .link-btn:hover{opacity:.8;} `); /*─────────────────────────────────────────────────────────────────────── 5. UI 参考 ───────────────────────────────────────────────────────────────────────*/ const UI = { panel, header: panel.querySelector('.ixl-header'), body: document.getElementById('ixl-body'), minBtn: document.getElementById('btn-min'), logsBtn: document.getElementById('btn-logs'), closeBtn: document.getElementById('btn-close'), tokenCount: document.getElementById('token-count'), modeSelect: document.getElementById('sel-mode'), startBtn: document.getElementById('btn-start'), rollbackBtn: document.getElementById('btn-rollback'), confAssistBtn: document.getElementById('btn-config-assist'), answerBox: document.getElementById('answer-box'), answerContent: document.getElementById('answer-content'), stepsContent: document.getElementById('steps-content'), progressArea: document.getElementById('progress-area'), progressBar: document.getElementById('progress-bar'), progressLabel: document.getElementById('progress-label'), statusLine: document.getElementById('status-line'), logArea: document.getElementById('log-area'), rentBtn: document.getElementById('btn-rent'), settingsBtn: document.getElementById('btn-settings'), settingsArea: document.getElementById('settings-area'), modelSelect: document.getElementById('sel-model'), modelDesc: document.getElementById('model-desc'), customModelArea: document.getElementById('custom-model-area'), customModelInput: document.getElementById('custom-model-input'), langSelect: document.getElementById('sel-lang'), autoSubmitRow: document.getElementById('auto-submit-row'), autoSubmitToggle: document.getElementById('chk-auto-submit'), txtApiKey: document.getElementById('txt-apikey'), saveKeyBtn: document.getElementById('btn-save-key'), testKeyBtn: document.getElementById('btn-test-key'), txtApiBase: document.getElementById('txt-apibase'), saveBaseBtn: document.getElementById('btn-save-base'), linkGetKey: document.getElementById('link-getkey'), refreshBtn: document.getElementById('btn-refresh') }; /*─────────────────────────────────────────────────────────────────────── 6. 日志助手 ───────────────────────────────────────────────────────────────────────*/ function logMsg(msg) { const div = document.createElement('div'); div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; UI.logArea.appendChild(div); console.log('[IXL-Auto]', msg); } function logDump(label, val) { try { logMsg(`[DUMP] ${label}: ${JSON.stringify(val)}`); } catch (e) { logMsg(`[DUMP] ${label}: ${String(val)}`); } } /*─────────────────────────────────────────────────────────────────────── 7. 更新语言文本 ───────────────────────────────────────────────────────────────────────*/ function updateLangText() { UI.logsBtn.textContent = UI.logArea.style.display === 'none' ? langText[config.language].logsButton : langText[config.language].logsHide; UI.closeBtn.textContent = langText[config.language].closeButton; UI.tokenCount.textContent = langText[config.language].tokensLabel + config.totalTokens; UI.statusLine.textContent = langText[config.language].statusIdle; UI.progressLabel.textContent = langText[config.language].statusWaiting; UI.modeSelect.options[0].text = langText[config.language].modeAuto; UI.modeSelect.options[1].text = langText[config.language].modeDisp; UI.startBtn.textContent = langText[config.language].startButton; UI.rollbackBtn.textContent = langText[config.language].rollbackButton; UI.confAssistBtn.textContent = langText[config.language].configAssistant; document.getElementById('answer-title').textContent = langText[config.language].finalAnswerTitle; document.getElementById('steps-title').textContent = langText[config.language].stepsTitle; UI.txtApiKey.placeholder = langText[config.language].placeKey; UI.txtApiBase.placeholder = langText[config.language].placeBase; UI.saveKeyBtn.textContent = langText[config.language].saveButton; UI.testKeyBtn.textContent = langText[config.language].testKeyButton; UI.saveBaseBtn.textContent = langText[config.language].saveButton; UI.linkGetKey.textContent = langText[config.language].getKeyLinkLabel; UI.refreshBtn.textContent = langText[config.language].refreshModels; UI.rentBtn.textContent = langText[config.language].rentKeyButton; UI.settingsBtn.textContent = langText[config.language].settingsKeyButton; UI.minBtn.title = langText[config.language].minButton; } updateLangText(); /*─────────────────────────────────────────────────────────────────────── 8. 构建模型选择 ───────────────────────────────────────────────────────────────────────*/ function buildModelSelect() { UI.modelSelect.innerHTML = ''; const ogPre = document.createElement('optgroup'); ogPre.label = 'Predefined'; ['gpt-4.1','gpt-4.1-mini','gpt-4.1-nano','gpt-4o','gpt-4o-mini','o3','o4-mini','o1','o3-mini','deepseek-reasoner','deepseek-chat','chatgpt-4o-least'] .forEach(m => { const o = document.createElement('option'); o.value = m; o.textContent = m; ogPre.appendChild(o); }); UI.modelSelect.appendChild(ogPre); const discovered = Object.keys(modelConfigs).filter(k => modelConfigs[k].discovered); if (discovered.length) { const ogDisc = document.createElement('optgroup'); ogDisc.label = 'Discovered'; discovered.forEach(m => { const o = document.createElement('option'); o.value = m; o.textContent = m; ogDisc.appendChild(o); }); UI.modelSelect.appendChild(ogDisc); } const optCust = document.createElement('option'); optCust.value = 'custom'; optCust.textContent = 'custom'; UI.modelSelect.appendChild(optCust); UI.modelSelect.value = config.selectedModel in modelDescDB ? config.selectedModel : 'custom'; UI.modelDesc.textContent = modelDescDB[config.selectedModel] || 'User-defined model'; UI.customModelArea.style.display = config.selectedModel === 'custom' ? 'block' : 'none'; } /*─────────────────────────────────────────────────────────────────────── 9. 拖拽 & 最小化 ───────────────────────────────────────────────────────────────────────*/ let dragOn = false, dx = 0, dy = 0; UI.header.addEventListener('mousedown', e => { if (e.target.tagName === 'BUTTON') return; dragOn = true; dx = e.clientX - panel.offsetLeft; dy = e.clientY - panel.offsetTop; panel.style.opacity = 0.8; }); document.addEventListener('mousemove', e => { if (!dragOn) return; panel.style.left = (e.clientX - dx) + 'px'; panel.style.top = (e.clientY - dy) + 'px'; }); document.addEventListener('mouseup', () => { dragOn = false; panel.style.opacity = 1; }); let minimized = false; UI.minBtn.addEventListener('click', () => { minimized = !minimized; UI.body.style.display = minimized ? 'none' : 'block'; UI.minBtn.textContent = minimized ? '+' : '—'; }); /*─────────────────────────────────────────────────────────────────────── 10. 事件绑定 ───────────────────────────────────────────────────────────────────────*/ UI.logsBtn.addEventListener('click', () => { UI.logArea.style.display = UI.logArea.style.display === 'none' ? 'block' : 'none'; updateLangText(); }); UI.closeBtn.addEventListener('click', () => { panel.style.display = 'none'; }); UI.modeSelect.addEventListener('change', () => { config.mode = UI.modeSelect.value; if (config.mode === 'autoFill') { UI.answerBox.style.display = 'none'; UI.autoSubmitRow.style.display = 'block'; alert(langText[config.language].disclaimAutoFill); } else { UI.answerBox.style.display = 'none'; UI.autoSubmitRow.style.display = 'none'; } }); UI.startBtn.addEventListener('click', startAnswer); UI.rollbackBtn.addEventListener('click', () => { if (config.lastState) { const d = getQuestionDiv(); if (d) { d.innerHTML = config.lastState; logMsg('Rolled back.'); } } else logMsg('No stored state.'); }); UI.confAssistBtn.addEventListener('click', openConfigAssistant); UI.autoSubmitToggle.addEventListener('change', () => { config.autoSubmit = UI.autoSubmitToggle.checked; }); UI.modelSelect.addEventListener('change', () => { config.selectedModel = UI.modelSelect.value; if (!modelConfigs[config.selectedModel]) { modelConfigs[config.selectedModel] = { apiKey: '', apiBase: 'https://api.openai.com/v1/chat/completions', discovered: false, modelList: [] }; } UI.customModelArea.style.display = config.selectedModel === 'custom' ? 'block' : 'none'; UI.modelDesc.textContent = modelDescDB[config.selectedModel] || 'User-defined model'; UI.txtApiKey.value = modelConfigs[config.selectedModel].apiKey; UI.txtApiBase.value = modelConfigs[config.selectedModel].apiBase; if (config.selectedModel.toLowerCase().includes('deepseek')) { UI.txtApiBase.value = 'https://api.deepseek.com/v1/chat/completions'; modelConfigs[config.selectedModel].apiBase = 'https://api.deepseek.com/v1/chat/completions'; } updateManageLink(); }); UI.customModelInput.addEventListener('change', () => { const name = UI.customModelInput.value.trim(); if (!name) return; config.selectedModel = name; if (!modelConfigs[name]) { modelConfigs[name] = { apiKey: '', apiBase: 'https://api.openai.com/v1/chat/completions', discovered: false, modelList: [] }; } buildModelSelect(); UI.modelSelect.value = 'custom'; UI.txtApiKey.value = modelConfigs[name].apiKey; UI.txtApiBase.value = modelConfigs[name].apiBase; updateManageLink(); }); UI.langSelect.addEventListener('change', () => { config.language = UI.langSelect.value; saveConfig(); updateLangText(); }); UI.rentBtn.addEventListener('click', openRentPopup); UI.saveKeyBtn.addEventListener('click', () => { modelConfigs[config.selectedModel].apiKey = UI.txtApiKey.value.trim(); saveConfig(); logMsg('API key saved.'); }); UI.testKeyBtn.addEventListener('click', testApiKey); UI.saveBaseBtn.addEventListener('click', () => { modelConfigs[config.selectedModel].apiBase = UI.txtApiBase.value.trim(); saveConfig(); logMsg('API base saved.'); }); UI.refreshBtn.addEventListener('click', refreshModelList); UI.settingsBtn.addEventListener('click', () => { UI.settingsArea.style.display = UI.settingsArea.style.display === 'none' ? 'block' : 'none'; }); /*─────────────────────────────────────────────────────────────────────── 11. 更新管理链接 ───────────────────────────────────────────────────────────────────────*/ function updateManageLink() { const mod = config.selectedModel.toLowerCase(); const link = mod.includes('deepseek') ? 'https://platform.deepseek.com/api_keys' : 'https://platform.openai.com/api-keys'; modelConfigs[config.selectedModel].manageUrl = link; UI.linkGetKey.href = link; saveConfig(); } /*─────────────────────────────────────────────────────────────────────── 12. 租用弹窗 ───────────────────────────────────────────────────────────────────────*/ function openRentPopup() { const overlay = document.createElement('div'); Object.assign(overlay.style, { position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.4)', zIndex: 999999999 }); const box = document.createElement('div'); Object.assign(box.style, { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: '300px', backgroundColor: '#fff', borderRadius: '6px', padding: '10px' }); box.innerHTML = ` <h3 style="margin-top:0;">Rent Key</h3> <p>Contact me to rent an API key:</p> <ul> <li>[email protected]</li> <li>[email protected]</li> </ul> <p>Thanks for supporting!</p> <button id="rent-close-btn">${langText[config.language].closeButton}</button> `; overlay.appendChild(box); document.body.appendChild(overlay); box.querySelector('#rent-close-btn').addEventListener('click', () => { document.body.removeChild(overlay); }); } /*─────────────────────────────────────────────────────────────────────── 13. 测试 API Key ───────────────────────────────────────────────────────────────────────*/ function testApiKey() { UI.statusLine.textContent = langText[config.language].testKeyMsg; const conf = modelConfigs[config.selectedModel]; const payload = { model: config.selectedModel, messages: [ { role: "system", content: "Test key." }, { role: "user", content: "Please ONLY respond with: test success" } ] }; GM_xmlhttpRequest({ method: "POST", url: conf.apiBase, headers: { "Content-Type": "application/json", "Authorization": "Bearer " + conf.apiKey }, data: JSON.stringify(payload), onload: (resp) => { UI.statusLine.textContent = langText[config.language].statusIdle; try { const data = JSON.parse(resp.responseText); const c = data.choices[0].message.content.toLowerCase(); alert(c.includes("test success") ? langText[config.language].keyOK : langText[config.language].keyBad); } catch (e) { alert("Parse error: " + e); } }, onerror: (err) => { UI.statusLine.textContent = langText[config.language].statusIdle; alert("Test error: " + JSON.stringify(err)); } }); } /*─────────────────────────────────────────────────────────────────────── 14. 刷新模型列表 ───────────────────────────────────────────────────────────────────────*/ function refreshModelList() { const c = modelConfigs[config.selectedModel]; if (!c) return; const url = c.apiBase.replace("/chat/completions", "/models"); logMsg("Refreshing models from: " + url); GM_xmlhttpRequest({ method: "GET", url: url, headers: { "Authorization": "Bearer " + c.apiKey }, onload: (resp) => { try { const d = JSON.parse(resp.responseText); logDump("Model Refresh", d); if (Array.isArray(d.data)) { const arr = d.data.map(x => x.id); c.modelList = arr; for (let m of arr) { if (!modelConfigs[m]) { modelConfigs[m] = { apiKey: c.apiKey, apiBase: c.apiBase, discovered: true, modelList: [] }; } } saveConfig(); buildModelSelect(); alert("Found models: " + arr.join(", ")); } } catch (e) { alert("Parse error: " + e); } }, onerror: (err) => { alert("Refresh error: " + JSON.stringify(err)); } }); } /*─────────────────────────────────────────────────────────────────────── 15. Config Assistant ───────────────────────────────────────────────────────────────────────*/ function openConfigAssistant() { const overlay = document.createElement('div'); Object.assign(overlay.style, { position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 999999999 }); const box = document.createElement('div'); Object.assign(box.style, { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: '340px', backgroundColor: '#fff', borderRadius: '6px', padding: '10px' }); box.innerHTML = ` <h3 style="margin-top:0;">${langText[config.language].configAssistant}</h3> <textarea id="assistant-inp" style="width:100%;height:80px;"></textarea> <button id="assistant-ask" style="margin-top:6px;">${langText[config.language].shortAI}</button> <button id="assistant-close" style="margin-top:6px;">${langText[config.language].closeButton}</button> <div id="assistant-out" style="margin-top:6px;border:1px solid #ccc;background:#fafafa;padding:6px;white-space:pre-wrap;max-height:200px;overflow-y:auto;"></div>`; overlay.appendChild(box); document.body.appendChild(overlay); const closeBtn = box.querySelector('#assistant-close'); const askBtn = box.querySelector('#assistant-ask'); const inp = box.querySelector('#assistant-inp'); const outDiv = box.querySelector('#assistant-out'); closeBtn.addEventListener('click', () => document.body.removeChild(overlay)); askBtn.addEventListener('click', () => { const q = inp.value.trim(); if (!q) return; outDiv.textContent = '(waiting…)'; askAssistant(q, resp => { outDiv.innerHTML = marked.parse(resp || ''); }, err => { outDiv.textContent = '[Error] ' + err; } ); }); } function askAssistant(question, onSuccess, onError) { const conf = modelConfigs[config.selectedModel]; const payload = { model: config.selectedModel, messages: [ { role: 'system', content: 'You are the config assistant. Provide concise, helpful configuration advice.' }, { role: 'user', content: question } ] }; GM_xmlhttpRequest({ method: 'POST', url: conf.apiBase, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + conf.apiKey }, data: JSON.stringify(payload), onload: resp => { try { const d = JSON.parse(resp.responseText); onSuccess(d.choices[0].message.content); } catch (e) { onError(e); } }, onerror: err => { onError(err); } }); } /*─────────────────────────────────────────────────────────────────────── 16. 获取题目 DIV / 捕获 LaTeX / 画布 ───────────────────────────────────────────────────────────────────────*/ function getQuestionDiv() { let d = document.evaluate( '/html/body/main/div/article/section/section/div/div[1]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; if (!d) d = document.querySelector('main div.article, main>div, article'); return d; } function captureLatex(div) { const arr = div.querySelectorAll('script[type="math/tex"], .MathJax, .mjx-chtml'); if (arr.length) { let s = ''; arr.forEach(e => s += e.textContent + '\n'); return s; } return null; } function captureCanvas(div) { const c = div.querySelector('canvas'); if (c) { const cv = document.createElement('canvas'); cv.width = c.width; cv.height = c.height; cv.getContext('2d').drawImage(c, 0, 0); return cv.toDataURL('image/png').split(',')[1]; } return null; } /*─────────────────────────────────────────────────────────────────────── 17. 进度条助手 ───────────────────────────────────────────────────────────────────────*/ let progTimer = null; function startProgress() { UI.progressArea.style.display = 'block'; UI.progressBar.value = 0; progTimer = setInterval(() => { if (UI.progressBar.value < 90) UI.progressBar.value += 2; }, 200); } function stopProgress() { clearInterval(progTimer); UI.progressBar.value = 100; setTimeout(() => { UI.progressArea.style.display = 'none'; UI.progressBar.value = 0; }, 400); } /*─────────────────────────────────────────────────────────────────────── 18. 主逻辑:startAnswer() ───────────────────────────────────────────────────────────────────────*/ function startAnswer() { logMsg('Start pressed.'); const qDiv = getQuestionDiv(); if (!qDiv) { logMsg('Question div not found'); return; } config.lastState = qDiv.innerHTML; let userPrompt = 'HTML:\n' + qDiv.outerHTML + '\n'; const latex = captureLatex(qDiv); if (latex) userPrompt += 'LaTeX:\n' + latex + '\n'; else { const c64 = captureCanvas(qDiv); if (c64) userPrompt += 'Canvas image base64 attached.\n'; } UI.answerBox.style.display = 'none'; UI.statusLine.textContent = langText[config.language].statusWaiting; startProgress(); const autoFillPrompt = ` You are an IXL math solver with automation support. 1. Solve the problem. 2. Provide final answer inside <answer>...</answer>. 3. After a blank line, show steps in Markdown. 4. At end, include one \`\`\`javascript block to autofill the input.`; const displayOnlyPrompt = ` You are an IXL math solver. First return <answer>RESULT</answer> on its own line. Then a blank line, then solution steps in Markdown.`; const messages = config.mode === 'autoFill' ? [{ role: 'system', content: autoFillPrompt }, { role: 'user', content: userPrompt }] : [{ role: 'system', content: displayOnlyPrompt }, { role: 'user', content: userPrompt }]; const payload = { model: config.selectedModel, messages: messages, stream: config.mode === 'displayOnly' }; const conf = modelConfigs[config.selectedModel]; if (config.mode === 'displayOnly') { // SSE 流式 let buffer = ''; let answerDone = false; GM_xmlhttpRequest({ method: 'POST', url: conf.apiBase, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + conf.apiKey, 'Accept': 'text/event-stream' }, data: JSON.stringify(payload), onprogress: e => { const chunk = e.responseText.substring(e.loadedPrev || 0); e.loadedPrev = e.responseText.length; const lines = chunk.split('\n').filter(l => l.startsWith('data:')); lines.forEach(line => { const data = line.replace(/^data:\s*/, '').trim(); if (data === '[DONE]') return; try { const json = JSON.parse(data); const delta = json.choices?.[0]?.delta?.content; if (!delta) return; buffer += delta; if (!answerDone) { const m = buffer.match(/<answer>[\s\S]*?<\/answer>/i); if (m) { answerDone = true; UI.answerContent.innerHTML = marked.parse(wrapLatex(m[0])); UI.answerBox.style.display = 'block'; if (window.MathJax && typeof MathJax.typesetPromise === 'function') { MathJax.typesetPromise([UI.answerContent]).catch(() => {}); } } } } catch {} }); }, onload: () => { stopProgress(); const md = buffer.replace(/<answer>[\s\S]*?<\/answer>/i, '').trim(); UI.stepsContent.innerHTML = marked.parse(wrapLatex(unescapeDollar(md))); if (window.MathJax && typeof MathJax.typesetPromise === 'function') { MathJax.typesetPromise([UI.stepsContent]).catch(() => {}); } UI.statusLine.textContent = langText[config.language].statusDone; }, onerror: err => { stopProgress(); UI.statusLine.textContent = 'Stream error'; logDump('SSE error', err); } }); return; } // AutoFill 模式 GM_xmlhttpRequest({ method: 'POST', url: conf.apiBase, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + conf.apiKey }, data: JSON.stringify(payload), onload: resp => { stopProgress(); try { const d = JSON.parse(resp.responseText); logDump('GPT raw', d); if (d.usage?.total_tokens) { config.totalTokens += d.usage.total_tokens; UI.tokenCount.textContent = langText[config.language].tokensLabel + config.totalTokens; } const out = d.choices[0].message.content; const ansMatch = out.match(/<answer>([\s\S]*?)<\/answer>/i); const ansTag = ansMatch ? ansMatch[0] : `<answer>${langText[config.language].missingAnswerTag}</answer>`; const steps = ansMatch ? out.replace(ansTag, '') : out; UI.answerContent.innerHTML = marked.parse(wrapLatex(ansTag)); UI.stepsContent.innerHTML = marked.parse(wrapLatex(unescapeDollar(steps))); if (window.MathJax && typeof MathJax.typesetPromise === 'function') { MathJax.typesetPromise([UI.answerContent, UI.stepsContent]).catch(() => {}); } const codeMatch = out.match(/```(?:javascript|js)?\s*([\s\S]*?)```/i); if (codeMatch && codeMatch[1]) { try { (new Function(codeMatch[1]))(); } catch (e) { logDump('RunJS error', e); } if (config.autoSubmit) { const btn = document.querySelector('button.submit, button[class*=submit]'); if (btn) btn.click(); } } else { logMsg('No JS code block found'); } UI.statusLine.textContent = langText[config.language].statusDone; } catch (e) { UI.statusLine.textContent = 'Parse error'; logDump('Parse error', e); } }, onerror: err => { stopProgress(); UI.statusLine.textContent = langText[config.language].requestError + JSON.stringify(err); logDump('Request error', err); } }); } /*─────────────────────────────────────────────────────────────────────── 19. 初始化 ───────────────────────────────────────────────────────────────────────*/ function initAll() { buildModelSelect(); UI.txtApiKey.value = modelConfigs[config.selectedModel].apiKey; UI.txtApiBase.value = modelConfigs[config.selectedModel].apiBase; UI.modeSelect.value = config.mode; UI.autoSubmitRow.style.display = config.mode === 'autoFill' ? 'block' : 'none'; UI.langSelect.value = config.language; updateManageLink(); updateLangText(); document.getElementById('settings-area').style.display = 'none'; logMsg('IXL Auto Answer v9.1 loaded.'); } window.MathJax = { tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] }, svg: { fontCache: 'global' } }; initAll(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址