// ==UserScript== // @name 超星作业 AI 助手 // @namespace https://github.com/TextlineX/chaoxing-ai-assistant // @homepageURL https://github.com/TextlineX/chaoxing-ai-assistant // @supportURL https://greasyfork.org/zh-CN/scripts/577882-%E8%B6%85%E6%98%9F%E4%BD%9C%E4%B8%9A-ai-%E5%8A%A9%E6%89%8B // @icon https://raw.githubusercontent.com/TextlineX/chaoxing-ai-assistant/main/docs/icon.svg // @version 24.5 // @description 用于超星学习通作业页面的用户脚本,支持题目导出、AI 回填、UEditor 解锁、拖动浮球与可配置面板。 // @author Textline // @license MIT // @match https://mooc1.chaoxing.com/mooc-ans/mooc2/work/view* // @match https://mooc1.chaoxing.com/mooc-ans/mooc2/work/dowork* // @grant GM_setClipboard // @grant GM_addStyle // @run-at document-end // ==/UserScript== (function() { 'use strict'; // 1. 解锁粘贴限制 const hookUEditor = () => { if (window.UE && window.UE.Editor) { const originalFire = window.UE.Editor.prototype.fireEvent; window.UE.Editor.prototype.fireEvent = function(type) { if (type === 'beforepaste') return; return originalFire.apply(this, arguments); }; log("🔓 粘贴限制已自动解除"); } else { setTimeout(hookUEditor, 1500); } }; GM_addStyle(` :root { --yan-panel-width: 360px; --yan-panel-opacity: 0.97; --yan-float-duration: 4.8s; --yan-ball-size: 64px; } #yan-ball { position: fixed; left: 20px; top: 20px; width: var(--yan-ball-size); height: var(--yan-ball-size); background: radial-gradient(circle at 28% 24%, rgba(255,255,255,0.98) 0 8%, rgba(255,255,255,0.38) 10%, rgba(255,255,255,0) 30%), radial-gradient(circle at 68% 72%, rgba(31,145,255,0.24) 0 14%, rgba(31,145,255,0) 54%), linear-gradient(145deg, #58b9ff 0%, #2d93ff 40%, #0f68ef 100%); border-radius: 46% 54% 52% 48% / 44% 42% 58% 56%; box-shadow: 0 18px 34px rgba(8, 72, 170, 0.28), inset 0 1px 3px rgba(255,255,255,0.45), inset 0 -10px 18px rgba(0,0,0,0.14); cursor: grab; z-index: 9999999; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 13px; font-weight: 700; letter-spacing: 0.04em; user-select: none; -webkit-user-select: none; touch-action: none; overflow: hidden; transition: box-shadow 0.22s ease, filter 0.22s ease; animation: yan-morph var(--yan-float-duration) ease-in-out infinite, yan-breathe 3.1s ease-in-out infinite, yan-drift 7.8s ease-in-out infinite; will-change: transform, left, top, border-radius; } #yan-ball::before { content: ""; position: absolute; inset: -12%; border-radius: 48% 52% 54% 46% / 50% 46% 54% 50%; background: radial-gradient(circle at 28% 28%, rgba(255,255,255,0.56) 0 10%, rgba(255,255,255,0.12) 28%, rgba(255,255,255,0) 56%), radial-gradient(circle at 68% 72%, rgba(34,141,255,0.32) 0 12%, rgba(34,141,255,0) 60%); filter: blur(6px); opacity: 0.9; transform: translate3d(0, 0, 0); animation: yan-swim calc(var(--yan-float-duration) * 1.15) ease-in-out infinite; pointer-events: none; } #yan-ball::after { content: ""; position: absolute; inset: 12% 16% 50% 16%; border-radius: 50%; background: linear-gradient(to bottom, rgba(255,255,255,0.72), rgba(255,255,255,0)); filter: blur(1px); opacity: 0.95; animation: yan-sheen 2.7s ease-in-out infinite; pointer-events: none; } #yan-ball span { position: relative; z-index: 1; text-shadow: 0 1px 2px rgba(0,0,0,0.16); } #yan-ball:hover { filter: saturate(1.05) brightness(1.03); box-shadow: 0 20px 38px rgba(8, 72, 170, 0.32), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -10px 18px rgba(0,0,0,0.12); } #yan-ball.dragging { cursor: grabbing; animation: none; transform: scale(1.08); box-shadow: 0 24px 42px rgba(8, 72, 170, 0.35), inset 0 1px 3px rgba(255,255,255,0.5), inset 0 -10px 18px rgba(0,0,0,0.12); } #yan-ball.was-dragged { animation: yan-settle 0.22s ease-out; } #yan-ball.releasing { animation: yan-release 0.36s cubic-bezier(.2,.8,.2,1); } #yan-panel { position: fixed; width: var(--yan-panel-width); background: rgba(255,255,255,var(--yan-panel-opacity)); border-radius: 22px; box-shadow: 0 24px 60px rgba(0,0,0,0.18); border: 1px solid rgba(255,255,255,0.72); backdrop-filter: blur(14px); z-index: 9999998; overflow: hidden; font-family: sans-serif; opacity: 0; transform: translateY(14px) scale(0.96); transition: opacity 220ms ease, transform 260ms cubic-bezier(.2,.9,.2,1); pointer-events: none; } #yan-panel.is-open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; } .yan-panel-shell { display: flex; flex-direction: column; max-height: min(82vh, 760px); } #yan-wizard-mask { position: fixed; inset: 0; background: rgba(13, 22, 39, 0.42); backdrop-filter: blur(8px); z-index: 9999997; opacity: 0; pointer-events: none; transition: opacity 220ms ease; } #yan-wizard-mask.is-open { opacity: 1; pointer-events: auto; } #yan-wizard-panel { position: fixed; left: 50%; top: 50%; z-index: 9999998; width: min(440px, calc(100vw - 32px)); border-radius: 24px; background: rgba(255,255,255,0.98); box-shadow: 0 26px 70px rgba(0,0,0,0.22); border: 1px solid rgba(255,255,255,0.72); overflow: hidden; opacity: 0; transform: translate(-50%, -46%) scale(0.96); pointer-events: none; transition: opacity 220ms ease, transform 260ms cubic-bezier(.2,.9,.2,1); } #yan-wizard-panel.is-open { opacity: 1; transform: translate(-50%, -50%) scale(1); pointer-events: auto; } .yan-wizard-shell { display: flex; flex-direction: column; } .yan-wizard-head { padding: 18px 18px 14px; background: linear-gradient(180deg, #fdfefe 0%, #f3f7ff 100%); border-bottom: 1px solid #e8eef9; } .yan-wizard-head-top { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; } .yan-wizard-title { margin: 0; font-size: 17px; font-weight: 900; color: #20324a; } .yan-wizard-subtitle { margin-top: 8px; font-size: 12px; line-height: 1.6; color: #61738b; } .yan-wizard-body { padding: 16px 18px 18px; } .yan-wizard-step { display: grid; gap: 10px; padding: 14px; border-radius: 18px; background: linear-gradient(180deg, #fbfcff 0%, #f8fbff 100%); border: 1px solid #edf1f7; margin-bottom: 12px; } .yan-wizard-step:last-child { margin-bottom: 0; } .yan-wizard-step-head { display: flex; align-items: center; gap: 10px; font-weight: 900; color: #21314a; font-size: 14px; } .yan-wizard-step-number { width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #fff; background: linear-gradient(135deg, #3e76ff 0%, #1a57e8 100%); box-shadow: 0 10px 20px rgba(34, 89, 226, 0.16); flex: 0 0 auto; } .yan-wizard-step p { margin: 0; font-size: 13px; line-height: 1.65; color: #5d6f86; } .yan-wizard-actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; } .yan-wizard-btn { flex: 1 1 140px; border: none; border-radius: 14px; padding: 12px 14px; font-size: 13px; font-weight: 800; cursor: pointer; transition: transform 160ms ease, box-shadow 160ms ease, filter 160ms ease; } .yan-wizard-btn:hover { transform: translateY(-1px); filter: brightness(1.02); } .yan-wizard-btn.primary { color: #fff; background: linear-gradient(135deg, #3e76ff 0%, #1a57e8 100%); box-shadow: 0 12px 24px rgba(34, 89, 226, 0.18); } .yan-wizard-btn.secondary { color: #243449; background: #eff4ff; border: 1px solid #dbe5fb; } .yan-wizard-btn.secondary:hover { background: #e7efff; color: #1f3357; } .yan-header { padding: 14px 16px 12px; background: linear-gradient(180deg, #fafcff 0%, #eff5ff 100%); border-bottom: 1px solid #e8eef9; } .yan-header-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } .yan-title-wrap { display: flex; flex-direction: column; gap: 6px; } .yan-title { font-weight: 800; font-size: 16px; color: #20324a; display: block; letter-spacing: 0.01em; } .yan-subtitle { font-size: 12px; color: #62738b; line-height: 1.4; } .yan-close { border: none; background: rgba(27, 52, 89, 0.08); color: #20324a; width: 30px; height: 30px; border-radius: 50%; cursor: pointer; flex: 0 0 auto; } .yan-body { padding: 14px; overflow: auto; } .yan-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 10px 14px 0; } .yan-tab { border: 1px solid #e6ecf6; background: #f8fbff; color: #42526b; border-radius: 999px; padding: 10px 12px; font-size: 12px; font-weight: 800; cursor: pointer; transition: transform 160ms ease, background 160ms ease, color 160ms ease, border-color 160ms ease; } .yan-tab:hover { transform: translateY(-1px); background: #eef4ff; border-color: #cfdcf8; } .yan-tab.is-active { background: linear-gradient(135deg, #3e76ff 0%, #1a57e8 100%); border-color: transparent; color: #fff; box-shadow: 0 10px 22px rgba(34, 89, 226, 0.18); } .yan-page { display: none; animation: yan-page-in 180ms ease-out; } .yan-page.is-active { display: block; } .yan-section { border: 1px solid #edf1f7; background: #fff; border-radius: 18px; margin-bottom: 12px; overflow: hidden; } .yan-section-title { padding: 10px 14px 8px; font-size: 13px; font-weight: 800; color: #21314a; background: linear-gradient(180deg, #fbfcfe 0%, #f7f9fd 100%); border-bottom: 1px solid #edf1f7; letter-spacing: 0.02em; } .yan-section-desc { padding: 10px 14px 0; font-size: 12px; line-height: 1.55; color: #718197; } .yan-action-grid { display: grid; grid-template-columns: 1fr; gap: 10px; padding: 12px 14px 14px; } .yan-btn { width: 100%; padding: 13px 16px; border: none; cursor: pointer; font-weight: 700; font-size: 14px; color: #eaf1ff; text-align: left; border-radius: 14px; box-shadow: 0 8px 18px rgba(0,0,0,0.1); transition: transform 160ms ease, box-shadow 160ms ease, filter 160ms ease; } .yan-btn:hover { transform: translateY(-1px); filter: brightness(1.03); box-shadow: 0 12px 24px rgba(0,0,0,0.14); } .yan-btn:active { transform: translateY(0); } #btn-export { background: linear-gradient(135deg, #364a63 0%, #243449 100%); } #btn-import { background: linear-gradient(135deg, #25b46b 0%, #18a35d 100%); } #btn-reset { background: linear-gradient(135deg, #f59b23 0%, #e77b12 100%); } #btn-wizard { background: linear-gradient(135deg, #f8fbff 0%, #eef4ff 100%); color: #243449; border: 1px solid #dbe5fb; box-shadow: 0 8px 18px rgba(25, 53, 96, 0.06); } #btn-wizard:hover { color: #1f3357; background: linear-gradient(135deg, #eff4ff 0%, #e4ecff 100%); } .yan-settings { display: grid; gap: 12px; padding: 12px 14px 14px; } .yan-field { display: grid; gap: 8px; } .yan-field-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 12px; color: #42526b; } .yan-field-label { font-weight: 700; color: #22324a; } .yan-field-value { font-variant-numeric: tabular-nums; color: #61738b; } .yan-range { width: 100%; margin: 0; accent-color: #3f79ff; } .yan-switch { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #edf1f7; border-radius: 14px; background: #fbfcfe; } .yan-switch small { display: block; color: #7a8797; margin-top: 4px; line-height: 1.4; } .yan-switch strong { color: #21314a; } .yan-switch input { width: 18px; height: 18px; } .yan-setting-select { width: 100%; padding: 11px 12px; border: 1px solid #dfe6f1; border-radius: 14px; background: #fff; color: #22324a; font-weight: 700; outline: none; } .yan-setting-select option { color: #22324a; } #yan-log { height: 160px; background: #20242a; color: #89ff9c; overflow-y: auto; padding: 12px; font-size: 12px; font-family: monospace; border-radius: 0 0 18px 18px; } .yan-log-frame { border: 1px solid #edf1f7; border-radius: 18px; overflow: hidden; background: #20242a; } @keyframes yan-morph { 0%, 100% { border-radius: 46% 54% 52% 48% / 44% 42% 58% 56%; transform: translate3d(0, 0, 0) rotate(-2deg); } 25% { border-radius: 52% 48% 41% 59% / 50% 58% 42% 50%; transform: translate3d(0, -4px, 0) rotate(1deg); } 50% { border-radius: 42% 58% 56% 44% / 58% 46% 54% 42%; transform: translate3d(0, 2px, 0) rotate(2deg); } 75% { border-radius: 58% 42% 48% 52% / 42% 56% 44% 58%; transform: translate3d(0, -2px, 0) rotate(-1deg); } } @keyframes yan-swim { 0%, 100% { transform: translate3d(-6px, -2px, 0) scale(1); } 33% { transform: translate3d(5px, 3px, 0) scale(1.03); } 66% { transform: translate3d(-2px, 5px, 0) scale(1.01); } } @keyframes yan-sheen { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.95; } 50% { transform: translate3d(2px, 4px, 0) scale(0.98); opacity: 0.8; } } @keyframes yan-breathe { 0%, 100% { filter: saturate(1) brightness(1); } 50% { filter: saturate(1.08) brightness(1.03); } } @keyframes yan-settle { 0% { transform: scale(1.12); } 70% { transform: scale(0.98); } 100% { transform: scale(1); } } @keyframes yan-release { 0% { transform: scale(1.05); } 40% { transform: scale(0.96); } 100% { transform: scale(1); } } @keyframes yan-drift { 0%, 100% { filter: hue-rotate(0deg) saturate(1); } 50% { filter: hue-rotate(10deg) saturate(1.08); } } @keyframes yan-page-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } @media (prefers-reduced-motion: reduce) { #yan-ball, #yan-ball::before, #yan-ball::after, #yan-panel { animation: none !important; transition-duration: 0.01ms !important; } } `); const log = (msg, color="#00ff00") => { const box = document.getElementById('yan-log'); if (box) { const div = document.createElement('div'); div.style.color = color; div.innerText = `> ${msg}`; box.appendChild(div); box.scrollTop = box.scrollHeight; } }; const STORAGE_KEYS = { ballPosition: 'chaoxing-ai-assistant.ball-position.v2', settings: 'chaoxing-ai-assistant.ui-settings.v2' }; const DEFAULT_MARGIN = 20; const DEFAULT_SETTINGS = { ballSize: 64, floatDuration: 4.8, panelWidth: 360, panelOpacity: 97, rememberPosition: true, reducedMotion: false, autoOpenPage: 'basic' }; const WIZARD_SEEN_KEY = 'chaoxing-ai-assistant.wizard-seen.v1'; const clamp = (value, min, max) => Math.min(Math.max(value, min), max); const safeParse = (text) => { try { return JSON.parse(text); } catch (e) { return null; } }; const loadSettings = () => { const raw = safeParse(localStorage.getItem(STORAGE_KEYS.settings) || ''); const merged = { ...DEFAULT_SETTINGS, ...(raw || {}) }; merged.ballSize = clamp(Number(merged.ballSize) || DEFAULT_SETTINGS.ballSize, 52, 92); merged.floatDuration = clamp(Number(merged.floatDuration) || DEFAULT_SETTINGS.floatDuration, 2.8, 8); merged.panelWidth = clamp(Number(merged.panelWidth) || DEFAULT_SETTINGS.panelWidth, 300, 480); merged.panelOpacity = clamp(Number(merged.panelOpacity) || DEFAULT_SETTINGS.panelOpacity, 85, 100); merged.rememberPosition = Boolean(merged.rememberPosition); merged.reducedMotion = Boolean(merged.reducedMotion); merged.autoOpenPage = ['basic', 'output', 'settings'].includes(merged.autoOpenPage) ? merged.autoOpenPage : DEFAULT_SETTINGS.autoOpenPage; return merged; }; const saveSettings = (settings) => { localStorage.setItem(STORAGE_KEYS.settings, JSON.stringify(settings)); }; let uiSettings = loadSettings(); const getBallSize = () => uiSettings.ballSize; const getDefaultBallPosition = () => ({ left: DEFAULT_MARGIN, top: Math.max(DEFAULT_MARGIN, window.innerHeight - getBallSize() - 80) }); const loadBallPosition = () => { if (!uiSettings.rememberPosition) return null; const parsed = safeParse(localStorage.getItem(STORAGE_KEYS.ballPosition) || ''); if (parsed && Number.isFinite(parsed.left) && Number.isFinite(parsed.top)) { return parsed; } return null; }; const saveBallPosition = (ball) => { if (!uiSettings.rememberPosition) { localStorage.removeItem(STORAGE_KEYS.ballPosition); return; } const rect = ball.getBoundingClientRect(); localStorage.setItem(STORAGE_KEYS.ballPosition, JSON.stringify({ left: Math.round(rect.left), top: Math.round(rect.top) })); }; const applyBallStyle = (ball) => { const size = getBallSize(); ball.style.width = `${size}px`; ball.style.height = `${size}px`; ball.style.fontSize = `${clamp(Math.round(size * 0.2), 11, 16)}px`; ball.style.setProperty('--yan-float-duration', `${uiSettings.floatDuration}s`); ball.style.animation = uiSettings.reducedMotion ? 'none' : `yan-morph ${uiSettings.floatDuration}s ease-in-out infinite, yan-breathe 3.1s ease-in-out infinite`; }; const applyPanelStyle = (panel) => { document.documentElement.style.setProperty('--yan-panel-width', `${uiSettings.panelWidth}px`); document.documentElement.style.setProperty('--yan-panel-opacity', `${uiSettings.panelOpacity / 100}`); panel.style.width = `${uiSettings.panelWidth}px`; }; const applySettingsToUI = (ball, panel, controls = {}) => { applyBallStyle(ball); applyPanelStyle(panel); if (controls.ballSize) controls.ballSize.value = String(uiSettings.ballSize); if (controls.floatDuration) controls.floatDuration.value = String(uiSettings.floatDuration); if (controls.panelWidth) controls.panelWidth.value = String(uiSettings.panelWidth); if (controls.panelOpacity) controls.panelOpacity.value = String(uiSettings.panelOpacity); if (controls.rememberPosition) controls.rememberPosition.checked = uiSettings.rememberPosition; if (controls.reducedMotion) controls.reducedMotion.checked = uiSettings.reducedMotion; if (controls.autoOpenPage) controls.autoOpenPage.value = uiSettings.autoOpenPage; }; const hasSeenWizard = () => localStorage.getItem(WIZARD_SEEN_KEY) === '1'; const markWizardSeen = () => localStorage.setItem(WIZARD_SEEN_KEY, '1'); const applyBallPosition = (ball, left, top) => { const size = getBallSize(); const maxLeft = Math.max(DEFAULT_MARGIN, window.innerWidth - size - DEFAULT_MARGIN); const maxTop = Math.max(DEFAULT_MARGIN, window.innerHeight - size - DEFAULT_MARGIN); ball.style.left = `${clamp(left, DEFAULT_MARGIN, maxLeft)}px`; ball.style.top = `${clamp(top, DEFAULT_MARGIN, maxTop)}px`; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }; const positionPanel = (ball, panel) => { const ballRect = ball.getBoundingClientRect(); const panelRect = panel.getBoundingClientRect(); const gap = 14; let left = ballRect.left; let top = ballRect.top - panelRect.height - gap; const below = top < DEFAULT_MARGIN; if (below) { top = ballRect.bottom + gap; } left = clamp(left, DEFAULT_MARGIN, Math.max(DEFAULT_MARGIN, window.innerWidth - panelRect.width - DEFAULT_MARGIN)); top = clamp(top, DEFAULT_MARGIN, Math.max(DEFAULT_MARGIN, window.innerHeight - panelRect.height - DEFAULT_MARGIN)); panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.bottom = 'auto'; panel.style.transformOrigin = below ? 'left top' : 'left bottom'; }; const createRangeField = (id, label, min, max, step, unit, valueText) => `
导出题目、一键回填、恢复默认参数。先把最常用的动作放在这里,方便你直接开干。
看导出内容、回填结果和解析日志。这里负责把过程和结果展示清楚。
调浮球大小、动效、透明度和默认打开页。需要怎么用,直接在这里调。