// ==UserScript== // @name 中小学教师人工智能应用能力培训专|全民AI智慧教育培训自动助手 // @namespace https://www.wlxy.top // @version 1.0 // @author 柠檬真酸 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @description 支持高等教育出版社,全民AI智慧教育培训中小学教师人工智能应用能力培训专项aiedu.cnve.com、ifelong. smartedu. cn/Event/AItsjy,训前测评、能力画像、培训课单、作业提交、结课考试一键完成 // @match https://aiedu.cnve.com/* // @match https://lifelong. smartedu. cn/Event/AItsjy // @connect aiedu.cnve.com // @connect oa6.ahzsksw.cn // @connect * // @connect ccfile.chinabett.com // @connect teacher-eval-prod.oss-cn-beijing.aliyuncs.com // @connect cdn.jsdelivr.net // @grant GM_addStyle // @grant GM_xmlhttpRequest // @run-at document-idle // ==/UserScript== (function () { "use strict"; const _w=(()=>{ const S=[ "jMXVz4jJwM/P2YLd28XVy5zR29He1tyVyMjczMs=", "jMXVz4jJwM/P2YLd28XVy5zR29He1tyVyMjYzg==", "jMXVz4jJwM/P2YLNw9nU3MeZ1tnZ3tDd", "jMXVz4jJwM/P2YLezt7U3p7a2sLe29w=", "jMXVz4jJwM/P2YLCytHC1w==", "jMXVz4jJwM/P2YLCxtPU3MDRmsDSytDcwg==", "y9DR1tSShoXEzZuAztjLwdjHwpjU1g==", "y9DR1tSShoXfyczNx9XDnNDaw9OZ29bXlM7Y0MWopKyk66mpoKGn" ]; const k=1443; return (i)=>{ const b=atob(S[i]); let r=""; for (let j=0;j { try { if (typeof GM_info !== "undefined" && GM_info?.script?.version) return String(GM_info.script.version); } catch (_) {} return "2.0.0"; })(); const state = { running: false, stopFlag: false, userId: "", logLines: [], lastProgressLogKey: "", cloudApiBase: DEFAULT_CLOUD_API_BASE, proBuyUrl: PRO_BUY_URL, panelNoticePath: _w(3), cloudToken: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim(), cloudTier: String(localStorage.getItem(CLOUD_TOKEN_KEY) || "").trim() ? "unknown" : "free", cloudLease: "", cloudLeaseExp: 0, cloudProExpireAt: 0, cloudProExpireText: "", cloudTokenVerified: false, cloudSaving: false, freePreLimit: 2, freeUsedPre: 0, remotePanelNotice: "", userProfile: null, panelProgressBusy: false, panelProject: null, panelPreDims: [], panelExamDims: [], panelCourses: [], panelAssignments: [], panelPortrait: null, panelPortraitStatus: null, panelOpenStep: "", panelTab: "project", presetFileCache: {}, certBannerHidden: false, }; const PROGRESS_STEPS = [ { key: "intro", label: "\u9879\u76ee\u4ecb\u7ecd", proOnly: false, static: true }, { key: "assessment", label: "\u8bad\u524d\u6d4b\u8bc4", proOnly: false }, { key: "profile", label: "\u80fd\u529b\u753b\u50cf", proOnly: true }, { key: "curriculum", label: "\u57f9\u8bad\u8bfe\u5355", proOnly: true }, { key: "assignment", label: "\u4f5c\u4e1a\u63d0\u4ea4", proOnly: true }, { key: "exam", label: "\u7ed3\u4e1a\u8003\u8bd5", proOnly: true }, ]; function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function esc(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">"); } function getToken() { return (localStorage.getItem("token") || localStorage.getItem("silentToken") || "").trim(); } function getGjsToken() { return (localStorage.getItem("gjsToken") || "").trim(); } function getUserId() { if (state.userId) return state.userId; try { const raw = sessionStorage.getItem("storage-useUserInfo"); if (raw) { const uid = JSON.parse(raw)?.state?.userInfoData?.userId; if (uid) { state.userId = String(uid); return state.userId; } } } catch (_) {} return ""; } function getUserProfile() { if (state.userProfile?.learning_user_id) return state.userProfile; try { const raw = sessionStorage.getItem("storage-useUserInfo"); if (!raw) return null; const u = JSON.parse(raw)?.state?.userInfoData || {}; const prof = { learning_user_id: String(u.userId || getUserId() || "").trim() }; if (u.name || u.realName) prof.name = String(u.name || u.realName).trim(); if (u.loginName) prof.userName = String(u.loginName).trim(); if (u.mobile || u.phone) prof.phoneNum = String(u.mobile || u.phone).trim(); if (u.schoolName || u.orgName) prof.schoolName = String(u.schoolName || u.orgName).trim(); return prof.learning_user_id ? prof : null; } catch (_) { return null; } } async function _fp() { if (state.userProfile?.learning_user_id) return state.userProfile; try { const res = await api("GET", TEACHER_API + "/user/get-user-info"); const u = res.data || {}; const uid = String(u.userId || "").trim(); if (!uid) return getUserProfile(); state.userId = uid; state.userProfile = { learning_user_id: uid, name: String(u.name || "").trim(), userName: String(u.loginName || "").trim(), phoneNum: String(u.mobile || "").trim(), schoolName: String(u.schoolName || "").trim(), }; return state.userProfile; } catch (_) { return getUserProfile(); } } function isLoggedIn() { return !!getToken(); } let cloudLeaseInflight = null; function withTimeout(promise, ms, label) { const sec = Math.max(1, Math.floor(Number(ms || 30000) / 1000)); return Promise.race([ promise, new Promise(function (_, reject) { setTimeout(function () { reject(new Error(String(label || "\u8bf7\u6c42") + "\u8d85\u65f6\uff08" + sec + "s\uff09")); }, ms || 30000); }), ]); } async function api(method, path, { params, body, timeoutMs } = {}) { const url = new URL(path, location.origin); if (params) { Object.entries(params).forEach(([k, v]) => { if (v != null && v !== "") url.searchParams.set(k, String(v)); }); } const headers = { Accept: "application/json, text/plain, */*", "X-App-Id": APP_ID }; const token = getToken(); if (!token) throw new Error("\u672a\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\u5e73\u53f0"); headers.Authorization = token; const gjs = getGjsToken(); if (gjs) headers["gjs-token"] = gjs; if (body != null) headers["Content-Type"] = "application/json;charset=UTF-8"; const controller = typeof AbortController !== "undefined" ? new AbortController() : null; const timer = controller ? setTimeout(function () { controller.abort(); }, timeoutMs || 15000) : null; try { const resp = await fetch(url.toString(), { method, credentials: "include", headers, body: body != null ? JSON.stringify(body) : undefined, signal: controller ? controller.signal : undefined, }); const newAuth = resp.headers.get("Authorization"); if (newAuth) localStorage.setItem("token", newAuth); const data = await resp.json().catch(() => ({})); if (!resp.ok) throw new Error(data.errorMessage || resp.statusText); if (data.success === false && data.errorCode && data.errorCode !== "200") { throw new Error(data.errorMessage || JSON.stringify(data)); } return data; } catch (e) { if (controller && controller.signal && controller.signal.aborted) { throw new Error("\u5e73\u53f0\u8bf7\u6c42\u8d85\u65f6"); } throw e; } finally { if (timer) clearTimeout(timer); } } function _gm(url, method, headers, data, timeoutMs) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: method || "GET", url, headers: headers || {}, data: data == null ? undefined : data, timeout: timeoutMs || 20000, onload(res) { resolve({ status: res.status, text: res.responseText || "" }); }, onerror: () => reject(new Error("GM \u7f51\u7edc\u8bf7\u6c42\u5931\u8d25")), ontimeout: () => reject(new Error("GM \u8bf7\u6c42\u8d85\u65f6")), }); }); } async function _cff(url, method, headers, data, timeoutMs) { const methodU = String(method || "GET").toUpperCase(); const controller = typeof AbortController !== "undefined" ? new AbortController() : null; const timer = controller ? setTimeout(function () { controller.abort(); }, timeoutMs || 20000) : null; try { const resp = await fetch(url, { method: methodU, headers: headers || {}, body: data == null || methodU === "GET" || methodU === "HEAD" ? undefined : data, signal: controller ? controller.signal : undefined, credentials: "omit", mode: "cors", }); return { status: resp.status, text: await resp.text(), via: "fetch" }; } catch (e) { if (controller && controller.signal && controller.signal.aborted) { throw new Error("fetch \u8d85\u65f6"); } throw e; } finally { if (timer) clearTimeout(timer); } } async function _ch(url, method, headers, data, timeoutMs) { const ms = Math.max(12000, Number(timeoutMs || 20000)); const perTry = Math.max(6000, Math.floor(ms / 2)); let host = "oa6.ahzsksw.cn"; try { host = new URL(url).host || host; } catch (_) {} try { return await _cff(url, method, headers, data, perTry); } catch (_) {} try { const res = await withTimeout( _gm(url, method, headers, data, perTry), perTry + 1500, "GM" ); return Object.assign({}, res, { via: "gm" }); } catch (_) {} throw new Error( "\u4e91\u7aef\u4e0d\u53ef\u8fbe\uff08" + host + "\uff09\u3002\u8bf7\u786e\u8ba4\uff1a① \u6d4f\u89c8\u5668\u80fd\u6253\u5f00 https://" + host + "/health \uff1b② Tampermonkey \u5df2\u5141\u8bb8\u811a\u672c\u8bbf\u95ee " + host ); } function parseCloudHttpResponse(res, path) { let data = {}; if (res.text) { try { data = JSON.parse(res.text); } catch (_) { if (res.status === 404) throw new Error("Not Found"); } } if (res.status < 200 || res.status >= 300) { throw new Error(translateCloudError(String(data.detail || data.message || data.error || "http_" + res.status))); } return data; } function translateCloudError(raw) { const s = String(raw || "").trim(); const map = { free_quota_exhausted: "\u514d\u8d39\u7ef4\u5ea6\u4f53\u9a8c\u5df2\u7528\u5b8c\uff082/2\uff09\uff0c\u8bf7\u5347\u7ea7 Pro", pro_required: "\u5b8c\u6574\u6d41\u7a0b\u9700 Pro Token", "invalid token": "Token \u65e0\u6548", revoked: "Token \u5df2\u7981\u7528", expired: "Token \u5df2\u8fc7\u671f", invalid_lease: "\u4e91\u7aef\u6388\u6743\u5931\u6548\uff0c\u8bf7\u91cd\u8bd5", session_not_found: "\u4e91\u7aef\u4f1a\u8bdd\u4e22\u5931\uff0c\u8bf7\u91cd\u65b0\u70b9\u300c\u5f00\u59cb\u300d", session_expired: "\u4e91\u7aef\u4f1a\u8bdd\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u70b9\u300c\u5f00\u59cb\u300d", upload_failed: "\u4f5c\u4e1a\u6587\u4ef6\u4e0a\u4f20\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u540e\u91cd\u8bd5", assignment_review_timeout: "\u4f5c\u4e1a1 AI \u8bc4\u9605\u8d85\u65f6\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5", platform_error: "\u5e73\u53f0\u8bf7\u6c42\u5931\u8d25\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u540e\u91cd\u8bd5", "Not Found": "\u4e91\u7aef API \u672a\u90e8\u7f72(404)\uff0c\u8bf7\u786e\u8ba4\u670d\u52a1\u7aef\u5df2\u542f\u52a8", "\u8bfe\u7a0b\u4e0d\u5b58\u5728": "\u5237\u8bfe\u53c2\u6570\u9519\u8bef(\u8bfe\u7a0b/\u65f6\u957f)\uff0c\u8bf7\u66f4\u65b0\u4e91\u7aef\u5f15\u64ce", }; if (/not found|http_404|^404$/i.test(s)) return "\u4e91\u7aef API \u672a\u90e8\u7f72(404)\uff0c\u8bf7\u786e\u8ba4\u670d\u52a1\u7aef\u5df2\u542f\u52a8"; for (const [k, v] of Object.entries(map)) { if (s.includes(k)) return v; } return s; } function getCloudApiBase() { return String(state.cloudApiBase || DEFAULT_CLOUD_API_BASE).replace(/\/$/, ""); } function formatExpireText(expSec, textFallback) { const ex = Number(expSec || 0); if (Number.isFinite(ex) && ex > 0) { const d = new Date(ex * 1000); if (!Number.isNaN(d.getTime())) { return d.toLocaleString("zh-CN", { hour12: false, timeZone: "Asia/Shanghai" }); } } const txt = String(textFallback || "").trim(); return txt || "—"; } function resolveProExpireSec(data) { const raw = data?.pro_expires_at ?? data?.proExpiresAt ?? 0; const n = Number(raw || 0); if (Number.isFinite(n) && n > 1e12) return Math.floor(n / 1000); return Number.isFinite(n) ? Math.floor(n) : 0; } function readCloudLeaseCache() { try { const parsed = JSON.parse(localStorage.getItem(CLOUD_LEASE_CACHE_KEY) || "null"); if (!parsed || typeof parsed !== "object") return null; const lease = String(parsed.lease || "").trim(); const exp = Number(parsed.exp || 0); if (!lease || !Number.isFinite(exp) || exp <= 0) return null; return { lease, exp, tier: String(parsed.tier || "").trim(), freePreLimit: Number(parsed.freePreLimit ?? parsed.free_pre_limit ?? 2), freeUsedPre: Number(parsed.freeUsedPre ?? parsed.free_used_pre ?? 0), proExpireAt: Number(parsed.proExpireAt ?? 0), proExpireText: String(parsed.proExpireText || "").trim(), }; } catch (_) { return null; } } function writeCloudLeaseCache(lease, exp, extra) { if (!lease || !exp) { localStorage.removeItem(CLOUD_LEASE_CACHE_KEY); return; } localStorage.setItem( CLOUD_LEASE_CACHE_KEY, JSON.stringify(Object.assign({ lease, exp: Number(exp || 0) }, extra || {})) ); } async function _cr(path, method, payload) { const url = getCloudApiBase() + (path.startsWith("/") ? path : "/" + path); const headers = { Accept: "application/json", "Content-Type": "application/json" }; const uid = getUserId(); if (uid) headers["x-learning-user-id"] = uid; if (state.cloudToken) headers.Authorization = "Bearer " + state.cloudToken; let body = payload; if (body && typeof body === "object" && path !== _w(4) && state.cloudLease && body.lease == null) { body = Object.assign({}, body, { lease: state.cloudLease }); } const res = await _ch( url, method || "POST", headers, body == null ? null : JSON.stringify(body), 25000 ); return parseCloudHttpResponse(res, path); } function _sq(data) { if (!data || typeof data !== "object") return; if (data.tier) state.cloudTier = String(data.tier); const lim = data.free_pre_limit ?? data.free_chapter_limit ?? data.freePreLimit; const used = data.free_used_pre ?? data.free_used_chapters ?? data.freeUsedPre; if (lim != null) state.freePreLimit = Number(lim); if (used != null) state.freeUsedPre = Number(used); const proExp = resolveProExpireSec(data); if (proExp > 0) state.cloudProExpireAt = proExp; if (data.pro_expires_at_text) state.cloudProExpireText = String(data.pro_expires_at_text).trim(); updateCloudPanelUI(); if (state.panelProject) renderProgressPanel(state.panelProject); } async function _clc(force) { const now = Math.floor(Date.now() / 1000); if (!force && state.cloudLease && state.cloudLeaseExp - now > 60) return true; if (!force) { const cached = readCloudLeaseCache(); if (cached && cached.exp - now > 60) { state.cloudLease = cached.lease; state.cloudLeaseExp = cached.exp; if (cached.tier) state.cloudTier = cached.tier; if (cached.freePreLimit > 0) state.freePreLimit = cached.freePreLimit; if (cached.freeUsedPre >= 0) state.freeUsedPre = cached.freeUsedPre; if (cached.proExpireAt > 0) state.cloudProExpireAt = cached.proExpireAt; if (cached.proExpireText) state.cloudProExpireText = cached.proExpireText; updateCloudPanelUI(); return true; } } const uid = getUserId() || (await withTimeout(_fp(), 15000, "\u83b7\u53d6\u7528\u6237\u4fe1\u606f"))?.learning_user_id; if (!uid) throw new Error("\u672a\u767b\u5f55\uff0c\u65e0\u6cd5\u83b7\u53d6\u4e91\u7aef\u6388\u6743"); const prof = (await withTimeout(_fp(), 15000, "\u83b7\u53d6\u7528\u6237\u4fe1\u606f")) || getUserProfile(); const leaseBody = Object.assign({ learning_user_id: uid }, prof || {}); const data = await withTimeout(_cr(_w(4), "POST", leaseBody), 30000, "\u4e91\u7aef\u6388\u6743"); state.cloudLease = String(data.lease || ""); state.cloudLeaseExp = Number(data.exp || 0); const leaseTier = String(data.tier || "").trim().toLowerCase(); if (leaseTier === "pro") { state.cloudTier = "pro"; } else if (state.cloudTokenVerified) { state.cloudTier = "pro"; } else { state.cloudTier = leaseTier || (state.cloudToken ? "pro" : "free"); } state.freePreLimit = Number(data.free_pre_limit ?? data.free_chapter_limit ?? 2); state.freeUsedPre = Number(data.free_used_pre ?? data.free_used_chapters ?? 0); state.cloudProExpireAt = resolveProExpireSec(data); state.cloudProExpireText = String(data.pro_expires_at_text || "").trim(); writeCloudLeaseCache(state.cloudLease, state.cloudLeaseExp, { tier: state.cloudTier, freePreLimit: state.freePreLimit, freeUsedPre: state.freeUsedPre, proExpireAt: state.cloudProExpireAt, proExpireText: state.cloudProExpireText, }); updateCloudPanelUI(); return !!state.cloudLease; } async function _cl(force) { if (cloudLeaseInflight) { if (!force) return cloudLeaseInflight; try { await withTimeout(cloudLeaseInflight, 32000, "\u7b49\u5f85\u4e0a\u6b21\u4e91\u7aef\u6388\u6743"); } catch (_) {} } const task = _clc(!!force); cloudLeaseInflight = task; try { return await task; } finally { if (cloudLeaseInflight === task) cloudLeaseInflight = null; } } async function _vt() { if (!state.cloudToken) { state.cloudTokenVerified = false; return true; } const uid = getUserId() || state.userProfile?.learning_user_id || ""; const url = getCloudApiBase() + _w(5); const headers = { Accept: "application/json", Authorization: "Bearer " + state.cloudToken }; if (uid) headers["x-learning-user-id"] = uid; const res = await _ch(url, "GET", headers, null, 20000); let data = {}; try { data = res.text ? JSON.parse(res.text) : {}; } catch (_) { throw new Error("Token \u6821\u9a8c\u54cd\u5e94\u5f02\u5e38"); } if (res.status < 200 || res.status >= 300 || !data.ok) { state.cloudTokenVerified = false; throw new Error(translateCloudError(String(data.message || "Token \u6821\u9a8c\u5931\u8d25"))); } state.cloudTokenVerified = true; if (data.tier) state.cloudTier = String(data.tier); updateCloudPanelUI(); return true; } async function refreshCloudAuth(force) { if (state.cloudToken) { await _vt(); } else { state.cloudTokenVerified = false; state.cloudTier = "free"; } await _cl(!!force); updateCloudPanelUI(); } async function _cc() { try { const url = getCloudApiBase() + _w(2); const res = await _ch(url, "GET", { Accept: "application/json" }, null, 12000); const data = res.text ? JSON.parse(res.text) : {}; if (data.cloudApiBase) state.cloudApiBase = String(data.cloudApiBase).replace(/\/$/, ""); if (data.proBuyUrl) state.proBuyUrl = String(data.proBuyUrl); if (data.panelNoticePath) state.panelNoticePath = String(data.panelNoticePath); if (data.freePreLimit != null) state.freePreLimit = Number(data.freePreLimit); } catch (_) {} try { const noticeUrl = getCloudApiBase() + state.panelNoticePath; const res = await _ch(noticeUrl, "GET", { Accept: "text/plain, */*" }, null, 12000); if (res.status >= 200 && res.status < 300 && String(res.text || "").trim()) { state.remotePanelNotice = String(res.text).trim(); } } catch (_) {} const ann = document.getElementById("aiedu-ann-text"); if (ann) ann.textContent = String(state.remotePanelNotice || PANEL_NOTICE_FALLBACK).trim() || PANEL_NOTICE_FALLBACK; } function appendLog(msg, kind) { const text = String(msg || "").trim(); if (!text) return; const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false }); state.logLines.push({ ts, msg: text, kind: kind || "info" }); if (state.logLines.length > 40) state.logLines.shift(); renderLog(); } function logStep(phase, step) { const text = String(step || "").trim(); if (!text) return; const kind = phase === "done" ? "done" : "doing"; const msg = text + (phase === "start" ? " \u5f00\u59cb" : phase === "done" ? " \u5b8c\u6210" : ""); const dedupeKey = kind + "|" + msg; if (state.lastProgressLogKey === dedupeKey) return; state.lastProgressLogKey = dedupeKey; const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false }); state.logLines.push({ ts, msg, kind }); if (state.logLines.length > 40) state.logLines.shift(); renderLog(); } function logError(msg) { const text = translateCloudError(String(msg || "").trim()); if (!text) return; const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false }); state.logLines.push({ ts, msg: text, kind: "err" }); if (state.logLines.length > 40) state.logLines.shift(); renderLog(); const st = document.getElementById("aiedu-auto-status"); if (st) { st.textContent = "\u5f02\u5e38"; st.style.color = "#dc2626"; } } function renderLog() { const box = document.getElementById("aiedu-run-log"); if (!box) return; const dotClass = { done: "cqrl-log-dot-done", doing: "cqrl-log-dot-doing", err: "cqrl-log-dot-err" }; const dotChar = { done: "✓", doing: "…", err: "!" }; box.innerHTML = state.logLines .map(function (row) { const k = row.kind || "info"; return ( '
' + (dotChar[k] || "·") + '' + esc(row.ts) + '' + esc(row.msg) + "
" ); }) .join(""); box.scrollTop = box.scrollHeight; } function buildDownloadHeaders(url) { const headers = { "User-Agent": navigator.userAgent }; try { const u = new URL(url); if (u.hostname.includes("chinabett.com")) headers.Referer = u.protocol + "//" + u.hostname + "/"; } catch (_) {} return headers; } function guessFilename(url, contentType, fallbackExt) { try { const p = decodeURIComponent(new URL(url).pathname.split("/").pop() || ""); if (p && /\.\w{2,5}$/i.test(p)) return p; } catch (_) {} return "file." + (fallbackExt || "bin"); } function parseGmContentType(responseHeaders) { if (!responseHeaders) return ""; const text = typeof responseHeaders === "string" ? responseHeaders : Array.isArray(responseHeaders) ? responseHeaders.join("\n") : ""; const m = text.match(/content-type:\s*([^\r\n]+)/i); return m && m[1] ? m[1].split(";")[0].trim() : ""; } function fetchUrlAsFile(url, fallbackExt) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, responseType: "arraybuffer", timeout: 600000, headers: buildDownloadHeaders(url), onload(res) { if (res.status < 200 || res.status >= 300) { reject(new Error("\u4e0b\u8f7d\u5931\u8d25 HTTP " + res.status)); return; } const mime = parseGmContentType(res.responseHeaders) || "application/octet-stream"; const name = guessFilename(url, mime, fallbackExt); resolve(new File([new Blob([res.response], { type: mime })], name, { type: mime })); }, onerror: () => reject(new Error("\u4e0b\u8f7d\u5931\u8d25")), ontimeout: () => reject(new Error("\u4e0b\u8f7d\u8d85\u65f6")), }); }); } function gmUpload(file) { return new Promise((resolve, reject) => { const fd = new FormData(); fd.append("file", file, file.name); fd.append("isConvert", "0"); fd.append("plat", "aiedu.cnve.com"); GM_xmlhttpRequest({ method: "POST", url: UPLOAD_URL, data: fd, onload(res) { try { const j = JSON.parse(res.responseText || "{}"); const url = j.FullPath || j.OrigFullPath || j.url; if (!url) reject(new Error("\u4e0a\u4f20\u65e0\u8fd4\u56de\u5730\u5740")); else resolve({ url, raw: j }); } catch (e) { reject(e); } }, onerror: () => reject(new Error("\u4e0a\u4f20\u5931\u8d25")), }); }); } async function resolvePresetFile(slot) { if (!state.presetFileCache) state.presetFileCache = {}; if (state.presetFileCache[slot]) return state.presetFileCache[slot]; const url = PRESET_URLS[slot]; if (!url) throw new Error("\u672a\u914d\u7f6e\u4f5c\u4e1a" + slot); const ext = slot === 1 ? "docx" : "mp4"; const file = await fetchUrlAsFile(url, ext); state.presetFileCache[slot] = file; return file; } function getReactFiber(el) { if (!el) return null; const k = Object.keys(el).find((x) => x.startsWith("__reactFiber$") || x.startsWith("__reactInternalInstance$")); return k ? el[k] : null; } function reactSetProgramTab(tabKey) { const tabValues = new Set(["intro", "assessment", "profile", "curriculum", "assignment", "exam", "certificate"]); const starts = [document.getElementById("root"), ...document.querySelectorAll('button[class*="stepperStep"]')].filter(Boolean); for (const start of starts) { let fiber = getReactFiber(start); for (let i = 0; i < 80 && fiber; i++, fiber = fiber.return) { let hook = fiber.memoizedState; while (hook) { const v = hook.memoizedState; if (typeof v === "string" && tabValues.has(v) && hook.queue?.dispatch) { if (v !== tabKey) hook.queue.dispatch(tabKey); return true; } hook = hook.next; } } } return false; } function findStepperBtn(labelRe) { return [...document.querySelectorAll('button[class*="stepperStep"]')].find((b) => labelRe.test((b.textContent || "").replace(/\s/g, "")) ); } function getBlueStepperCheckSrc() { for (const re of [/\u57f9\u8bad\u8bfe\u5355/, /\u4f5c\u4e1a\u63d0\u4ea4/, /\u80fd\u529b\u753b\u50cf/, /\u8bad\u524d\u6d4b\u8bc4/]) { const btn = findStepperBtn(re); const img = btn?.querySelector('img[class*="stepperIconImg"]'); if (img?.src) return img.src; } return document.querySelector('button[class*="stepperStep"] img[class*="stepperIconImg"]')?.src || ""; } function applyExamStepperVisual() { if (localStorage.getItem(PREFIX + "exam_stepper_done") !== "1") return; const btn = findStepperBtn(/\u7ed3\u4e1a\u8003\u8bd5/); if (!btn) return; const iconSpan = btn.querySelector('[class*="stepperIcon"]'); if (!iconSpan) return; iconSpan.classList.forEach((c) => { if (c.startsWith("notStarted_")) iconSpan.classList.remove(c); }); const blueSrc = getBlueStepperCheckSrc(); if (!blueSrc) return; iconSpan.querySelectorAll("img").forEach((i) => i.remove()); const img = document.createElement("img"); img.className = document.querySelector('img[class*="stepperIconImg"]')?.className || "stepperIconImg_31f41"; img.alt = ""; img.src = blueSrc; iconSpan.appendChild(img); document.body.classList.add("aiedu-exam-stepper-done"); } function installStepperHelpers() { if (window.__aieduStepperPtr) return; window.__aieduStepperPtr = true; GM_addStyle(` button[class*="stepperStep_"][disabled]:has(span), body.aiedu-exam-stepper-done button[class*="stepperStep_"] { cursor: pointer !important; } `); document.addEventListener( "pointerdown", (e) => { if (!location.pathname.includes("NewTeacherProgramDetail")) return; const hit = [...document.querySelectorAll('button[class*="stepperStep"]')].find((btn) => { const r = btn.getBoundingClientRect(); return e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom && /\u7ed3\u4e1a\u8003\u8bd5/.test(btn.textContent || ""); }); if (!hit) return; if (localStorage.getItem(PREFIX + "exam_stepper_done") !== "1" && !hit.disabled) return; e.preventDefault(); e.stopPropagation(); reactSetProgramTab("exam"); setTimeout(applyExamStepperVisual, 600); }, true ); } async function _xc(cmd) { const c = cmd || {}; const action = String(c.action || ""); if (action === "log") { logStep(String(c.phase || "start"), String(c.step || "")); return { lastResult: null }; } if (action === "sleep") { await sleep(Math.max(100, Number(c.ms || 500))); return { lastResult: null }; } if (action === "platform_fetch") { const opId = String(c.op_id || ""); try { const data = await api(String(c.method || "GET"), c.path, { params: c.params, body: c.body }); return { lastResult: { ok: true, op_id: opId, data } }; } catch (e) { return { lastResult: { ok: false, op_id: opId, error: String(e.message || e) } }; } } if (action === "upload_preset") { const slot = Number(c.slot || 1); const opId = String(c.op_id || "up_a" + slot); try { const file = await resolvePresetFile(slot); const { url, raw } = await gmUpload(file); const fileUrl = raw.OrigFullPath || url; return { lastResult: { ok: true, action: "upload_preset", op_id: opId, file_url: fileUrl, file_name: file.name, }, }; } catch (e) { return { lastResult: { ok: false, op_id: opId, error: String(e.message || e) } }; } } if (action === "ui") { const uiAction = String(c.ui_action || ""); if (uiAction === "switch_exam_tab") reactSetProgramTab("exam"); if (uiAction === "exam_stepper_done") { localStorage.setItem(PREFIX + "exam_stepper_done", "1"); applyExamStepperVisual(); } return { lastResult: null }; } if (action === "finish") { return { terminal: true, ok: !!c.ok, msg: String(c.message || "\u5b8c\u6210"), }; } return { lastResult: null }; } async function _es(kind, context) { await _cl(false); const data = await _cr(_w(0), "POST", { kind, context: context || {}, config: {}, }); _sq(data); return data; } async function _ep(sessionId, lastResult) { await _cl(false); const data = await _cr(_w(1), "POST", { session_id: sessionId, last_result: lastResult, }); _sq(data); return data; } async function _ve(startRes) { let sessionId = String(startRes.session_id || ""); let cmd = startRes.command; for (let i = 0; i < 12000 && cmd && !state.stopFlag; i++) { const out = await _xc(cmd); if (out.terminal !== undefined) return out; const lr = out.lastResult; if (lr && lr.ok === false) { const hint = lr.op_id ? "\uff08" + lr.op_id + "\uff09" : ""; throw new Error(String(lr.error || "platform_error") + hint); } const next = await _ep(sessionId, lr); sessionId = String(next.session_id || sessionId); cmd = next.command; } return { terminal: true, ok: false, msg: state.stopFlag ? "\u5df2\u505c\u6b62" : "\u5f15\u64ce\u6b65\u6570\u8d85\u9650" }; } function updateCloudPanelUI() { const tierEl = document.getElementById("aiedu-cloud-tier"); const freeEl = document.getElementById("aiedu-cloud-free"); const freeLabel = document.getElementById("aiedu-cloud-free-label"); const tier = String(state.cloudTier || "free").toLowerCase(); if (tierEl) { tierEl.textContent = tier === "pro" ? "Pro" : tier === "free" ? "\u514d\u8d39" : "\u672a\u6821\u9a8c"; tierEl.style.color = tier === "pro" ? "#0f766e" : "#64748b"; } if (freeLabel && freeEl) { if (tier === "pro") { freeLabel.textContent = "Pro \u5230\u671f"; freeEl.textContent = formatExpireText(state.cloudProExpireAt, state.cloudProExpireText); } else { freeLabel.textContent = "\u514d\u8d39\u4f53\u9a8c\uff08\u8bad\u524d\u6d4b\u8bc4 " + state.freePreLimit + " \u4e2a\u7ef4\u5ea6\uff09"; freeEl.textContent = state.freeUsedPre + "/" + state.freePreLimit + " \u7ef4\u5ea6"; } } } function syncStartStopButtons() { const start = document.getElementById("aiedu-start"); const stop = document.getElementById("aiedu-stop"); const on = state.running; if (start) { start.disabled = on; start.classList.toggle("cqrl-btn-off", on); } if (stop) { stop.disabled = !on; stop.classList.toggle("cqrl-btn-off", !on); } const status = document.getElementById("aiedu-auto-status"); if (status && !on) { status.textContent = "\u5df2\u505c\u6b62"; status.style.color = "#64748b"; } else if (status && on) { status.textContent = "\u8fd0\u884c\u4e2d"; status.style.color = "#15803d"; } } function isTrainingFullyComplete() { return ["assessment", "profile", "curriculum", "assignment", "exam"].every(function (key) { return stepCompletion(key).done; }); } function openCertApplyPage() { window.open(CERT_APPLY_URL, "_blank", "noopener"); } function showCertCompleteBanner(force) { const banner = document.getElementById("aiedu-cert-banner"); if (!banner) return; if (force || (isTrainingFullyComplete() && !state.certBannerHidden)) { banner.classList.add("show"); } } function hideCertCompleteBanner() { const banner = document.getElementById("aiedu-cert-banner"); if (banner) banner.classList.remove("show"); state.certBannerHidden = true; } function syncActionButtons() { syncStartStopButtons(); const cert = document.getElementById("aiedu-cert-apply"); const ready = isTrainingFullyComplete(); if (cert) { cert.disabled = !ready; cert.classList.toggle("cqrl-btn-off", !ready); cert.title = ready ? "\u524d\u5f80\u8ba4\u8bc1\u5e73\u53f0\u7533\u8bf7\u7ed3\u4e1a\u8bc1\u4e66" : "\u8bf7\u5148\u5b8c\u6210\u8bad\u524d\u6d4b\u8bc4\u3001\u753b\u50cf\u3001\u8bfe\u5355\u3001\u4f5c\u4e1a\u3001\u7ed3\u4e1a\u8003\u8bd5"; } if (ready && !state.certBannerHidden) showCertCompleteBanner(false); } async function startFlow() { if (state.running) return; if (!isLoggedIn()) { logError("\u8bf7\u5148\u767b\u5f55 aiedu.cnve.com"); return; } state.running = true; state.stopFlag = false; state.lastProgressLogKey = ""; syncActionButtons(); try { await _cl(false); if (state.cloudToken) await _vt(); const tier = String(state.cloudTier || "free").toLowerCase(); if (tier === "free" && Number(state.freeUsedPre) >= Number(state.freePreLimit)) { logError("\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff08" + state.freeUsedPre + "/" + state.freePreLimit + "\uff09"); return; } const kind = tier === "pro" ? "pro_full" : "free_pre"; const context = { user_id: getUserId() || state.userProfile?.learning_user_id || "" }; const startRes = await _es(kind, context); const result = await _ve(startRes); if (result.ok) { logStep("done", result.msg || (kind === "pro_full" ? "\u5168\u6d41\u7a0b" : "\u514d\u8d39\u4f53\u9a8c")); await loadPanelProgress(); if (kind === "pro_full") { state.certBannerHidden = false; showCertCompleteBanner(true); } } else if (result.msg) logError(result.msg); } catch (e) { logError(String(e.message || e)); } finally { state.running = false; syncActionButtons(); } } function stopFlow() { state.stopFlag = true; state.running = false; syncActionButtons(); } function openProBuyPage() { window.open(state.proBuyUrl || PRO_BUY_URL, "_blank", "noopener"); } function stepStatusBadge(done, partial) { if (done) return '\u5df2\u5b8c\u6210'; if (partial) return '\u8fdb\u884c\u4e2d'; return '\u672a\u5b8c\u6210'; } function courseTypeTag(type, name) { const isMust = Number(type) === 1; return ( '' + esc(name || (isMust ? "\u5fc5\u4fee" : "\u9009\u4fee")) + "" ); } function portraitStatusCode() { return Number( state.panelPortraitStatus ?? state.panelPortrait?.status ?? state.panelPortrait?.portraitStatus ?? 0 ); } function isPortraitDone(project) { const st = portraitStatusCode(); if (st === 3) return true; const p = state.panelPortrait; if (p) { if (p.developmentSuggestions) return true; if (Array.isArray(p.requiredCourses) && p.requiredCourses.length > 0) return true; if (p.portraitUrl || p.reportUrl) return true; } const node = Number(project?.currentNode || state.panelProject?.currentNode || 0); if (node >= 4) return true; if (node >= 3 && (state.panelCourses || []).length > 0) return true; return false; } function renderIntroBody(project) { const summary = String(project.summary || "").trim(); const time = (project.startTime || "") + (project.endTime ? " ~ " + project.endTime : ""); return ( '
\u57f9\u8bad\u5468\u671f' + esc(time || "—") + '
' + esc(summary || "\u6682\u65e0\u9879\u76ee\u4ecb\u7ecd") + "
" ); } function renderDimsBody(dims) { const rows = (dims || []).filter((d) => d.isRequired !== false); if (!rows.length) return '
\u6682\u65e0\u6d4b\u8bc4\u7ef4\u5ea6
'; return rows .map(function (d) { const done = !!d.isSubmitted; const score = done && d.score != null && d.score !== "" ? ' ' + Number(d.score).toFixed(0) + "\u5206" : ""; return ( '
' + esc(d.dimensionName || "\u7ef4\u5ea6") + "" + (done ? "\u5df2\u4ea4\u5377" + score : "\u672a\u4ea4\u5377") + "
" ); }) .join(""); } function renderPortraitBody(project) { const done = isPortraitDone(project); const st = portraitStatusCode(); if (!done && !state.panelPortrait && !st) { return '
\u5c1a\u672a\u751f\u6210\u80fd\u529b\u753b\u50cf
'; } let html = '
\u72b6\u6001' + (done ? "\u5df2\u751f\u6210" : st === 2 ? "\u751f\u6210\u4e2d" : st === 1 ? "\u6392\u961f\u4e2d" : "\u672a\u751f\u6210") + "
"; const p = state.panelPortrait; if (p?.developmentSuggestions) { html += '
' + esc(String(p.developmentSuggestions).slice(0, 160)) + "…
"; } else if (done && Array.isArray(p?.requiredCourses) && p.requiredCourses.length) { html += '
\u5df2\u751f\u6210\u8bfe\u5355\u63a8\u8350\uff08' + p.requiredCourses.length + " \u95e8\u5fc5\u4fee\u8bfe\uff09
"; } return html; } const TARGET_STUDY_HOURS = 30; function courseIsFinished(c) { return !!c.isFinished || Number(c.studyProgress) >= 100; } function courseStudiedHours(c) { const hours = Number(c.creditHours || 0); if (!hours) return 0; if (courseIsFinished(c)) return hours; return (hours * Math.min(100, Number(c.studyProgress || 0))) / 100; } function renderCoursesBody() { const list = state.panelCourses || []; if (!list.length) return '
\u6682\u65e0\u8bfe\u7a0b
'; const req = list.filter((c) => Number(c.courseType) === 1); const opt = list.filter((c) => Number(c.courseType) !== 1); const reqDone = req.filter(courseIsFinished).length; const optDone = opt.filter(courseIsFinished).length; const studiedHours = list.reduce((sum, c) => sum + courseStudiedHours(c), 0); let html = '
\u5b66\u65f6' + studiedHours.toFixed(1) + "/" + TARGET_STUDY_HOURS + "\uff08\u5fc5\u4fee+\u9009\u4fee\u5747\u9700\u5b8c\u6210\uff09
"; html += '
\u5fc5\u4fee' + reqDone + "/" + (req.length || list.length) + " \u95e8\u5df2\u5b8c\u6210
"; if (opt.length) { html += '
\u9009\u4fee' + optDone + "/" + opt.length + " \u95e8\u5df2\u5b8c\u6210
"; } html += list .slice() .sort(function (a, b) { const ta = Number(a.courseType) === 1 ? 0 : 1; const tb = Number(b.courseType) === 1 ? 0 : 1; return ta - tb || Number(a.courseId) - Number(b.courseId); }) .map(function (c) { const pct = Number(c.studyProgress || 0); const finished = courseIsFinished(c); return ( '
' + courseTypeTag(c.courseType, c.courseTypeName) + '' + esc(c.courseName) + '' + Number(c.creditHours || 0).toFixed(1) + "\u5b66\u65f6 · " + pct.toFixed(0) + "%
" ); }) .join(""); return html; } function renderAssignmentsBody() { const all = state.panelAssignments || []; const list = all .slice() .sort((a, b) => Number(a.assignmentId) - Number(b.assignmentId)) .filter((a, i) => i < 2); if (!list.length) return '
\u6682\u65e0\u5fc5\u4fee\u4f5c\u4e1a
'; let html = list .map(function (a, i) { const done = !!a.isSubmitted; return ( '
\u4f5c\u4e1a' + (i + 1) + " · " + esc(a.title || "\u4f5c\u4e1a") + '' + (done ? "\u5df2\u63d0\u4ea4" : "\u672a\u63d0\u4ea4") + "
" ); }) .join(""); html += '
\u4f5c\u4e1a3 \u9ed8\u8ba4\u8df3\u8fc7
'; return html; } function stepCompletion(key) { if (key === "intro") return { done: true, partial: false }; if (key === "assessment") { const dims = (state.panelPreDims || []).filter((d) => d.isRequired !== false); const done = dims.length && dims.every((d) => d.isSubmitted); const partial = dims.some((d) => d.isSubmitted); return { done, partial: partial && !done }; } if (key === "profile") { const done = isPortraitDone(state.panelProject); const st = portraitStatusCode(); return { done, partial: !done && (st === 1 || st === 2) }; } if (key === "curriculum") { const list = state.panelCourses || []; const done = list.length && list.every(courseIsFinished); const partial = list.some((c) => Number(c.studyProgress) > 0); return { done, partial: partial && !done }; } if (key === "assignment") { const list = (state.panelAssignments || []) .slice() .sort((a, b) => Number(a.assignmentId) - Number(b.assignmentId)) .slice(0, 2); const done = list.length && list.every((a) => a.isSubmitted); const partial = list.some((a) => a.isSubmitted); return { done, partial: partial && !done }; } if (key === "exam") { const dims = (state.panelExamDims || []).filter((d) => d.isRequired !== false); const done = dims.length && dims.every((d) => d.isSubmitted); const partial = dims.some((d) => d.isSubmitted); return { done, partial: partial && !done }; } return { done: false, partial: false }; } function renderStepBody(key, project) { if (key === "intro") return renderIntroBody(project); if (key === "assessment") return renderDimsBody(state.panelPreDims); if (key === "profile") return renderPortraitBody(project); if (key === "curriculum") return renderCoursesBody(); if (key === "assignment") return renderAssignmentsBody(); if (key === "exam") return renderDimsBody(state.panelExamDims); return ""; } function renderStepDetail(project) { const box = document.getElementById("aiedu-step-detail"); if (!box) return; const key = state.panelOpenStep; if (!key || !project) { box.innerHTML = '
\u70b9\u51fb\u73af\u8282\u67e5\u770b\u8be6\u60c5
'; return; } const step = PROGRESS_STEPS.find((s) => s.key === key); const tier = String(state.cloudTier || "free").toLowerCase(); const locked = step?.proOnly && tier !== "pro"; const title = step ? step.label : key; let html = '
' + esc(title) + (locked ? ' \u81ea\u52a8\u5316\u9700Pro' : "") + "
"; html += renderStepBody(key, project); if (locked) { html += '
\u514d\u8d39\u7528\u6237\u53ef\u67e5\u770b\u8fdb\u5ea6\uff1b\u81ea\u52a8\u5316\u4ec5\u652f\u6301\u8bad\u524d\u6d4b\u8bc4\u524d ' + state.freePreLimit + " \u4e2a\u4f5c\u4e1a
"; } box.innerHTML = html; } function renderProgressPanel(project) { const head = document.getElementById("aiedu-project-head"); const list = document.getElementById("aiedu-step-list"); if (!head || !list) return; if (!project) { head.innerHTML = '
\u672a\u627e\u5230\u57f9\u8bad\u9879\u76ee
'; list.innerHTML = ""; renderStepDetail(null); syncActionButtons(); return; } const time = (project.startTime || "") + (project.endTime ? " ~ " + project.endTime : ""); head.innerHTML = '
' + esc(project.projectName) + '
\u9879\u76eeID ' + esc(project.projectId) + " · \u5f53\u524d\u8282\u70b9 " + esc(project.currentNode || "—") + (time ? " · " + esc(time) : "") + "
"; const tier = String(state.cloudTier || "free").toLowerCase(); list.innerHTML = PROGRESS_STEPS.map(function (step) { const comp = stepCompletion(step.key); const locked = step.proOnly && tier !== "pro"; const active = state.panelOpenStep === step.key; if (step.static) { return ( '
' + '
' + '' + esc(step.label) + "" + stepStatusBadge(comp.done, comp.partial) + "
" ); } return ( '
' + '
' + '' + esc(step.label) + "" + (locked ? 'Pro' : "") + stepStatusBadge(comp.done, comp.partial) + "
" ); }).join(""); list.querySelectorAll(".aiedu-step-head:not(.static)").forEach(function (el) { el.addEventListener("click", function () { const key = el.closest(".aiedu-step-item")?.getAttribute("data-step"); state.panelOpenStep = state.panelOpenStep === key ? "" : key; renderProgressPanel(state.panelProject); }); }); renderStepDetail(project); syncActionButtons(); } async function loadPanelProgress() { if (!isLoggedIn() || state.panelProgressBusy) return; state.panelProgressBusy = true; const head = document.getElementById("aiedu-project-head"); if (head) head.innerHTML = '
\u52a0\u8f7d\u4e2d…
'; try { const projRes = await api("GET", TEACHER_API + "/training/student/project/list"); const project = (projRes.data || [])[0] || null; state.panelProject = project; if (!project) { renderProgressPanel(null); return; } const pid = String(project.projectId); const results = await Promise.all([ api("GET", TEACHER_API + "/training/assessment/dimensions", { params: { projectId: pid, configType: 1 }, }).catch(() => ({ data: [] })), api("GET", TEACHER_API + "/training/assessment/dimensions", { params: { projectId: pid, configType: 2 }, }).catch(() => ({ data: [] })), api("GET", TEACHER_API + "/training/course/list", { params: { projectId: pid } }).catch(() => ({ data: [] })), api("GET", TEACHER_API + "/training/assignment/list", { params: { projectId: pid } }).catch(() => ({ data: [] })), api("GET", TEACHER_API + "/training/portrait/detail", { params: { projectId: pid } }).catch(() => ({ data: null })), api("GET", TEACHER_API + "/training/portrait/generate/status", { params: { projectId: pid, portraitType: 1 }, }).catch(() => ({ data: null })), ]); state.panelPreDims = results[0].data || []; state.panelExamDims = results[1].data || []; state.panelCourses = results[2].data || []; state.panelAssignments = results[3].data || []; state.panelPortrait = results[4].data || null; state.panelPortraitStatus = results[5].data?.status ?? results[5].data?.portraitStatus ?? null; renderProgressPanel(project); } catch (e) { if (head) head.innerHTML = '
' + esc(e.message || e) + "
"; } finally { state.panelProgressBusy = false; } } function switchPanelTab(tab) { const name = String(tab || "project").trim() || "project"; state.panelTab = name; localStorage.setItem(PANEL_TAB_KEY, name); document.querySelectorAll(".aiedu-tab-btn").forEach(function (btn) { const on = btn.getAttribute("data-tab") === name; btn.classList.toggle("active", on); }); document.querySelectorAll(".aiedu-tab-pane").forEach(function (pane) { pane.classList.toggle("active", pane.id === "aiedu-tab-" + name); }); } function readPanelTab() { const v = String(localStorage.getItem(PANEL_TAB_KEY) || "project").trim(); return v === "log" || v === "auth" ? v : "project"; } function injectPanelStyles() { document.querySelectorAll('[id^="aiedu-panel-style-v"]').forEach(function (el) { el.remove(); }); const id = "aiedu-panel-style-v8"; if (document.getElementById(id) || !document.head) return; const st = document.createElement("style"); st.id = id; st.textContent = ` #aiedu-auto-panel{position:fixed;right:20px;top:80px;z-index:999999;width:380px;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:visible;} #aiedu-auto-panel.aiedu-panel-min #aiedu-panel-body{display:none!important;} #aiedu-panel-header{padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:center;cursor:move;user-select:none;border-radius:16px 16px 0 0;} #aiedu-panel-brand{display:flex;align-items:center;gap:9px;min-width:0;flex:1;} #aiedu-panel-logo{width:30px;height:30px;border-radius:9px;object-fit:cover;border:1px solid rgba(148,163,184,.45);background:#fff;} #aiedu-panel-title{font-size:13px;font-weight:900;color:#9a3412;line-height:1.26;} #aiedu-panel-sub{margin-top:3px;font-size:11px;color:#7c2d12;} .aiedu-panel-version{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0;margin-left:5px;} .aiedu-panel-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);} #aiedu-panel-body{padding:8px;} .aiedu-status-bar{display:flex;justify-content:space-between;align-items:center;gap:7px;padding:2px 2px 6px;font-size:11px;} .aiedu-status-bar .cqrl-status-label{color:#64748b;font-weight:700;} #aiedu-panel-tabs{display:flex;gap:5px;margin-bottom:6px;} .aiedu-tab-btn{flex:1;border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:6px 4px;border-radius:8px;font-size:11px;font-weight:800;cursor:pointer;line-height:1.2;} .aiedu-tab-btn.active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8;box-shadow:inset 0 0 0 1px #bfdbfe;} .aiedu-action-card{margin:6px 0;padding:6px 8px;} #aiedu-panel-tab-body{margin-bottom:6px;} .aiedu-tab-pane{display:none;background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;} .aiedu-tab-pane.active{display:block;} #aiedu-panel-notice{padding:6px 10px;background:linear-gradient(180deg,#fffdf5,#fff7e6);border:1px solid #fcd34d;border-radius:10px;max-height:54px;overflow-y:auto;margin-top:2px;} #aiedu-ann-text{display:block;font-size:11px;color:#92400e;line-height:1.42;word-break:break-word;} .cqrl-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:7px 9px;margin-bottom:7px;} .cqrl-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px;} .cqrl-status-label{font-size:11px;color:#64748b;font-weight:700;} #aiedu-auto-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1;} .cqrl-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:5px;} .cqrl-meta-label{color:#64748b;}.cqrl-meta-value{font-weight:700;text-align:right;word-break:break-all;} .cqrl-token-input{width:100%;border:1px solid #cbd5e1;border-radius:6px;padding:4px 6px;font-size:11px;box-sizing:border-box;} .cqrl-btn-row{display:flex;gap:5px;flex-wrap:nowrap;} .cqrl-btn{flex:1;min-width:0;border:none;color:#fff;padding:7px 6px;border-radius:10px;cursor:pointer;font-weight:800;font-size:11px;line-height:1.2;} .cqrl-btn-start{background:#16a34a;}.cqrl-btn-stop{background:#ef4444;} .cqrl-btn-cert{background:#0f766e;} .cqrl-btn-off,.cqrl-btn:disabled{background:#cbd5e1!important;color:#64748b!important;cursor:not-allowed;pointer-events:none;} .cqrl-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a;} .aiedu-cert-banner{display:none;margin-bottom:7px;padding:8px 10px;background:linear-gradient(180deg,#ecfdf5,#d1fae5);border:1px solid #6ee7b7;border-radius:10px;} .aiedu-cert-banner.show{display:block;} .aiedu-cert-banner-head{display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:4px;} .aiedu-cert-banner-title{font-size:12px;font-weight:900;color:#065f46;} .aiedu-cert-banner-close{border:none;background:transparent;color:#64748b;font-size:16px;line-height:1;cursor:pointer;padding:0 2px;} .aiedu-cert-banner-text{font-size:11px;color:#047857;line-height:1.45;margin-bottom:7px;word-break:break-word;} .aiedu-cert-banner-actions{display:flex;gap:6px;} .cqrl-btn-banner{flex:1;font-size:11px;padding:6px 8px;} #aiedu-run-log{height:104px;max-height:104px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:5px;} .cqrl-log-row{display:flex;align-items:flex-start;gap:6px;padding:4px 6px;border-bottom:1px dashed #d4deea;font-size:12px;line-height:1.42;} .cqrl-log-dot{flex:0 0 auto;width:14px;text-align:center;font-weight:900;} .cqrl-log-dot-done{color:#16a34a;}.cqrl-log-dot-doing{color:#2563eb;}.cqrl-log-dot-err{color:#dc2626;} .cqrl-log-time{flex:0 0 auto;color:#94a3b8;font-size:11px;min-width:52px;} .cqrl-log-text{flex:1;min-width:0;color:#334155;word-break:break-word;} .cqrl-list-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;} .cqrl-list-title{font-size:12px;color:#64748b;font-weight:700;} .cqrl-log-clear-btn{border:1px solid #cbd5e1;background:#fff;color:#64748b;padding:1px 7px;border-radius:999px;font-size:10px;cursor:pointer;} .aiedu-project-name{font-size:12px;font-weight:800;color:#0f172a;line-height:1.35;margin-bottom:3px;} .aiedu-project-meta{font-size:11px;color:#64748b;} .aiedu-empty{padding:8px 4px;text-align:center;color:#94a3b8;font-size:11px;} .aiedu-step-item{border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;background:#fff;margin:0;} #aiedu-step-list{display:grid;grid-template-columns:1fr 1fr;gap:5px;} .aiedu-step-static{grid-column:1/-1;} .aiedu-step-item[data-step="exam"]{grid-column:1/-1;} .aiedu-step-head{display:flex;align-items:center;gap:4px;padding:5px 7px;cursor:pointer;background:#f8fafc;user-select:none;min-height:30px;} .aiedu-step-head.static{cursor:default;background:#fff;} .aiedu-step-item.active .aiedu-step-head{border-color:#93c5fd;background:#eff6ff;box-shadow:inset 0 0 0 1px #93c5fd;} .aiedu-step-head.locked{background:#fafafa;} .aiedu-step-label{flex:1;min-width:0;font-weight:700;font-size:11px;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} .aiedu-step-badge{flex:0 0 auto;font-size:9px;padding:1px 5px;border-radius:999px;font-weight:700;} .aiedu-done{background:#dcfce7;color:#166534;}.aiedu-doing{background:#dbeafe;color:#1d4ed8;}.aiedu-pending{background:#f1f5f9;color:#64748b;} .aiedu-lock{flex:0 0 auto;font-size:9px;color:#b45309;background:#ffedd5;border:1px solid #fdba74;border-radius:999px;padding:0 4px;} #aiedu-step-detail{margin-top:6px;max-height:130px;overflow-y:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:8px;padding:6px 8px;font-size:11px;} .aiedu-step-detail-placeholder{color:#94a3b8;text-align:center;padding:18px 6px;} .aiedu-step-detail-title{font-weight:800;color:#1d4ed8;margin-bottom:5px;font-size:11px;} .aiedu-step-line{display:flex;justify-content:space-between;gap:8px;margin-bottom:4px;color:#475569;} .aiedu-step-desc{color:#64748b;line-height:1.45;margin-top:4px;word-break:break-word;} .aiedu-muted{color:#94a3b8;font-size:10px;} .aiedu-dim-row{display:flex;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px dashed #eef2f7;} .aiedu-dim-done span:last-child{color:#16a34a;font-weight:700;} .aiedu-score{font-style:normal;color:#0369a1;margin-left:3px;} .aiedu-course-row{display:flex;align-items:flex-start;gap:5px;padding:4px 0;border-bottom:1px dashed #eef2f7;flex-wrap:wrap;} .aiedu-course-name{flex:1;min-width:0;line-height:1.35;color:#334155;} .aiedu-course-meta{flex:0 0 auto;color:#0369a1;font-weight:700;font-size:10px;} .aiedu-course-done .aiedu-course-name{color:#16a34a;} .aiedu-type-tag{flex:0 0 auto;font-size:10px;font-weight:800;border-radius:999px;padding:0 5px;line-height:1.5;} .aiedu-type-must{color:#1d4ed8;background:#dbeafe;border:1px solid #93c5fd;} .aiedu-type-select{color:#b45309;background:#ffedd5;border:1px solid #fdba74;} `; document.head.appendChild(st); } function readPanelPos() { try { return JSON.parse(localStorage.getItem(PANEL_POS_KEY) || "null"); } catch (_) { return null; } } function writePanelPos(left, top) { localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left, top })); } function readPanelCollapsed() { return localStorage.getItem(PANEL_COLLAPSED_KEY) === "1"; } function writePanelCollapsed(v) { localStorage.setItem(PANEL_COLLAPSED_KEY, v ? "1" : "0"); } function applyPanelCollapsed(panel, collapsed) { panel.classList.toggle("aiedu-panel-min", !!collapsed); panel.classList.toggle("aiedu-panel-max", !collapsed); const btnMin = panel.querySelector("#aiedu-btn-min"); const btnMax = panel.querySelector("#aiedu-btn-max"); if (btnMin) btnMin.style.display = collapsed ? "none" : ""; if (btnMax) btnMax.style.display = collapsed ? "" : "none"; } function enablePanelDrag(panel) { const header = panel.querySelector("#aiedu-panel-header"); if (!header) return; let dragging = false; let sx = 0; let sy = 0; let sl = 0; let st = 0; header.addEventListener("mousedown", function (e) { if (e.target?.closest(".aiedu-panel-ctl")) return; dragging = true; sx = e.clientX; sy = e.clientY; const rect = panel.getBoundingClientRect(); sl = rect.left; st = rect.top; panel.style.right = "auto"; e.preventDefault(); }); document.addEventListener("mousemove", function (e) { if (!dragging) return; panel.style.left = Math.max(0, sl + e.clientX - sx) + "px"; panel.style.top = Math.max(0, st + e.clientY - sy) + "px"; }); document.addEventListener("mouseup", function () { if (!dragging) return; dragging = false; const rect = panel.getBoundingClientRect(); writePanelPos(rect.left, rect.top); }); } function createPanel() { if (document.getElementById("aiedu-auto-panel")) return; injectPanelStyles(); const panel = document.createElement("div"); panel.id = "aiedu-auto-panel"; panel.className = "aiedu-panel-max"; panel.innerHTML = '
' + '
' + '' + '
\u5168\u6c11AI\u667a\u6167\u6559\u80b2\u57f9\u8bad\u52a9\u624bv' + SCRIPT_VERSION + '
' + '
\u8bad\u524d\u6d4b\u8bc4 · \u57f9\u8bad\u8bfe\u5355 · \u4f5c\u4e1a\u63d0\u4ea4· \u7ed3\u4e1a\u8003\u8bd5
' + '
' + '
' + '
' + '
\u8fd0\u884c\u72b6\u6001\u5df2\u505c\u6b62
' + '
' + '' + '' + '
' + '
' + '
' + '
\u57f9\u8bad\u8fdb\u5ea6' + '
' + '
\u767b\u5f55\u540e\u52a0\u8f7d…
' + '
' + '
\u70b9\u51fb\u73af\u8282\u67e5\u770b\u8be6\u60c5
' + '
' + '
\u6d41\u7a0b\u65e5\u5fd7' + '
' + '
' + '
' + '
\u7528\u6237\u7c7b\u578b\u672a\u6821\u9a8c
' + '
\u514d\u8d39\u4f53\u9a8c0/2
' + '
Token' + '
' + '
' + '
' + '
' + '
' + '
\u5168\u6d41\u7a0b\u5df2\u5b8c\u6210' + '
' + '
\u57f9\u8bad\u73af\u8282\u5df2\u5168\u90e8\u5b8c\u6210\uff0c\u8bf7\u524d\u5f80\u8ba4\u8bc1\u5e73\u53f0\u7533\u8bf7\u7ed3\u4e1a\u8bc1\u4e66\u3002
' + '
' + '
' + '
' + '' + '' + '
' + '
' + esc(PANEL_NOTICE_FALLBACK) + "
"; document.body.appendChild(panel); const savedPos = readPanelPos(); if (savedPos && savedPos.left != null) { panel.style.right = "auto"; panel.style.left = savedPos.left + "px"; panel.style.top = savedPos.top + "px"; } applyPanelCollapsed(panel, readPanelCollapsed()); switchPanelTab(readPanelTab()); document.querySelectorAll(".aiedu-tab-btn").forEach(function (btn) { btn.addEventListener("click", function () { switchPanelTab(btn.getAttribute("data-tab")); }); }); document.getElementById("aiedu-btn-min")?.addEventListener("click", function (e) { e.stopPropagation(); writePanelCollapsed(true); applyPanelCollapsed(panel, true); }); document.getElementById("aiedu-btn-max")?.addEventListener("click", function (e) { e.stopPropagation(); writePanelCollapsed(false); applyPanelCollapsed(panel, false); }); document.getElementById("aiedu-clear-log")?.addEventListener("click", function () { state.logLines = []; renderLog(); }); document.getElementById("aiedu-refresh-progress")?.addEventListener("click", function () { void loadPanelProgress(); }); document.getElementById("aiedu-start")?.addEventListener("click", startFlow); document.getElementById("aiedu-stop")?.addEventListener("click", stopFlow); document.getElementById("aiedu-cert-apply")?.addEventListener("click", openCertApplyPage); document.getElementById("aiedu-cert-banner-go")?.addEventListener("click", openCertApplyPage); document.getElementById("aiedu-cert-banner-close")?.addEventListener("click", hideCertCompleteBanner); document.getElementById("aiedu-pro-buy")?.addEventListener("click", openProBuyPage); document.getElementById("aiedu-cloud-save")?.addEventListener("click", async function () { if (state.cloudSaving) return; const btn = this; const input = document.getElementById("aiedu-cloud-token"); const origText = btn.textContent; state.cloudSaving = true; state.cloudToken = String((input && input.value) || "").trim(); localStorage.setItem(CLOUD_TOKEN_KEY, state.cloudToken); writeCloudLeaseCache("", 0); state.cloudLease = ""; state.cloudLeaseExp = 0; state.cloudProExpireAt = 0; state.cloudProExpireText = ""; state.cloudTokenVerified = false; if (!state.cloudToken) state.cloudTier = "free"; btn.disabled = true; btn.textContent = "\u6821\u9a8c\u4e2d…"; state.lastProgressLogKey = ""; logStep("start", "\u4e91\u7aef\u6821\u9a8c"); updateCloudPanelUI(); const resetBtn = function () { btn.disabled = false; btn.textContent = origText; state.cloudSaving = false; updateCloudPanelUI(); }; const safetyTimer = setTimeout(resetBtn, 35000); try { await withTimeout(refreshCloudAuth(true), 32000, "\u4e91\u7aef\u6821\u9a8c"); const tier = String(state.cloudTier || "free").toLowerCase(); logStep("done", "\u4e91\u7aef\u6821\u9a8c"); appendLog( tier === "pro" ? "Pro \u5df2\u751f\u6548\uff0c\u5230\u671f " + formatExpireText(state.cloudProExpireAt, state.cloudProExpireText) : "\u514d\u8d39\u6a21\u5f0f\uff0c\u989d\u5ea6 " + state.freeUsedPre + "/" + state.freePreLimit + " \u4f5c\u4e1a", "done" ); } catch (e) { logError("\u4e91\u7aef\u6821\u9a8c\u5931\u8d25\uff1a" + (e.message || e)); } finally { clearTimeout(safetyTimer); resetBtn(); } }); const tokenInput = document.getElementById("aiedu-cloud-token"); if (tokenInput) tokenInput.value = state.cloudToken; updateCloudPanelUI(); enablePanelDrag(panel); syncActionButtons(); if (isLoggedIn()) void loadPanelProgress(); } function init() { installStepperHelpers(); createPanel(); void _cc(); if (isLoggedIn()) { getUserId(); void _fp(); refreshCloudAuth(false).catch(function (e) { logError("\u4e91\u7aef\u6388\u6743\uff1a" + (e.message || e)); }); } setInterval(function () { if (isLoggedIn() && !state.panelProgressBusy && !state.running) void loadPanelProgress(); }, 60000); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();