(样式见 assets/panel.css)。
*/
function box() {
const root = import_lib.$elements.root;
if (!root) return null;
return root.querySelector("container-element");
}
function clamp() {
const el = box();
if (!el) return;
const r = el.getBoundingClientRect();
if (!r.width || !r.height) return;
const maxX = Math.max(0, document.documentElement.clientWidth - r.width);
const maxY = Math.max(0, document.documentElement.clientHeight - r.height);
const x = Math.min(Math.max(0, el.offsetLeft), maxX);
const y = Math.min(Math.max(0, el.offsetTop), maxY);
if (x !== el.offsetLeft) el.style.left = x + "px";
if (y !== el.offsetTop) el.style.top = y + "px";
}
/**
* 自检。
*
* 面板挂在 **closed** Shadow DOM 里,页面控制台和 DevTools 的 Elements 搜索都进不去
* (wrapper.shadowRoot 恒为 null),只有脚本自己能看。所以把探针挂到页面 window 上,
* 出问题时在控制台敲 __ykt() 就能拿到面板的真实状态。
*/
function report() {
const w = import_lib.$elements.wrapper;
const el = box();
const cs = el ? getComputedStyle(el) : null;
const styleTags = el ? Array.from(el.querySelectorAll("style")) : [];
const body = el ? el.querySelector(".body") : null;
const hs = w ? getComputedStyle(w) : null;
return {
宿主存在: !!w,
宿主在DOM里: !!w && document.body.contains(w),
宿主display: hs ? hs.display : null,
宿主visibility: hs ? hs.visibility : null,
宿主宽高: w ? w.offsetWidth + "x" + w.offsetHeight : null,
宿主父节点: w && w.parentElement ? w.parentElement.tagName + (w.parentElement.id ? "#" + w.parentElement.id : "") : null,
容器存在: !!el,
容器类名: el ? el.className : null,
容器子节点数: el ? el.children.length : 0,
样式标签数: styleTags.length,
样式字符数: styleTags.map((t) => (t.textContent || "").length),
display: cs ? cs.display : null,
position: cs ? cs.position : null,
left: cs ? cs.left : null,
top: cs ? cs.top : null,
zIndex: cs ? cs.zIndex : null,
宽: el ? el.offsetWidth : 0,
高: el ? el.offsetHeight : 0,
矩形: el ? JSON.stringify(el.getBoundingClientRect()) : null,
面板正文长度: body ? body.innerHTML.length : -1
};
}
/** 应急:强制恢复显示并挪到左上角。控制台敲 __ykt_reset() */
function reset() {
const w = import_lib.$elements.wrapper;
if (w) {
if (!document.body.contains(w)) document.body.appendChild(w);
forceHost(w);
}
const el = box();
if (!el) return "浮窗还没建出来";
el.className = "normal";
el.style.position = "fixed";
el.style.left = "20px";
el.style.top = "20px";
el.style.zIndex = "2147483647";
el.style.display = "";
return "已强制恢复显示";
}
/**
* 把宿主 div 摁成可见。
*
* 宿主在 light DOM 里没有 class、没有 id、没有任何子节点,页面上一条
* `div:empty { display: none }` 之类的重置规则就能把它整个关掉。
* 宿主不渲染,里面的 container-element 就算 position:fixed 也量不出尺寸 ——
* 实测自检报的就是 display:block / position:fixed / z-index:99999 全对,
* 但 getBoundingClientRect 全是 0。
*
* assets/panel.css 里的 :host 规则已经兜了一层,这里再用行内 !important 兜一层:
* 行内 !important 的权重压得过页面里任何选择器,是最后一道保险。
*/
var HOST_STYLE = [
["display", "block"],
["visibility", "visible"],
["opacity", "1"],
["position", "static"],
["width", "auto"],
["height", "auto"],
["max-width", "none"],
["max-height", "none"],
["transform", "none"],
["filter", "none"],
["clip-path", "none"],
["contain", "none"],
["content-visibility", "visible"],
["pointer-events", "auto"]
];
function forceHost(w) {
for (const [k, v] of HOST_STYLE) w.style.setProperty(k, v, "important");
}
/** 只说一次,别把控制台刷爆 */
var warnedNoWrapper = false;
var warnedRemount = false;
/** 被 Ctrl+O / 三连击切成隐藏的话,本次加载纠正一次就好,别一直跟用户抢 */
var unhidOnce = false;
var reported = false;
function check() {
const w = import_lib.$elements.wrapper;
if (!w) {
if (!warnedNoWrapper) {
warnedNoWrapper = true;
console.warn(LOG, "顶层帧没有创建浮窗。看看控制台有没有 easy-us 抛的错。");
}
return;
}
if (!document.body.contains(w)) {
document.body.appendChild(w);
if (!warnedRemount) {
warnedRemount = true;
console.warn(LOG, "浮窗被页面从 DOM 里移走了,已重新挂回 document.body。");
}
}
forceHost(w);
const el = box();
if (el && el.classList.contains("hidden") && !unhidOnce) {
unhidOnce = true;
el.className = "normal";
console.warn(LOG, "面板之前被切成了隐藏状态(Ctrl+O / 三连击),已恢复显示。");
}
clamp();
if (!reported && el && document.body.contains(import_lib.$elements.wrapper)) {
reported = true;
console.log(LOG, "面板自检:", report(), "(随时可在控制台敲 __ykt() 重查,__ykt_reset() 强制恢复)");
}
}
/**
* 浮窗只在顶层帧存在(easy-us 的 start.js 里 `if (self === top)`),
* 所以这个守望也只在顶层帧跑。
*/
function watchPanel() {
if (self !== top) return;
try {
const g = win();
g.__ykt = report;
g.__ykt_reset = reset;
} catch {}
check();
setInterval(check, 1e3);
window.addEventListener("resize", clamp);
}
/**
* 面板避让底栏。
*
* legacy 里这是 placePanel():面板和 .problem-fixedbar(提交 / 下一题那一条)
* 都钉在同一个视口底部,会互相压住,所以要动态避让。
*
* 现在只在「顶层帧自己就是题目页」的情况下才有意义 —— 正常走 lms-graph 时
* 底栏在 iframe 里,顶层帧查不到这个选择器,这里自然就是个空转。
*
* 推完就不再重叠,下一拍自然不会再动 —— 会收敛,不会打架。
*/
function dodgeFixedBar() {
const el = box();
if (!el) return;
const bar = document.querySelector(".problem-fixedbar");
const h = bar && bar.offsetHeight || 0;
if (!h) return;
const rect = el.getBoundingClientRect();
if (!rect.height) return;
const overlap = rect.bottom - (window.innerHeight - h);
if (overlap > 0) el.style.top = Math.max(0, rect.top - overlap) + "px";
}
//#endregion
//#region src/ui/panel.ts
/**
* 「🤖 AI 自动答题」的面板主体。
*
* 和 legacy 的 mountPanel 相比少了一大半东西,因为这些活 easy-us 接管了:
* - 浮窗本身、拖动、最小化 —— 框架的 CustomWindow
* - 三个开关、Key / 模型 / 接口地址输入框、服务商下拉 —— 声明成 Script.configs 就自动渲染
* - 配置的持久化和跨帧同步 —— this.cfg 双向绑定,不用自己写 GM_getValue
*
* 还有一段 hack 可以整段删掉:legacy 为了对抗 Element-UI 的全局 reset,
* 强行给 checkbox 写 appearance:auto / -webkit-appearance:checkbox 一堆内联样式。
* easy-us 的面板挂在 closed Shadow DOM 里,页面 CSS 进不来,不需要这些。
*
* 这里只剩两个按钮和一行状态文字 —— 它们不是配置项,配置表单放不下。
*
* ⚠️ 这个面板只会在 **顶层帧** 渲染(easy-us 的窗口是 self === top 才建的),
* 而干活的代码在题目 iframe 里。所以状态文字要靠 setSayForward 往上送,
* 按钮要靠 lms/bridge.ts 往下发。
*/
var statusEl$1 = null;
/** 滚动字幕那一行。空的时候整行收起来,不占版面 */
var thinkEl = null;
/** 字幕也要攒着:面板可能比第一段思考过程晚渲染出来 */
var pendingThink = "";
/** 面板还没渲染出来时先攒着,渲染出来立刻补上(legacy 的 pending) */
var pending = "在 ⚙️ 里填好 API Key,然后点「🤖 解题」。当前:只勾选,不提交。";
/** 在题目帧里装上,把每一句状态转发给顶层面板;顶层帧不装 */
var forward = null;
/** 同上,字幕专用。两条通道分开,免得高频字幕把状态文字的去重搅乱 */
var forwardThink = null;
/** 题目帧专用:把状态文字接到跨帧通道上 */
function setSayForward(fn) {
forward = fn;
}
/** 题目帧专用:把滚动字幕接到跨帧通道上 */
function setThinkForward(fn) {
forwardThink = fn;
}
function say(msg) {
pending = msg;
if (statusEl$1) statusEl$1.textContent = msg;
if (forward) forward(msg);
}
/** 顶层帧收到题目帧送上来的状态时用这个写面板,不能再转发回去,否则会来回弹 */
function showStatus(msg) {
pending = msg;
if (statusEl$1) statusEl$1.textContent = msg;
}
/**
* 滚动字幕:模型正在想什么。
*
* 这是进度条,不是日志 —— 只显示当前这一行,看过就扔,
* 所以这里永远是覆盖(textContent =),绝不追加。空串表示收工,整行收起来。
*/
function sayThink(line) {
pendingThink = line;
paintThink();
if (forwardThink) forwardThink(line);
}
/** 顶层帧收到题目帧送上来的字幕时用这个,不再转发回去 */
function showThink(line) {
pendingThink = line;
paintThink();
}
function paintThink() {
if (!thinkEl) return;
thinkEl.textContent = pendingThink ? "💭 " + pendingThink : "";
thinkEl.style.display = pendingThink ? "block" : "none";
}
function button(label, title, primary) {
const b = document.createElement("button");
b.textContent = label;
b.title = title;
b.style.cssText = "cursor:pointer;border:1px solid #dcdfe6;border-radius:5px;padding:6px 9px;font:inherit;" + (primary ? "flex:1;border-color:#1677ff;background:#1677ff;color:#fff" : "background:#fafafa");
return b;
}
/** 面板里那行灰色小字的统一样式 */
var STATUS_CSS = "margin-top:7px;color:#909399;white-space:pre-wrap;word-break:break-all;max-height:160px;overflow:auto;font:12px/1.5 inherit";
/**
* 往 easy-us 的面板主体里塞自定义内容。
* 面板每次展开 / 切换都会重新渲染,所以这里必须是幂等的 —— 先清空再画。
*/
function renderPanelBody(body, actions) {
body.replaceChildren();
const row = document.createElement("div");
row.style.cssText = "display:flex;gap:6px;margin-top:8px";
const go = button("🤖 解题", "有答案就填上;没答案就问 AI", true);
const re = button("🔄", "重新抓题", false);
go.addEventListener("click", actions.solve);
re.addEventListener("click", actions.refetch);
row.append(go, re);
const st = document.createElement("div");
st.style.cssText = STATUS_CSS;
st.textContent = pending;
const th = document.createElement("div");
th.style.cssText = "margin-top:4px;color:#b0b3b8;font:11px/1.6 inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis";
body.append(row, st, th);
statusEl$1 = st;
thinkEl = th;
paintThink();
}
//#endregion
//#region src/scripts/answer.ts
/**
* 🤖 AI 自动答题。
*
* ⚠️ 这个脚本在两种帧里跑,干的事完全不同:
*
* 题目帧(/v2/web/iframe-exercise/…) 真正干活的地方。
* hook ajax 拿题、解字体、问 AI、勾选项、提交翻页。
* 这一帧里 **没有面板** —— easy-us 的浮窗只在顶层帧创建
* (lib/utils/start.js 的 `if (self === top)`)。
*
* 顶层帧(/ai-workspace/lms-graph/…/exercise/…) 只有面板。
* 它自己既抓不到题也点不了选项(DOM 全在 iframe 里),
* 所以按钮要隔着帧发下去、状态要隔着帧送上来,走 lms/bridge.ts。
*
* 用户直接打开 iframe-exercise 的地址时这两种帧重合,那就本地直调,不绕 cors。
*
* 配置全部声明成 configs 交给 easy-us:它负责渲染表单、持久化、跨帧同步,
* 所以 legacy 里那套 cfg() / setCfg() / GM_getValue 和手写的输入框全删了。
*
* ⚠️「② 自动提交并翻页」默认关,关着时这个脚本绝不会点任何提交按钮。
* 唯一的闸门在 runner/step.ts 的 step() 里读 ai_go。
*/
var started = false;
var busy = false;
/**
* 这一帧是不是真正装着题目的那一帧。
*
* 必须按 URL 判断而不是按 self === top:顶层的 lms-graph 地址里也带 /exercise/,
* 光看 matches 两种帧都命中,但只有 iframe-exercise 那一帧里有题目 DOM。
* 每次现算不缓存 —— SPA 会改 URL。
*/
function isExerciseFrame() {
return /\/iframe-exercise\//i.test(location.href);
}
/**
* 读配置。指向当前 Script 实例的 this.cfg,在 onstart 里装上。
* 做成模块级而不是 Script.methods:methods 的类型在 Script 内部推不出来,
* 而 aiConf / provId 要的就是一个 (key) => unknown 的纯函数。
*/
var read$1 = () => void 0;
/**
* 把一份接口 JSON 喂进解析链。两个入口共用:
* - hookAjax 拦到页面自己发的 get_exercise_list
* - doRefetch 自己补发的那一次
*/
function ingest(raw) {
captureJson(raw, {
seenFont: () => state.seenFont,
loadFont,
log: say,
onBatchStart: (font) => {
state.seenFont = font;
state.filled = {};
state.rewound = false;
setActiveFont(font);
},
onExercise: (ex) => {
state.EX = ex;
say("已解出 " + ex.list.length + " 题:" + ex.name);
if (read$1("ai_auto") && aiConf(read$1).key) solve();
}
});
}
/**
* 补发一次题目请求。
*
* 两件事一件都不能省:
* 1. 先把 seenFont 清掉 —— captureJson 开头有去重(同一份 font 直接 return),
* 不清的话重抓同一份作业会被静默吞掉,界面上就是点了 🔄 什么也没发生。
* 2. 必须拿回调里的 raw 显式喂给 ingest。refetch 用的是本脚本作用域里的裸 fetch,
* 而 hookAjax 替换的是 unsafeWindow 上的 fetch 属性 —— 这两个是不是同一个对象,
* 由脚本管理器的沙箱实现决定,不能赌。legacy 也从不赌:它清掉 seenFont 之后显式调 captureJson。
*
* 显式调不会造成重复处理:万一沙箱确实转发到了被 hook 的那个 fetch,
* 钩子会先跑一遍 ingest 并把 seenFont 设成 d.font,等回调里再调一次就被去重挡掉;
* 反过来也一样。两条路径至多生效一条 —— 不会问两遍 AI,也不会重复解字体。
*/
function doRefetch() {
state.seenFont = null;
refetch((err, raw) => {
if (err) {
say("抓题失败:" + err);
return;
}
ingest(raw);
});
}
/** 面板上「🤖 解题」按下去的完整分支,和 legacy 一致 */
function solve() {
if (state.EX && state.EX.answers) {
fillCurrent();
return;
}
if (state.EX) {
askAll({
ex: state.EX,
read: read$1,
log: say,
isBusy: () => busy,
setBusy: (v) => {
busy = v;
},
onAnswers: (answers, why) => {
if (!state.EX) return;
state.EX.answers = answers;
state.EX.why = why;
},
onReady: () => fillCurrent(),
tick: sayThink
});
return;
}
say("没抓到题目,正在补请求…");
doRefetch();
}
/** 🔄 重新抓题 */
function refetchAll() {
resetState();
say("重新抓题…");
doRefetch();
}
/**
* 面板按钮的统一出口。
* 面板在顶层帧,引擎在题目帧 —— 同一帧就直接调,不同帧就隔着 cors 发下去。
*/
function dispatch(cmd) {
if (isExerciseFrame()) {
if (cmd === "solve") solve();
else refetchAll();
return;
}
showStatus("已把「" + (cmd === "solve" ? "解题" : "重新抓题") + "」发给题目页,等它回话…");
sendCmd(cmd);
}
var answerScript = new import_lib.Script({
name: "🤖 AI 自动答题",
namespace: "ykt.answer",
matches: [["雨课堂题目页", /exercise/i]],
configs: {
notes: { defaultValue: "选服务商、填 API Key(点下面的「申请」去领,都有免费额度),然后点「🤖 解题」。
⚠️ 提交不可逆。自动化默认全关,关着时只勾选项、绝不点提交。" },
ai_provider: {
label: "服务商",
tag: "select",
options: PROV_ORDER.map((id) => ({
label: PROVIDERS[id].name + "(" + PROVIDERS[id].note + ")",
value: id
})),
defaultValue: "deepseek",
separator: "接入 AI"
},
...credConfigs(),
ai_think: {
label: "推理模式(更准,更慢)",
attrs: {
type: "checkbox",
title: "让模型先想再答。这家不支持推理时这个勾不起作用"
},
defaultValue: true
},
ai_auto: {
label: "① 自动解题并填入",
attrs: {
type: "checkbox",
title: "抓到题目后自动问 AI 并勾上选项。只勾不交,勾错了你随时能自己改"
},
defaultValue: false,
separator: "自动化(从上往下逐级打开,③ 在「📚 作业跳转」里)"
},
ai_go: {
label: "② 自动提交并翻到下一题",
attrs: {
type: "checkbox",
title: "⚠️ 提交不可逆。先用 ① 跑几道确认答得对,再开这个"
},
defaultValue: false
},
ai_stream: {
label: "流式输出(面板上滚动显示思考过程)",
attrs: {
type: "checkbox",
title: "一边生成一边显示,当进度条用,看得出它在搜什么、想到哪了。思考过程只滚不存,不占内存。个别脚本管理器或代理会把流整个缓冲到最后才给,那种情况关掉它"
},
defaultValue: true,
separator: "排查"
},
ai_debug: {
label: "把思考过程打进控制台",
attrs: {
type: "checkbox",
title: "F12 打开控制台,能看到完整的请求体、思考过程 CoT 和原始返回。答错了想知道它怎么想的就开这个"
},
defaultValue: false
}
},
onstart() {
if (started) return;
started = true;
read$1 = (key) => this.cfg[key];
if (!isExerciseFrame()) return;
if (self !== top) {
setSayForward(reportStatus);
setThinkForward(reportThink);
}
setHooks({
log: say,
getCfg: (key, dflt) => {
const v = this.cfg[key];
return v === void 0 ? dflt : v;
},
setCfg: (key, value) => {
this.cfg[key] = value;
},
onAllDone: (total) => {
say("通知课程页:这份作业 " + total + " 题已全部提交");
reportAllDone(total);
}
});
hookAjax(ingest);
},
oncomplete() {
if (isExerciseFrame()) {
serveExerciseFrame({
solve,
refetch: refetchAll
});
serveProgress(tabProgress);
startNavWatch();
setTickHook(dodgeFixedBar);
setInterval(tick, 1200);
} else {
onExerciseStatus(showStatus);
onExerciseThink(showThink);
}
},
onrender({ panel }) {
renderPanelBody(panel.body, {
solve: () => dispatch("solve"),
refetch: () => dispatch("refetch")
});
}
});
//#endregion
//#region src/clipboard/oncopy.ts
/**
* 复制拦截。搬自 ykt_template.js 源 317-438 行(mapChars / buildDecrypted / onCopy / diagnose)。
*
* 唯一的改动:**配置读取由调用方注入**。
* 旧版这里有个 cfg(key, dflt) 直接读 GM_getValue(源 312-315 行),onCopy 开头
* `if (!cfg('enabled', true)) return;`。现在配置归 easy-us 的 Script.cfg 管,
* 那是个带响应式的面板配置,不是 GM_getValue,而且这个模块不该知道配置存在哪、叫什么名字。
* 所以 cfg 整个删掉,改成 installCopyHook({ enabled }) 由调用方传一个「现读」的回调进来 ——
* 必须是回调而不是布尔值:用户在面板上改开关是随时的,复制那一刻才读到的才算数。
*
* 没有做成「强制解密」之类的额外开关,是因为 legacy/README.md 的硬约束在这儿卡死了:
* 字体没解析好就不干预复制(宁可复制出乱码,也不要复制出看起来通顺的错字),
* 而且只替换 .xuetangx-com-encrypted-font 内部的字符(混淆字表的 key 集合和 value 集合
* 相同,盲替换会毁掉正常中文)。任何「强行解密」的语义都只能靠破坏这两条来实现,
* 所以这里不留这个口子;真要加,得先想清楚加的到底是什么。
*/
function mapChars(s, m) {
let out = "";
for (const ch of s) {
const r = m.get(ch);
out += r === void 0 ? ch : r;
}
return out;
}
function buildDecrypted(range, m) {
const host = doc().createElement("div");
host.appendChild(range.cloneContents());
host.querySelectorAll(STRIP).forEach(function(n) {
n.remove();
});
let changed = 0;
function mapTextNodes(root) {
const w = doc().createTreeWalker(root, NodeFilter.SHOW_TEXT);
for (let n = w.nextNode(); n; n = w.nextNode()) {
const out = mapChars(n.data, m);
if (out !== n.data) {
n.data = out;
changed++;
}
}
}
const anchor = range.commonAncestorContainer;
const anchorEl = anchor.nodeType === 1 ? anchor : anchor.parentElement;
let hits;
if (anchorEl && typeof anchorEl.closest === "function" && anchorEl.closest(".xuetangx-com-encrypted-font")) {
hits = [anchorEl.closest(SELECTOR)];
mapTextNodes(host);
} else {
hits = Array.prototype.slice.call(host.querySelectorAll(SELECTOR));
hits.forEach(mapTextNodes);
}
if (changed === 0) return {
text: null,
changed: 0,
hits
};
host.style.cssText = "position:fixed;left:-99999px;top:0;white-space:pre-wrap;";
doc().body.appendChild(host);
const text = host.innerText;
host.remove();
return {
text,
changed,
hits
};
}
var warned = false;
/** 造一个 copy 监听器。配置从 opts 现读,模块自己不碰 GM_getValue */
function createCopyHandler(opts) {
return function onCopy(e) {
if (!opts.enabled()) return;
if (!e.clipboardData) return;
const sel = win().getSelection();
if (!sel || sel.isCollapsed || sel.rangeCount === 0) return;
const m = readyMap();
if (!m) {
if (!warned && doc().querySelector(".xuetangx-com-encrypted-font")) {
warned = true;
ensureFont();
console.warn(LOG, "字体尚未就绪,本次未干预。等一两秒再复制。");
}
return;
}
let res;
try {
res = buildDecrypted(sel.getRangeAt(0), m);
} catch (err) {
console.warn(LOG, "处理选区失败,已放行原生复制:", err);
return;
}
if (res.text === null) return;
e.clipboardData.setData("text/plain", res.text);
e.preventDefault();
};
}
/**
* 挂上 copy 拦截(旧版是 IIFE 里那句 document.addEventListener('copy', onCopy, true))。
* 返回卸载函数:easy-us 的脚本可能被停用,得能摘干净。
*/
function installCopyHook(opts) {
const onCopy = createCopyHandler(opts);
doc().addEventListener("copy", onCopy, true);
return function uninstall() {
doc().removeEventListener("copy", onCopy, true);
};
}
function diagnose() {
const lines = [];
(function walk(w, path) {
let d = null, href = "(跨域)";
try {
href = w.location.href;
d = w.document;
} catch (e) {}
if (!d) {
lines.push({
frame: path,
url: href,
crossOrigin: true
});
return;
}
const enc = d.querySelectorAll(SELECTOR);
let url = null;
try {
const ss = d.querySelectorAll("style");
for (let i = ss.length - 1; i >= 0 && !url; i--) {
const txt = ss[i].textContent;
if (txt && txt.indexOf("exam-data-decrypt-font") !== -1) {
const mm = FONT_URL_RE.exec(txt);
if (mm) url = mm[1];
}
}
} catch (e) {}
const ent = url ? cache.get(url) : null;
lines.push({
frame: path,
url: href,
encCount: enc.length,
font: url ? url.split("/").pop() : "(未找到)",
state: ent ? ent.state : "(本 frame 未处理该字体)",
mapped: ent && ent.map ? ent.map.size : 0,
sample: Array.prototype.slice.call(enc, 0, 3).map(function(el) {
const m = ent && ent.map;
return el.textContent + (m ? " → " + mapChars(el.textContent, m) : "");
})
});
for (let k = 0; k < w.frames.length; k++) try {
walk(w.frames[k], path + ">" + k);
} catch (e) {}
})(win().top, "top");
console.log(LOG, "诊断:");
console.table(lines.map(function(l) {
return {
frame: l.frame,
加密元素: l.encCount,
字体: l.font,
状态: l.state,
已还原: l.mapped
};
}));
lines.forEach(function(l) {
if (l.sample && l.sample.length) console.log(LOG, l.frame, l.sample);
});
console.log(LOG, "完整信息:", lines);
}
//#endregion
//#region src/scripts/copy.ts
/**
* 📋 复制解密。
*
* 匹配全站、document-start、每一帧都跑 —— 加密内容在 iframe 里,
* **绝不能加 @noframes**,也不能只在顶层帧装监听。
*
* 只改剪贴板,不动 DOM。页面显示的仍然是那套乱码字,
* 这样 ocsjs 的「🔤 字体解密」就算开着也不会互相覆盖(虽然还是建议关掉它)。
*/
var copyScript = new import_lib.Script({
name: "📋 复制解密",
namespace: "ykt.copy",
matches: [["雨课堂全站", /.*/]],
configs: {
notes: { defaultValue: "选中题目按 Ctrl-C,粘出来就是正确汉字 —— 只改剪贴板,页面显示原样不动。
普通中文照常复制,脚本不介入。
⚠️ 请关掉 ocsjs 的「🔤 字体解密」,它用的静态字表已经失效,开着会把页面改成另一套乱码。" },
enabled: {
label: "复制解密",
attrs: {
type: "checkbox",
title: "关掉之后 Ctrl-C 走浏览器原生复制,脚本完全不介入"
},
defaultValue: true
},
diagnose: {
label: "出问题时用",
attrs: {
type: "button",
value: "🔍 把当前帧状态打进控制台"
},
defaultValue: "",
onload() {
this.addEventListener("click", diagnose);
}
}
},
onstart() {
installCopyHook({ enabled: () => this.cfg.enabled });
}
});
//#endregion
//#region src/lms/dom.ts
/**
* AI 学伴课程页侧栏的 DOM 层。选择器全部来自实测 DOM,改之前先看这里的注释。
*
* ⚠️ ocsjs 在这块踩了两个坑,都不是语法错、不报错、只是静默失效,抄的时候别一起抄过来:
*
* 1. 展开态的标记类是 **is-expand**,而且挂在 **.expand-icon 的父节点 .nav-item-title** 上。
* ocsjs 查的是 `.expand-icon:not(.is-expanded )` —— .expand-icon 上永远没有 is-expanded,
* 于是全部命中,它那个 5 轮循环就变成对每个章节点 5 次 toggle(奇数次),
* 结果是把页面默认展开的章节全给合上了。
*
* 2. 完成标记的真实类名是 **icon-yuanquangou-mianzhuang**。
* ocsjs 写 `.icon-yuanquangou` —— CSS 类选择器是精确 token 匹配不是前缀匹配,
* 匹配不到,所以它的「过滤已完成」从来没生效过。
* 这里用 [class*="yuanquangou"] 做子串匹配,雨课堂换实心/空心图标也不会挂。
*/
/** 一个任务项。作业平铺在顶层 .nav-item-list 下,视频在 .nav-item-child 里,都是这个类 */
var LEAF = ".leaf-item";
/** 未展开的章节头上的那个箭头。父节点没有 is-expand 才是真的没展开 */
var COLLAPSED_ARROW = ".nav-item-title:not(.is-expand) .expand-icon";
/**
* 完成标记。真实类名是 icon-yuanquangou-mianzhuang,未完成时这个位置是 Vue 的空注释节点。
* 用子串匹配而不是写死类名:CSS 类选择器是精确 token 匹配,
* 写 .icon-yuanquangou 匹配不到 icon-yuanquangou-mianzhuang(ocsjs 就是栽在这儿)。
*/
var DONE_ICON = ".leaf-item-status [class*=\"yuanquangou\"]";
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** 标签文本实测是 "\n 作业\n ",空白必须全清掉 */
function tidy(el) {
return (el?.textContent || "").replace(/\s+/g, "");
}
function toView(el) {
return {
tag: tidy(el.querySelector(".leaf-item-tag")),
title: (el.querySelector(".leaf-item-title")?.textContent || "").trim(),
done: !!el.querySelector(DONE_ICON),
active: el.classList.contains("is-active")
};
}
function leafElements() {
return Array.from(doc().querySelectorAll(LEAF));
}
function leafViews() {
return leafElements().map(toView);
}
/**
* 把所有折叠着的章节展开。
*
* 只点没展开的,所以对「默认就是展开的」这种页面是彻底的空操作 —— 这正是 ocsjs 缺的那一下。
* 循环是因为展开一层会把下一层的章节头渲染出来,得再扫一遍;
* 一轮下来没有可点的就立刻停,不会空转满 max 轮。
*/
async function expandAll(max = 8) {
let clicked = 0;
for (let i = 0; i < max; i++) {
const todo = Array.from(doc().querySelectorAll(COLLAPSED_ARROW));
if (todo.length === 0) break;
for (const el of todo) {
el.click();
clicked++;
}
await sleep(150);
}
return clicked;
}
/** 等侧栏渲染出来。SPA,进页面时 .leaf-item 还不存在 */
async function waitLeaves(timeout = 15e3) {
const until = Date.now() + timeout;
while (Date.now() < until) {
if (leafElements().length > 0) return true;
await sleep(300);
}
return false;
}
/** 点进某一项,并滚到可视区中间 */
async function openLeaf(index) {
const el = leafElements()[index];
if (!el) return;
el.click();
await sleep(200);
el.scrollIntoView({
behavior: "smooth",
block: "center"
});
}
//#endregion
//#region src/lms/nav.ts
var TAG_HOMEWORK = "作业";
/**
* 挑出下一个该做的项,返回它在 views 里的下标,没有就返回 -1。
*
* 顺序规则:先从当前项往后找,找不到再从头回绕一遍。
* 回绕是必须的 —— 作业在侧栏里是平铺的一长条,用户完全可能从中间点进去,
* 前面还漏着没做的。ocsjs 的 getNextJob 用 splice(active_index) 直接把前半截扔了,
* 那样前面漏的永远够不着。
*/
function pickNext(views, opts) {
const ok = (v) => v.tag === opts.tag && (opts.restudy || !v.done);
const ai = views.findIndex((v) => v.active);
const from = ai < 0 ? 0 : opts.includeActive ? ai : ai + 1;
for (let i = from; i < views.length; i++) if (ok(views[i])) return i;
for (let i = 0; i < Math.min(from, views.length); i++) if (ok(views[i])) return i;
return -1;
}
/** 统计:面板上要显示「作业 3/10」 */
function countBy(views, tag) {
const hit = views.filter((v) => v.tag === tag);
return {
done: hit.filter((v) => v.done).length,
total: hit.length
};
}
/** 「跳转前等待」下拉框的选项。value 是字符串 —— easy-us 的 select 存的就是字符串 */
var DELAY_CHOICES = [
{
value: "3",
label: "3 秒(最短)",
title: "几乎不给你反悔的时间"
},
{
value: "8",
label: "8 秒(推荐)",
title: "够扫一眼 AI 填的答案,不对就按「取消」"
},
{
value: "15",
label: "15 秒",
title: "慢慢看"
},
{
value: "30",
label: "30 秒(近乎手动)",
title: "基本等于每道都自己确认一次"
}
];
/**
* 读「跳转前等待」的秒数。
*
* 下限锁 3 秒不是随便定的:交卷是异步的,题目帧喊「交完了」的时候回执可能还在路上,
* 这会儿把 iframe 换掉,那条回执落地时里面已经是下一份作业了。
* 同 runner/clock.ts 里 POST_SUBMIT_HOLD 的道理,只是这里跨的是帧。
* 所以哪怕存进去的是 0,也要抬回 3。
*/
function delaySeconds(raw) {
const n = Math.floor(Number(raw));
if (!isFinite(n) || n <= 0) return 8;
return Math.min(60, Math.max(3, n));
}
//#endregion
//#region src/ui/jump-panel.ts
/**
* 「📚 作业跳转」的面板主体。
*
* 单独一个模块、不和 ui/panel.ts 共用模块级变量:两个脚本的面板会在同一帧里
* 先后渲染(lms-graph 的题目页两个脚本都匹配得上),共用一个 statusEl
* 会让后渲染的那个把前一个的引用覆盖掉。
*
* 比 ui/panel.ts 多一样东西:倒计时行。自动跳转不能说跳就跳 ——
* 页面在眼皮底下自己换掉是很糟的体验,而且那会儿人可能正想看一眼 AI 答得对不对。
* 所以倒计时是看得见的,而且随时能按「取消」留下来。
*/
var statusEl = null;
var countRow = null;
var countText = null;
/** 面板还没渲染出来时先攒着,渲染出来立刻补上 */
var pendingStatus = "正在等侧栏加载…";
var pendingCount = "";
function jumpSay(msg) {
pendingStatus = msg;
if (statusEl) statusEl.textContent = msg;
}
/** 显示 / 刷新倒计时行 */
function showCountdown(text) {
pendingCount = text;
if (countText) countText.textContent = text;
if (countRow) countRow.style.display = "flex";
}
function hideCountdown() {
pendingCount = "";
if (countRow) countRow.style.display = "none";
}
function renderJumpBody(body, actions) {
body.replaceChildren();
const row = document.createElement("div");
row.style.cssText = "display:flex;gap:6px;margin-top:8px";
const go = button("⏭ 下一道没做完的作业", "当前这道没做完就先做这道,做完了才往后翻", true);
go.addEventListener("click", actions.next);
row.append(go);
const st = document.createElement("div");
st.style.cssText = STATUS_CSS;
st.textContent = pendingStatus;
const cd = document.createElement("div");
cd.style.cssText = "display:none;align-items:center;gap:6px;margin-top:7px;padding:6px 8px;border:1px solid #ffe58f;background:#fffbe6;border-radius:5px;font:12px/1.5 inherit";
const txt = document.createElement("span");
txt.style.cssText = "flex:1;color:#874d00";
txt.textContent = pendingCount;
const stop = button("取消", "留在这一页,不自动跳转", false);
stop.style.padding = "2px 8px";
stop.addEventListener("click", actions.cancel);
cd.append(txt, stop);
body.append(row, st, cd);
statusEl = st;
countRow = cd;
countText = txt;
if (pendingCount) showCountdown(pendingCount);
}
//#endregion
//#region src/scripts/jump.ts
/**
* 📚 作业跳转 —— AI 学伴课程页(/ai-workspace/lms-graph)。
*
* 这一页只有侧栏导航,题目本身在右边的 iframe 里,由「🤖 AI 自动答题」接管。
* 本脚本干四件事:展开章节、找出下一道没做完的作业、点进去、等内层答完再往下走。
*
* 排除手机版:/ai-workspace/lms-graph-mobile 的 DOM 不长这样,
* 而字符串匹配是子串匹配,'lms-graph' 会把 mobile 一起吃掉,所以这里用正则断言。
*/
/** 指向当前 Script 的 this.cfg。config 里 button 的 onload 拿不到 Script 实例,只能这么绕 */
var read = () => void 0;
/**
* 已经因为「做完了」而跳走过的作业标题。
*
* 跨帧信号和侧栏轮询是两条独立的触发路径,同一道作业两条都可能响;
* 内层帧还会因为看门狗解锁再喊一次。共用这一个集合,保证一道作业只跳一次 ——
* 少了它就会一次连跳两道,中间那道直接被略过。
*/
var jumped = /* @__PURE__ */ new Set();
/**
* 本次会话里已经确认交完的作业标题。
*
* 侧栏那个「已完成」圆圈勾是服务端下发的,刷新慢一拍 —— 四道题全交完了,
* 侧栏还显示没做完,于是「⏭ 下一道」一直说「就是当前这道,先把它做完」。
* 这个集合是盖在侧栏状态上的一层修正:只要题目帧说交完了,就按交完算。
*/
var finished = /* @__PURE__ */ new Set();
/** 侧栏状态 + finished 修正。除了读原始 DOM,其它地方一律用这个 */
function views() {
return leafViews().map((v) => v.done || !finished.has(v.title) ? v : {
...v,
done: true
});
}
function progress() {
const c = countBy(views(), TAG_HOMEWORK);
return "作业 " + c.done + "/" + c.total;
}
function activeTitle() {
return views().find((v) => v.active)?.title || "";
}
/**
* 跳到下一道没做完的作业。
* includeActive=true 用于手动点按钮(当前这道没做完就先做这道,别莫名其妙往后跳)。
*/
async function jumpNext(includeActive) {
const list = views();
if (list.length === 0) {
jumpSay("侧栏还没加载出来,等一下再点");
return false;
}
const i = pickNext(list, {
tag: TAG_HOMEWORK,
includeActive,
restudy: !!read("lms_restudy")
});
if (i < 0) {
jumpSay("作业全做完了(" + progress() + ")。想重做就打开复习模式。");
import_lib.$message.success("作业全做完了");
return false;
}
if (list[i].active) {
jumpSay("就是当前这道,先把它做完:" + list[i].title);
return false;
}
jumpSay("跳转:" + list[i].title + " " + progress());
await openLeaf(i);
return true;
}
/**
* 「⏭ 下一道」按下去。
*
* 不能直接信侧栏:它的完成勾是服务端下发的,交完卷要等下一次刷新才亮。
* 所以当侧栏说「当前这道没做完」时,先隔着帧问一句题目 iframe ——
* 题号格子在那一帧里是实时的,四个格子全打勾就是真交完了。
*
* 问不到(课程页上还没点开任何一道作业,没人应答)就退回原来的行为。
*/
function manualNext() {
const cur = views().find((v) => v.active);
if (!cur || cur.tag !== "作业" || cur.done) {
jumpNext(true);
return;
}
jumpSay("侧栏说这道没做完,问一下题目页…");
askProgress((p) => {
if (!p || p.total === 0) {
jumpNext(true);
return;
}
if (p.done >= p.total) {
finished.add(cur.title);
jumpSay("这道 " + p.total + " 题都交过了(侧栏还没刷新),继续往下找。");
jumpNext(false);
return;
}
jumpSay("这道还有 " + (p.total - p.done) + " 题没交(" + p.done + "/" + p.total + "),先做完它:" + cur.title);
});
}
/** 正在走的倒计时;0 表示没有 */
var ticker = 0;
function stopCountdown() {
if (ticker) clearInterval(ticker);
ticker = 0;
hideCountdown();
}
/**
* 用户点了「取消」:留在这一页。
* 这道作业已经记进 jumped 了,所以不会再自己跳第二次;想走随时点「⏭」。
*/
function cancelCountdown() {
if (!ticker) {
jumpSay("现在没有在倒计时");
return;
}
stopCountdown();
jumpSay("已取消,留在这一页。想走随时点「⏭」。");
}
function startCountdown(why) {
stopCountdown();
let left = delaySeconds(read("lms_delay"));
const paint = () => showCountdown(why + "," + left + " 秒后切下一道");
paint();
ticker = window.setInterval(() => {
left--;
if (left > 0) {
paint();
return;
}
stopCountdown();
jumpNext(false);
}, 1e3);
}
/** 两条触发路径共用的收尾:记下这道已经跳过,然后往后走 */
function advanceFrom(title, why) {
if (!title || jumped.has(title)) return;
jumped.add(title);
if (!read("lms_auto")) {
jumpSay(why + "。自动跳转没开 —— 点「⏭」手动走下一道。");
return;
}
startCountdown(why);
}
var jumpScript = new import_lib.Script({
name: "📚 作业跳转",
namespace: "ykt.jump",
matches: [["AI学伴课程页", /\/ai-workspace\/lms-graph(?!-mobile)/]],
configs: {
notes: { defaultValue: "课程页侧栏助手:挑出没做完的作业、点进去,题目交给「🤖 AI 自动答题」。
上面的「⏭ 下一道」随时能手动跳,一个自动开关都不开也照样好用。
⚠️ ③ 要生效,得先在题目页把「② 自动提交并翻到下一题」打开,否则里面永远交不了卷。" },
lms_expand: {
label: "进页面自动展开折叠的章节",
attrs: {
type: "checkbox",
title: "只点折叠着的箭头,本来就展开的不会被合上"
},
defaultValue: true,
separator: "侧栏"
},
lms_restudy: {
label: "复习模式(已完成的也跳进去)",
attrs: {
type: "checkbox",
title: "默认只挑没做完的;打开后已完成的也算候选,可以重做"
},
defaultValue: false
},
lms_auto: {
label: "③ 一道交完自动跳下一道",
attrs: {
type: "checkbox",
title: "⚠️ 会连着把整门课的作业做完。先用「⏭」手动跳一道,确认 AI 答得对再开"
},
defaultValue: false,
separator: "连跳(需要题目页先开到 ②)"
},
lms_delay: {
label: "跳转前等待",
tag: "select",
options: DELAY_CHOICES,
defaultValue: "8",
showIf: "ykt.jump.lms_auto"
}
},
async oncomplete() {
read = (key) => this.cfg[key];
onExerciseDone((total) => {
const title = activeTitle();
if (title) finished.add(title);
advanceFrom(title, "这份作业 " + total + " 题已全部提交");
});
if (!await waitLeaves()) {
jumpSay("侧栏一直没出来,刷新页面重试");
return;
}
if (this.cfg.lms_expand) {
const n = await expandAll();
jumpSay(n > 0 ? "展开了 " + n + " 个折叠的章节。" + progress() : progress());
} else jumpSay(progress());
setInterval(() => {
const cur = views().find((v) => v.active);
if (!cur || cur.tag !== "作业" || !cur.done) return;
advanceFrom(cur.title, "侧栏显示这道已完成");
}, 3e3);
},
onrender({ panel }) {
renderJumpBody(panel.body, {
next: manualNext,
cancel: cancelCountdown
});
}
});
//#endregion
//#region src/project.ts
var yktProject = import_lib.Project.create({
name: "雨课堂助手",
domains: ["yuketang.cn"],
scripts: {
copy: copyScript,
answer: answerScript,
jump: jumpScript
}
});
//#endregion
//#region assets/panel.css?raw
var panel_default = "/*\n * 面板样式。\n *\n * 来源:ocsjs/packages/scripts/assets/css/style.css,原样复制。\n *\n * 为什么必须有这个文件:easy-us **自己不带任何 CSS**。\n * 它的 container-element / header-element / script-panel-element 都是裸自定义元素,\n * 没有样式的话 position 是默认的 inline,面板就是一堆纯文本直接掉在\n * document.body 里某个随机位置(mount() 真的是随机插的),\n * 在 SPA 里通常被 overflow:hidden 裁掉 —— 表现就是「脚本在跑但没有 UI」。\n *\n * 关键的一行是 `container-element { position: fixed }`(在下面 container-element 那一段)。\n *\n * 这是 ykt-pass 里唯一一处从 ocsjs 抄来的资源,改 easy-us 版本时留意元素名有没有变。\n */\n\n/*\n * 宿主 div 的兜底。\n *\n * :host 指的是 attachShadow 的那个 (easy-us 的 $elements.wrapper)。\n * 它在 light DOM 里没有任何子节点、没有 class、没有 id —— 也就是说页面上\n * 随便一条 `div:empty { display: none }` 之类的重置规则就能把它整个关掉。\n * 一旦宿主不渲染,里面的 container-element 哪怕是 position:fixed 也量不出尺寸:\n * getBoundingClientRect 全是 0,computed style 却还报 display:block —— 排查时极其迷惑。\n *\n * 页面样式的选择器权重通常压得过 :host,所以这几条必须带 !important。\n */\n:host {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n pointer-events: auto !important;\n /* 宿主本身不参与布局,面板靠 container-element 的 position:fixed 定位 */\n position: static !important;\n margin: 0 !important;\n padding: 0 !important;\n border: 0 !important;\n width: auto !important;\n height: auto !important;\n max-width: none !important;\n max-height: none !important;\n transform: none !important;\n filter: none !important;\n clip-path: none !important;\n contain: none !important;\n content-visibility: visible !important;\n}\n/** 默认字体 */\n/** 输入框默认边距 */\nul,\nol {\n line-height: 26px;\n padding-left: 22px;\n margin: 0px;\n}\na {\n color: #1890ff;\n}\nhr {\n border-style: solid;\n border-color: #63636346;\n border-width: 0px;\n border-bottom: 1px solid #63636346;\n margin-block-start: 1em;\n margin-block-end: 1em;\n}\n.base-style-active-form-control {\n border: 1px solid #ffffff00;\n}\n.base-style-active-form-control:focus {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n}\n.base-style-active-form-control:focus:not([type='checkbox'], [type='radio']) {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n background-color: white !important;\n}\n.base-style-active-form-control:hover {\n background-color: #ebeef4;\n}\n.base-style-input {\n outline: none;\n border: 1px solid #ffffff00;\n padding: 2px 8px;\n margin: 0px;\n background-color: #eef2f7;\n border-radius: 2px;\n color: black;\n}\n.base-style-input::placeholder {\n color: #bababa;\n}\n.base-style-switch {\n appearance: none;\n -moz-appearance: none;\n -webkit-appearance: none;\n width: fit-content;\n min-width: 48px;\n height: 20px;\n border-radius: 100px;\n display: flex;\n align-items: center;\n padding: 2px 4px;\n transition: all 0.2s ease-in-out;\n width: auto;\n background: gainsboro;\n}\n.base-style-switch:checked {\n background: #1890ff;\n}\n.base-style-switch:disabled {\n background-color: #f7f7f78b;\n}\n.base-style-switch:checked::before {\n transform: translate(24px, 0px);\n}\n.base-style-switch::before {\n background-color: #fff;\n border-radius: 9px;\n box-shadow: 0 2px 4px #00230b33;\n width: 14px;\n height: 14px;\n content: '';\n}\n.base-style-button {\n appearance: none;\n -moz-appearance: none;\n -webkit-appearance: none;\n border-radius: 4px;\n background-color: white;\n border: 1px solid #2c92ff;\n color: #409eff;\n cursor: pointer !important;\n margin-bottom: 4px;\n}\n.base-style-button:active {\n box-shadow: 0px 0px 8px #0e8de2a5;\n}\n.base-style-button + .base-style-button {\n margin-left: 12px;\n}\n.base-style-button:hover {\n background-color: #7abbff24;\n}\n.base-style-button.danger:hover {\n background-color: #ffdede86;\n}\n.base-style-button:disabled {\n background-color: white;\n border: 1px solid #c0c0c0;\n color: #aeaeae;\n cursor: not-allowed;\n}\n.base-style-button.danger {\n color: #f36669;\n border-color: #dd5a5d;\n}\n.base-style-button:disabled:active {\n box-shadow: none;\n}\n.base-style-button-secondary {\n appearance: none;\n -moz-appearance: none;\n -webkit-appearance: none;\n border-radius: 4px;\n border: 1px solid #2c92ff;\n color: #409eff;\n cursor: pointer !important;\n margin-bottom: 4px;\n color: gray;\n background-color: white;\n border: 1px solid #dcdcdc;\n}\n.base-style-button-secondary:active {\n box-shadow: 0px 0px 8px #0e8de2a5;\n}\n.base-style-button-secondary + .base-style-button-secondary {\n margin-left: 12px;\n}\n.base-style-button-secondary:hover {\n background-color: #7abbff24;\n}\n.base-style-button-secondary.danger:hover {\n background-color: #ffdede86;\n}\n.base-style-button-secondary:disabled {\n background-color: white;\n border: 1px solid #c0c0c0;\n color: #aeaeae;\n cursor: not-allowed;\n}\n.base-style-button-secondary.danger {\n color: #f36669;\n border-color: #dd5a5d;\n}\n.base-style-button-secondary:disabled:active {\n box-shadow: none;\n}\ncontainer-element.hidden {\n display: none;\n}\ncontainer-element.minimize {\n min-width: unset;\n}\ncontainer-element {\n position: fixed;\n top: 10%;\n left: 10%;\n z-index: 99999;\n text-align: left;\n min-width: 300px;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n color: #636363;\n box-shadow: 0 0 24px -12px #3f3f3f;\n border-radius: 8px;\n letter-spacing: 0.5px;\n border: 1px solid #c1c1c1;\n}\nheader-element {\n display: flex;\n align-items: center;\n background-color: white;\n border-radius: 8px 8px 0px 0px;\n user-select: none;\n padding: 4px;\n padding-bottom: 0px;\n}\nheader-element .extra-menu-bar {\n width: 100%;\n padding: 4px;\n padding-bottom: 0px;\n margin-top: 4px;\n border-top: 1px solid #e8e8e8;\n /** 默认隐藏,一直到需要激活的时候再更改 */\n display: none;\n}\nheader-element .extra-menu-bar .script-panel-link {\n appearance: none;\n -moz-appearance: none;\n -webkit-appearance: none;\n border-radius: 4px;\n border: 1px solid #2c92ff;\n color: #409eff;\n cursor: pointer !important;\n margin-bottom: 4px;\n color: gray;\n background-color: white;\n border: 1px solid #dcdcdc;\n padding-bottom: 2px;\n margin-bottom: 0px;\n}\nheader-element .extra-menu-bar .script-panel-link:active {\n box-shadow: 0px 0px 8px #0e8de2a5;\n}\nheader-element .extra-menu-bar .script-panel-link + header-element .extra-menu-bar .script-panel-link {\n margin-left: 12px;\n}\nheader-element .extra-menu-bar .script-panel-link:hover {\n background-color: #7abbff24;\n}\nheader-element .extra-menu-bar .script-panel-link.danger:hover {\n background-color: #ffdede86;\n}\nheader-element .extra-menu-bar .script-panel-link:disabled {\n background-color: white;\n border: 1px solid #c0c0c0;\n color: #aeaeae;\n cursor: not-allowed;\n}\nheader-element .extra-menu-bar .script-panel-link.danger {\n color: #f36669;\n border-color: #dd5a5d;\n}\nheader-element .extra-menu-bar .script-panel-link:disabled:active {\n box-shadow: none;\n}\nheader-element .extra-menu-bar .script-panel-link.active {\n background-color: #1890ff1a;\n border-color: #1890ff;\n color: #1890ff;\n}\nheader-element .extra-menu-bar .script-panel-link + .script-panel-link {\n margin-left: 4px;\n}\nheader-element .profile {\n flex: 1;\n cursor: move;\n}\nheader-element .switch:hover,\nheader-element .dropdown:hover {\n background-color: #f3f3f3;\n}\nheader-element .close:hover {\n background-color: #ff000038;\n}\nheader-element .switch,\nheader-element .close {\n cursor: pointer;\n}\nheader-element .dropdown {\n line-height: 24px;\n text-decoration: underline;\n}\nheader-element .switch,\nheader-element .close,\nheader-element .profile {\n display: inline-flex;\n align-items: center;\n padding: 0px 8px;\n}\n.logo {\n width: 18px;\n height: 18px;\n cursor: pointer;\n}\n.project-selector {\n display: flex;\n align-items: center;\n}\n.project-selector select {\n background: #ffffff00;\n border-radius: 4px;\n border: 1px solid #63636346;\n padding: 4px;\n}\n.project-selector.expand-all {\n display: none;\n}\n.body {\n overflow: auto;\n width: auto;\n height: 100%;\n}\nscript-panel-element {\n display: block;\n background-color: white;\n border-radius: 0px 0px 8px 8px;\n padding: 0px 8px 12px 8px;\n overflow: auto;\n}\nscript-panel-element .script-panel-body {\n padding: 0px 8px;\n}\nscript-panel-element + script-panel-element {\n margin-top: 12px;\n}\n.card + .card {\n margin-top: 12px;\n}\n.card {\n background-color: white;\n border-radius: 2px;\n padding: 0px 8px;\n}\n.notes {\n background: #0099ff0e;\n border-left: 4px solid #0099ff65;\n width: -webkit-fill-available;\n margin: 0px 8px;\n line-height: 26px;\n letter-spacing: 1px;\n}\n.secondary {\n font-size: 12px;\n color: #8b8b8b;\n}\n.tooltip-container {\n z-index: 99999999999999;\n margin: 12px 0px 0px 12px;\n padding: 4px;\n color: black;\n background: #f0f0f0;\n box-shadow: 0px 0px 4px #949494;\n position: fixed;\n white-space: normal;\n max-width: 200px;\n height: auto;\n border-radius: 2px;\n line-height: 18px;\n}\n.configs-container.lock {\n filter: blur(1px);\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n}\n.configs-container .lock-wrapper {\n cursor: not-allowed !important;\n border-radius: 4px;\n position: absolute;\n left: 0px;\n z-index: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n.configs-container .lock-message {\n background-color: #ffffff7d;\n border-radius: 4px;\n box-shadow: 0px 0px 12px #6a6a6a98;\n padding: 2px;\n}\n.configs {\n display: table;\n background: #e1e1e107;\n width: -webkit-fill-available;\n}\n.configs .configs-body {\n display: table-row-group;\n}\n.configs .configs-body config-element + config-element label {\n padding-top: 4px;\n}\n.configs .configs-body config-element + config-element .config-wrapper {\n padding-top: 4px;\n}\n.configs .configs-body config-element {\n width: 100%;\n display: table-row;\n line-height: 26px;\n}\n.configs .configs-body config-element label {\n white-space: nowrap;\n color: #4e5969;\n display: table-cell;\n padding-right: 12px;\n text-align: left;\n vertical-align: top;\n margin-right: 12px;\n}\n.configs .configs-body config-element .config-wrapper {\n display: table-cell;\n vertical-align: middle;\n /** check box 的样式 */\n}\n.configs .configs-body config-element .config-wrapper select {\n outline: none;\n border: none;\n border: 1px solid #e4e4e4;\n border-radius: 4px;\n padding: 2px 8px;\n border: 1px solid #ffffff00;\n}\n.configs .configs-body config-element .config-wrapper select:focus {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n}\n.configs .configs-body config-element .config-wrapper select:focus:not([type='checkbox'], [type='radio']) {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n background-color: white !important;\n}\n.configs .configs-body config-element .config-wrapper select:hover {\n background-color: #ebeef4;\n}\n.configs .configs-body config-element .config-wrapper textarea {\n padding: 2px 8px;\n outline: none;\n border: none;\n border: 1px solid #ffffff00;\n}\n.configs .configs-body config-element .config-wrapper textarea:focus {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n}\n.configs .configs-body config-element .config-wrapper textarea:focus:not([type='checkbox'], [type='radio']) {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n background-color: white !important;\n}\n.configs .configs-body config-element .config-wrapper textarea:hover {\n background-color: #ebeef4;\n}\n.configs .configs-body config-element .config-wrapper input:not([type='button']) {\n outline: none;\n padding: 2px 8px;\n margin: 0px;\n background-color: #eef2f7;\n border-radius: 2px;\n color: black;\n border: 1px solid #ffffff00;\n}\n.configs .configs-body config-element .config-wrapper input:not([type='button'])::placeholder {\n color: #bababa;\n}\n.configs .configs-body config-element .config-wrapper input:not([type='button']):focus {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n}\n.configs .configs-body config-element .config-wrapper input:not([type='button']):focus:not([type='checkbox'], [type='radio']) {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n background-color: white !important;\n}\n.configs .configs-body config-element .config-wrapper input:not([type='button']):hover {\n background-color: #ebeef4;\n}\n.configs .configs-body config-element .config-wrapper input[type='range'] {\n padding: 0px;\n}\n.configs .configs-body config-element .config-wrapper input[type='button'] {\n appearance: none;\n -moz-appearance: none;\n -webkit-appearance: none;\n border-radius: 4px;\n background-color: white;\n border: 1px solid #2c92ff;\n color: #409eff;\n cursor: pointer !important;\n margin-bottom: 4px;\n}\n.configs .configs-body config-element .config-wrapper input[type='button']:active {\n box-shadow: 0px 0px 8px #0e8de2a5;\n}\n.configs .configs-body config-element .config-wrapper input[type='button'] + .configs .configs-body config-element .config-wrapper input[type='button'] {\n margin-left: 12px;\n}\n.configs .configs-body config-element .config-wrapper input[type='button']:hover {\n background-color: #7abbff24;\n}\n.configs .configs-body config-element .config-wrapper input[type='button'].danger:hover {\n background-color: #ffdede86;\n}\n.configs .configs-body config-element .config-wrapper input[type='button']:disabled {\n background-color: white;\n border: 1px solid #c0c0c0;\n color: #aeaeae;\n cursor: not-allowed;\n}\n.configs .configs-body config-element .config-wrapper input[type='button'].danger {\n color: #f36669;\n border-color: #dd5a5d;\n}\n.configs .configs-body config-element .config-wrapper input[type='button']:disabled:active {\n box-shadow: none;\n}\n.configs .configs-body config-element .config-wrapper input[type='checkbox'] {\n appearance: none;\n -moz-appearance: none;\n -webkit-appearance: none;\n width: fit-content;\n min-width: 48px;\n height: 20px;\n border-radius: 100px;\n display: flex;\n align-items: center;\n padding: 2px 4px;\n transition: all 0.2s ease-in-out;\n width: auto;\n background: gainsboro;\n}\n.configs .configs-body config-element .config-wrapper input[type='checkbox']:checked {\n background: #1890ff;\n}\n.configs .configs-body config-element .config-wrapper input[type='checkbox']:disabled {\n background-color: #f7f7f78b;\n}\n.configs .configs-body config-element .config-wrapper input[type='checkbox']:checked::before {\n transform: translate(24px, 0px);\n}\n.configs .configs-body config-element .config-wrapper input[type='checkbox']::before {\n background-color: #fff;\n border-radius: 9px;\n box-shadow: 0 2px 4px #00230b33;\n width: 14px;\n height: 14px;\n content: '';\n}\n.configs .configs-body config-element .config-wrapper input:not([type='checkbox'], [type='radio']),\n.configs .configs-body config-element .config-wrapper textarea,\n.configs .configs-body config-element .config-wrapper select {\n width: -webkit-fill-available;\n font-size: inherit;\n}\n.configs .configs-body config-element .config-wrapper input[type='checkbox'],\n.configs .configs-body config-element .config-wrapper input[type='radio'],\n.configs .configs-body config-element .config-wrapper input[type='range'] {\n accent-color: #0e8ee2;\n}\n.configs .configs-body config-element .config-wrapper > *:not(.tooltip) {\n background-color: #eef2f7;\n border-radius: 2px;\n color: black;\n float: right;\n}\n.configs .configs-body config-element .config-wrapper > *:disabled {\n cursor: not-allowed;\n background-color: #f7f7f78b;\n}\n.message-container {\n margin-bottom: 4px;\n position: absolute;\n bottom: 100%;\n left: 50%;\n width: 100%;\n transform: translate(-50%, 0px);\n min-width: 300px;\n}\n.message-container message-element {\n display: flex;\n border-radius: 4px;\n padding: 4px 12px;\n margin-bottom: 4px;\n}\n.message-container message-element .message-content-container {\n margin-right: 8px;\n flex: auto;\n}\n.message-container message-element .message-text {\n letter-spacing: 1px;\n font-weight: bold;\n}\n.message-container message-element .message-closer {\n width: 18px;\n min-width: 18px;\n cursor: pointer;\n background-color: #ffffffb3;\n color: #a1a1a1;\n border-radius: 100%;\n text-align: center;\n height: 18px;\n vertical-align: middle;\n font-size: 12px;\n}\n.message-container message-element.error {\n background-color: #ffe6e6;\n color: #c70208;\n border: 1px solid #ff6b6ded;\n}\n.message-container message-element.info {\n background-color: #c9e7ff;\n color: #004d95;\n border: 1px solid #1890ff69;\n}\n.message-container message-element.success {\n background-color: #e8ffe0;\n color: #3e8d0d;\n border: 1px solid #6fd91d;\n}\n.message-container message-element.warn {\n background-color: #ffefc8;\n color: #9b7400;\n border: 1px solid #ffc107;\n}\nmodal-element {\n position: absolute;\n top: 50%;\n left: 50%;\n background-color: white;\n border-radius: 4px;\n box-shadow: 0px 0px 24px -12px black;\n border: 1px solid #929292;\n height: fit-content;\n transform: translate(-50%, -50%);\n padding: 12px 18px 18px 18px;\n font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;\n z-index: 99999999999;\n line-height: 24px;\n}\nmodal-element .modal-profile {\n zoom: 0.9;\n color: #969696;\n user-select: none;\n margin-bottom: 4px;\n}\nmodal-element .modal-title {\n font-size: 18px;\n font-weight: bold;\n user-select: none;\n}\nmodal-element .modal-body {\n margin: 12px 0px;\n overflow: auto;\n}\nmodal-element .modal-footer {\n display: flex;\n white-space: nowrap;\n justify-content: end;\n align-items: end;\n}\nmodal-element .modal-footer > * + * {\n margin-left: 12px;\n}\nmodal-element .modal-input {\n outline: none;\n padding: 2px 8px;\n margin: 0px;\n background-color: #eef2f7;\n border-radius: 2px;\n color: black;\n border: 1px solid #ffffff00;\n width: -webkit-fill-available;\n}\nmodal-element .modal-input::placeholder {\n color: #bababa;\n}\nmodal-element .modal-input:focus {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n}\nmodal-element .modal-input:focus:not([type='checkbox'], [type='radio']) {\n border: 1px solid #0e8de290;\n box-shadow: 0px 0px 4px #0e8de252;\n background-color: white !important;\n}\nmodal-element .modal-input:hover {\n background-color: #ebeef4;\n}\nmodal-element .modal-cancel-button {\n appearance: none;\n -moz-appearance: none;\n -webkit-appearance: none;\n border-radius: 4px;\n border: 1px solid #2c92ff;\n color: #409eff;\n cursor: pointer !important;\n margin-bottom: 4px;\n color: gray;\n background-color: white;\n border: 1px solid #dcdcdc;\n}\nmodal-element .modal-cancel-button:active {\n box-shadow: 0px 0px 8px #0e8de2a5;\n}\nmodal-element .modal-cancel-button + modal-element .modal-cancel-button {\n margin-left: 12px;\n}\nmodal-element .modal-cancel-button:hover {\n background-color: #7abbff24;\n}\nmodal-element .modal-cancel-button.danger:hover {\n background-color: #ffdede86;\n}\nmodal-element .modal-cancel-button:disabled {\n background-color: white;\n border: 1px solid #c0c0c0;\n color: #aeaeae;\n cursor: not-allowed;\n}\nmodal-element .modal-cancel-button.danger {\n color: #f36669;\n border-color: #dd5a5d;\n}\nmodal-element .modal-cancel-button:disabled:active {\n box-shadow: none;\n}\nmodal-element .modal-confirm-button {\n appearance: none;\n -moz-appearance: none;\n -webkit-appearance: none;\n border-radius: 4px;\n background-color: white;\n border: 1px solid #2c92ff;\n color: #409eff;\n cursor: pointer !important;\n margin-bottom: 4px;\n}\nmodal-element .modal-confirm-button:active {\n box-shadow: 0px 0px 8px #0e8de2a5;\n}\nmodal-element .modal-confirm-button + modal-element .modal-confirm-button {\n margin-left: 12px;\n}\nmodal-element .modal-confirm-button:hover {\n background-color: #7abbff24;\n}\nmodal-element .modal-confirm-button.danger:hover {\n background-color: #ffdede86;\n}\nmodal-element .modal-confirm-button:disabled {\n background-color: white;\n border: 1px solid #c0c0c0;\n color: #aeaeae;\n cursor: not-allowed;\n}\nmodal-element .modal-confirm-button.danger {\n color: #f36669;\n border-color: #dd5a5d;\n}\nmodal-element .modal-confirm-button:disabled:active {\n box-shadow: none;\n}\nmodal-element.alert .modal-input,\nmodal-element.alert .modal-cancel-button {\n display: none;\n}\nmodal-element.alert .modal-confirm-button {\n margin: 0;\n}\nmodal-element.prompt .modal-input,\nmodal-element.prompt .modal-cancel-button,\nmodal-element.prompt .modal-confirm-button {\n display: block;\n}\nmodal-element.confirm .modal-input {\n display: none;\n}\n.modal-wrapper {\n width: 100%;\n height: 100%;\n z-index: 9999;\n position: fixed;\n top: 0px;\n left: 0px;\n z-index: 9999999;\n background-color: rgba(0, 0, 0, 0.265);\n color: #636363;\n font: 14px Menlo, Monaco, Consolas, 'Courier New', monospace;\n}\n.pointer {\n cursor: pointer;\n}\n.separator {\n display: flex;\n align-items: center;\n text-align: center;\n padding: 4px 0px 8px 0px;\n}\n.separator::before,\n.separator::after {\n content: '';\n flex: 1;\n border-bottom: 1px solid #63636346;\n}\n.separator:not(:empty)::before {\n margin-right: 0.25em;\n}\n.separator:not(:empty)::after {\n margin-left: 0.25em;\n}\ncontainer-element.minimize .body,\ncontainer-element.minimize header-element .dropdown,\ncontainer-element.minimize .footer {\n display: none;\n}\ncontainer-element.minimize header-element {\n padding: 8px;\n border-radius: 8px;\n box-shadow: 0px 0px 24px -12px black;\n}\n.user-guide > li {\n padding: 4px 0px;\n}\n.search-infos-num {\n width: 26px;\n margin: 2px;\n height: 20px;\n border-radius: 4px;\n display: inline-block;\n background-color: white;\n text-align: center;\n cursor: pointer;\n border: 1px solid #b6b6b6;\n}\n.search-infos-num.requested {\n border: 1px solid #63b4ff;\n color: #63b4ff;\n}\n.search-infos-num.active {\n background-color: #127de1 !important;\n color: white;\n}\n.search-infos-num.error {\n border: 1px solid #ff8789ed;\n background-color: #ff6b6ded;\n color: white;\n}\n.search-infos-num.finish {\n background-color: #63b4ff;\n border: 1px solid #63b4ff;\n color: white;\n}\nsearch-infos-element {\n display: block;\n overflow: auto;\n}\nsearch-infos-element .search-result {\n margin-bottom: 12px;\n padding-left: 12px;\n}\nsearch-infos-element .search-result .question {\n font-weight: bold;\n max-height: 200px;\n overflow: auto;\n}\nsearch-infos-element .search-result .answer {\n color: #7c7c7c;\n}\nsearch-infos-element .search-result .answer code {\n border-bottom: 1px solid #dcdcdc;\n padding: 2px 0px;\n border-radius: 2px;\n margin: 4px;\n line-height: 22px;\n}\nsearch-infos-element .search-result .answer code + code {\n margin-left: 4px;\n}\nsearch-infos-element .search-result .search-result-answer-tag {\n padding: 2px 6px;\n border-radius: 2px;\n font-size: 12px;\n cursor: pointer;\n margin-right: 6px;\n}\nsearch-infos-element .search-result .search-result-answer-tag + .search-result-answer-tag {\n margin-left: 4px;\n}\nsearch-infos-element .search-result .search-result-answer-tag.blue {\n background-color: #e6f7ff;\n border: 1px solid #91d5ff;\n color: #1890ff;\n}\nsearch-infos-element .search-result .search-result-answer-tag.green {\n background-color: #f6ffed;\n border: 1px solid #b7eb8f;\n color: #52c41a;\n}\nsearch-infos-element .search-result .search-result-answer-tag.gray {\n background-color: #fafafa;\n border: 1px solid #d9d9d9;\n color: #595959;\n}\nsearch-infos-element .search-result .search-result-answer-tag.red {\n background-color: #fff1f0;\n border: 1px solid #ffa39e;\n color: #ff4d4f;\n}\nsearch-infos-element .search-result .search-result-answer-tag.yellow {\n background-color: #fffbe6;\n border: 1px solid #ffe58f;\n color: #faad14;\n}\nsearch-infos-element .search-result-question-type {\n background-color: #e6f7ff;\n border: 1px solid #91d5ff;\n color: #1890ff;\n margin-right: 8px;\n padding: 0px 4px;\n border-radius: 4px;\n}\nsearch-infos-element .error {\n color: #ff6b6ded;\n display: inline-block;\n padding-left: 12px;\n}\n.copy,\n.question-title-extra-btn {\n margin-left: 4px;\n padding: 2px 4px;\n border-radius: 2px;\n box-shadow: 0 0 4px #b1b1b1;\n cursor: pointer !important;\n font-weight: normal;\n font-size: 12px;\n}\n.work-result-question-container {\n position: absolute;\n width: 400px;\n left: -100%;\n top: 0px;\n background: white;\n border: 1px solid #cbcbcb;\n border-radius: 4px;\n box-shadow: 0px 0px 12px #d1cfcf;\n padding: 12px;\n}\n.work-result-question-container .close-search-result {\n font-size: 12px;\n margin-left: 8px;\n text-decoration: underline;\n color: gray;\n cursor: pointer;\n}\n.work-result-list {\n max-height: 400px;\n overflow: auto;\n margin: 12px 0px;\n padding: 6px;\n border: 1px solid #e1e1e1;\n border-radius: 4px;\n}\n.search-info-title {\n border: 1px solid #e1e1e1;\n border-radius: 4px;\n padding: 8px 12px;\n margin-bottom: 12px;\n line-height: 20px;\n overflow: auto;\n}\n.search-info-title .title-content {\n max-height: 200px;\n overflow: auto;\n}\n.search-info-details {\n margin-left: 4px;\n}\n.console {\n max-height: 300px;\n max-width: 400px;\n overflow: auto;\n background-color: #292929;\n padding: 12px 6px;\n color: #ececec;\n font-size: 12px;\n}\n.console .item {\n padding: 3px 0px;\n border-radius: 2px;\n}\n.console .item .time {\n color: #757575;\n}\n.console .item .info {\n background-color: #2196f3a3;\n}\n.console .item .warn {\n background-color: #ffc107db;\n}\n.console .item .error {\n background-color: #f36c71cc;\n}\n.console .item .debug,\n.console .item .log {\n background-color: #9e9e9ec4;\n}\n.console *::selection {\n background-color: #ffffff6b;\n}\n.markdown {\n max-width: 400px;\n max-height: 50vh;\n overflow: auto;\n}\n.markdown code {\n padding: 2px 4px;\n background-color: #f0f0f0;\n border-radius: 6px;\n font-size: 12px;\n}\n.markdown blockquote {\n padding: 4px 4px 4px 12px;\n margin: 0px;\n color: #b5b5b5;\n border-left: #ababab 2px solid;\n}\n.markdown blockquote p {\n margin: 0px;\n}\n.markdown h1,\n.markdown h2,\n.markdown h3,\n.markdown h4,\n.markdown h5,\n.markdown h6,\n.markdown p {\n margin: 8px 0px;\n}\n.dropdown {\n position: relative;\n display: inline-block;\n}\n.dropdown.active .dropdown-trigger-element {\n color: #1890ff;\n}\n.dropdown-trigger-element {\n cursor: pointer;\n}\n.dropdown-content {\n display: none;\n position: absolute;\n background-color: #ffffff;\n overflow: auto;\n box-shadow: 0px 8px 16px 0px #00000033;\n z-index: 1;\n border-radius: 4px;\n padding: 8px 12px;\n min-width: 120px;\n}\n.dropdown-content.show {\n display: block;\n}\n.dropdown-content {\n cursor: pointer;\n z-index: 999;\n}\n.dropdown-content .dropdown-option {\n padding-left: 4px;\n white-space: nowrap;\n}\n.dropdown-content .dropdown-option:hover {\n background-color: #f3f3f3;\n}\n.dropdown-content .dropdown-option.active {\n background-color: #1890ff1a;\n color: #1890ff;\n}\n.space {\n display: inline-flex;\n}\n.config-details {\n animation: fade-in 0.5s;\n}\n.config-details label {\n padding-left: 12px;\n}\n.alert-info-wrapper {\n margin-bottom: 8px;\n}\n.alert-info-wrapper .result-info {\n padding: 12px;\n text-align: center;\n border-radius: 6px;\n}\n.alert-info-wrapper .unresolved {\n color: #a1a1a1;\n background-color: #f7f7f7;\n}\n.alert-info-wrapper .no-answer {\n color: #a1a1a1;\n background-color: #f7f7f7;\n}\n.alert-info-wrapper .error {\n color: #ff4d4f;\n background-color: #fff1f0;\n}\nmessage-element {\n animation: show 0.5s;\n}\nscript-panel-element > div,\nscript-panel-link,\ncontainer-element,\nmodal-element {\n animation: fade-in 0.3s;\n}\n@keyframes show {\n 0% {\n transform: translateY(20px);\n opacity: 0;\n }\n 100% {\n transform: translateY(0);\n opacity: 1;\n }\n}\n@keyframes fade-in {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n}\n@keyframes fade-out {\n 0% {\n opacity: 1;\n }\n 100% {\n opacity: 0;\n }\n}\n.checkbox-label {\n display: inline-block !important;\n position: relative;\n cursor: pointer;\n font-size: 16px;\n color: #2c3e50;\n}\n/* 隐藏原始复选框 */\n.checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n}\n/* 自定义按钮样式 */\n.checkbox-label::after {\n content: '';\n display: inline-block;\n border-radius: 50%;\n transition: all 0.1s ease;\n position: relative;\n margin-left: 4px;\n vertical-align: middle;\n}\n/* 向下箭头(未选中状态) */\n.checkbox-label::before {\n content: '🔽';\n position: absolute;\n width: 0;\n height: 0;\n right: 8px;\n transition: all 0.1s ease;\n z-index: 2;\n}\n/* 向上箭头(选中状态) */\n.checked .checkbox-label::before {\n content: '🔼';\n}\n";
//#endregion
//#region src/main.ts
/**
* 入口。
*
* metadata 块不在这里 —— 由 vite-userscript-plugin 的 header 配置生成(见 vite.config.ts)。
* easy-us 只管运行时,两者不冲突。
*/
/**
* 面板初次打开时默认展开哪个脚本。
*
* ⚠️ 这里只能填 `script.namespace`,或者 `工程名-脚本名` 这个完整写法。
* easy-us 的 isCurrentPanel 就认这两种(interfaces/custom-window.js):
*
* projectName + '-' + script.name === currentPanelName || script.namespace === currentPanelName
*
* 以前填的是光秃秃的 '雨课堂助手'(工程名),两边都匹配不上,
* 结果是窗口只剩一条标题栏、底下什么都不画 —— 看着就像「面板不见了」。
*
* 顺手按当前页挑一个最相关的:作业页开 AI 答题,课程页开作业跳转,其余开复制解密。
* 用户自己在下拉里选过之后,框架会把选择存进 tab,这里的默认值就不再起作用了。
*/
function defaultPanel() {
const u = location.href;
if (/exercise/i.test(u)) return "ykt.answer";
if (/\/ai-workspace\/lms-graph/.test(u)) return "ykt.jump";
return "ykt.copy";
}
/**
* 窗口设置脚本(字体大小 / 连点次数 / 面板坐标 / 显隐状态都存在它的 cfg 里)。
*
* ⚠️ 必须自己补一次 createConfigProxy。
* easy-us 的 start() 只给 `projects` 里的脚本挂配置代理(utils/start.js 开头那段 map),
* 而 renderScript 不在 projects 里,于是它的 cfg 一直是个空对象 {} ——
* 读出来的 x / y / visual / fontsize 全是 undefined,
* 结果 handlePosition 会写出 `style.left = "undefinedpx"`(非法,被浏览器丢掉),
* 拖动位置也存不住,「字体大小」那一栏更是调了没反应。
*/
var renderScript = (0, import_lib.createRenderScript)({ name: "🖼️ 窗口设置" });
renderScript.cfg = import_lib.$.createConfigProxy(renderScript);
/**
* 关掉「连点三下把面板挪到鼠标位置」。
*
* easy-us 在 window 上挂了个 click 监听(interfaces/custom-window.js 的 handleVisible):
*
* if (e.detail === Math.max(config.render.switchPoint, 3)) { 移动面板 + setVisual('normal') }
*
* e.detail 是连击计数,双击选词之后再点一下就满三次 —— 在答题页上误触率很高,
* 面板会毫无预兆地跳到鼠标底下。监听器是匿名函数,摘不掉,
* 只能把阈值抬到一个够不着的数(Math.max 的下限是 3,所以只能往上顶)。
*
* 阈值是在 CustomWindow 构造时从 cfg 里读走的,所以必须赶在 start() 之前写。
* 键盘的 Ctrl+O 显示 / 隐藏不受影响,还留着。
*/
var NO_TRIPLE_CLICK = 999;
renderScript.cfg.switchPoint = NO_TRIPLE_CLICK;
var rsConfigs = renderScript.configs;
if (rsConfigs.switchPoint) {
rsConfigs.switchPoint.label = "连点唤出面板(已关闭)";
rsConfigs.switchPoint.attrs = {
type: "number",
min: 3,
max: NO_TRIPLE_CLICK,
step: 1,
title: "默认连点三下会把面板挪到鼠标位置,答题时太容易误触,这里已经关掉。想用就改回 3。"
};
}
(0, import_lib.start)({
projects: [yktProject],
renderConfig: {
renderScript,
title: "☂️",
styles: [panel_default],
defaultPanelName: defaultPanel()
}
});
watchPanel();
//#endregion
})();