多AI模型支持(全手动输入版) + 题库右上角关闭 + 模块化架构 + 性能优化
// ==UserScript==
// @name 传智教育满分脚本-多AI增强版 2025.11.22
// @namespace https://stu.ityxb.com/
// @version 13.6
// @description 多AI模型支持(全手动输入版) + 题库右上角关闭 + 模块化架构 + 性能优化
// @author 多AI增强版
// @match https://stu.ityxb.com/*
// @connect tk.enncy.cn
// @connect api.openai.com
// @connect fyra.im
// @connect api.anthropic.com
// @connect generativelanguage.googleapis.com
// @connect api.deepseek.com
// @connect burn.hair
// @connect api.chatanywhere.com.cn
// @connect openrouter.ai
// @connect ollama.com
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// ================ AI 模型配置 (无预设列表,全手动) ================
const AI_MODELS = {
openai: {
name: "OpenAI (GPT)",
endpoint: "https://api.openai.com/v1/chat/completions",
defaultModel: "gpt-4o-mini", // 仅作为建议默认值
authType: "Bearer",
formatRequest: (config, question) => ({
model: config.ai_model,
temperature: 0.1,
max_tokens: 500,
messages: [
{
role: "system",
content: "你是专业答题助手。直接给出准确答案,不要解释。多个答案用#分隔。",
},
{ role: "user", content: question },
],
}),
parseResponse: (data) => data.choices[0].message.content.trim(),
},
claude: {
name: "Claude (Anthropic)",
endpoint: "https://api.anthropic.com/v1/messages",
defaultModel: "claude-3-5-sonnet-20241022", // 仅作为建议默认值
authType: "x-api-key",
formatRequest: (config, question) => ({
model: config.ai_model,
max_tokens: 500,
messages: [
{
role: "user",
content:
"你是专业答题助手。直接给出准确答案,不要解释。多个答案用#分隔。\n\n" +
question,
},
],
}),
parseResponse: (data) => data.content[0].text.trim(),
extraHeaders: (config) => ({
"anthropic-version": "2023-06-01",
}),
},
gemini: {
name: "Google Gemini",
// 注意: URL包含 {model} 占位符
endpoint:
"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent",
defaultModel: "gemini-2.0-flash-exp", // 仅作为建议默认值
authType: "query",
formatRequest: (config, question) => ({
contents: [
{
parts: [
{
text:
"你是专业答题助手。直接给出准确答案,不要解释。多个答案用#分隔。\n\n" +
question,
},
],
},
],
generationConfig: {
temperature: 0.1,
maxOutputTokens: 500,
},
}),
parseResponse: (data) =>
data.candidates[0].content.parts[0].text.trim(),
buildUrl: (config) => {
// 支持用户自定义URL,若URL含{model}则替换
let url = config.ai_url;
if (url.includes("{model}")) {
url = url.replace("{model}", config.ai_model);
}
return `${url}?key=${config.ai_key}`;
},
},
deepseek: {
name: "DeepSeek",
endpoint: "https://api.deepseek.com/chat/completions", // 官方路径
defaultModel: "deepseek-chat",
authType: "Bearer",
formatRequest: (config, question) => ({
model: config.ai_model,
temperature: 0.1,
max_tokens: 500,
messages: [
{
role: "system",
content: "你是专业答题助手。直接给出准确答案,不要解释。多个答案用#分隔。",
},
{ role: "user", content: question },
],
}),
parseResponse: (data) => data.choices[0].message.content.trim(),
},
custom: {
name: "自定义 API",
endpoint: "",
defaultModel: "custom-model",
authType: "Bearer",
formatRequest: (config, question) => ({
model: config.ai_model,
temperature: 0.1,
max_tokens: 500,
messages: [
{
role: "system",
content: "你是专业答题助手。直接给出准确答案,不要解释。多个答案用#分隔。",
},
{ role: "user", content: question },
],
}),
parseResponse: (data) => {
// 尝试多种解析格式
if (data.choices && data.choices[0] && data.choices[0].message) {
return data.choices[0].message.content.trim();
}
if (data.content && data.content[0] && data.content[0].text) {
return data.content[0].text.trim();
}
if (data.response) {
return data.response.trim();
}
throw new Error("无法解析响应格式");
},
},
};
// ================ 工具函数模块 ================
const Utils = {
sanitizeHTML(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
},
normalizeText(text) {
return text
.replace(/^[A-Z]\.?\s*/, "")
.replace(/[\s\n\r\t]+/g, "")
.toLowerCase()
.trim();
},
throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return fn.apply(this, args);
}
};
},
debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
},
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
},
encrypt(text, salt = "chuanzhi_v13") {
try {
return btoa(
text
.split("")
.map((c, i) =>
String.fromCharCode(
c.charCodeAt(0) ^ salt.charCodeAt(i % salt.length)
)
)
.join("")
);
} catch (e) {
return text;
}
},
decrypt(encrypted, salt = "chuanzhi_v13") {
try {
return atob(encrypted)
.split("")
.map((c, i) =>
String.fromCharCode(
c.charCodeAt(0) ^ salt.charCodeAt(i % salt.length)
)
)
.join("");
} catch (e) {
return encrypted;
}
},
};
// ================ 缓存管理模块 ================
const CacheManager = {
MAX_SIZE: 1000,
EXPIRE_DAYS: 30,
get(key) {
const cache = GM_getValue("tiku_cache_v13", {});
const item = cache[key];
if (!item) return null;
if (item.timestamp) {
const now = Date.now();
const expireTime = this.EXPIRE_DAYS * 86400000;
if (now - item.timestamp > expireTime) {
delete cache[key];
GM_setValue("tiku_cache_v13", cache);
return null;
}
}
return item.answer;
},
set(key, answer) {
const cache = GM_getValue("tiku_cache_v13", {});
if (Object.keys(cache).length >= this.MAX_SIZE) {
const entries = Object.entries(cache)
.sort((a, b) => (a[1].timestamp || 0) - (b[1].timestamp || 0))
.slice(Math.floor(this.MAX_SIZE * 0.2));
const newCache = {};
entries.forEach(([k, v]) => (newCache[k] = v));
GM_setValue("tiku_cache_v13", newCache);
}
cache[key] = {
answer: answer,
timestamp: Date.now(),
};
GM_setValue("tiku_cache_v13", cache);
},
clear() {
GM_setValue("tiku_cache_v13", {});
},
getSize() {
return Object.keys(GM_getValue("tiku_cache_v13", {})).length;
},
};
// ================ 配置管理模块 ================
const ConfigManager = {
DEFAULT_CONFIG: {
// AI配置
ai_enabled: false,
ai_provider: "openai", // openai, claude, gemini, deepseek, custom
ai_key: "",
ai_url: "https://api.openai.com/v1/chat/completions",
ai_model: "gpt-4o-mini",
// 题库配置
banks: [
{
name: "言溪题库",
enabled: false,
homepage: "https://tk.enncy.cn/",
url: "https://tk.enncy.cn/query",
method: "GET",
token: "",
},
],
logLevel: "INFO",
},
load() {
// 为避免用户升级后配置丢失, 仍使用原存储 key
return GM_getValue("chuanzhi_config_v13_5", this.DEFAULT_CONFIG);
},
save(config) {
const saveConfig = JSON.parse(JSON.stringify(config));
if (saveConfig.ai_key) {
saveConfig.ai_key = Utils.encrypt(saveConfig.ai_key);
}
saveConfig.banks.forEach((bank) => {
if (bank.token) {
bank.token = Utils.encrypt(bank.token);
}
});
GM_setValue("chuanzhi_config_v13_5", saveConfig);
},
decrypt(config) {
if (config.ai_key) {
config.ai_key = Utils.decrypt(config.ai_key);
}
config.banks.forEach((bank) => {
if (bank.token) {
bank.token = Utils.decrypt(bank.token);
}
});
return config;
},
validate(config) {
const errors = [];
if (config.ai_enabled) {
if (!config.ai_key || config.ai_key.length < 5) {
errors.push("AI API Key 格式不正确(至少5个字符)");
}
if (!config.ai_model) {
errors.push("AI 模型名称不能为空");
}
}
config.banks.forEach((bank, i) => {
if (bank.enabled) {
if (!bank.url || !bank.url.match(/^https?:\/\/.+/)) {
errors.push(`题库 ${i + 1} URL 格式不正确`);
}
}
});
return errors;
},
export() {
const config = this.load();
const data = JSON.stringify(config, null, 2);
const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `chuanzhi_config_${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
},
import(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const config = JSON.parse(e.target.result);
const errors = this.validate(config);
if (errors.length > 0) {
reject(new Error(errors.join("\n")));
} else {
this.save(config);
resolve(config);
}
} catch (error) {
reject(new Error("配置文件格式错误"));
}
};
reader.onerror = () => reject(new Error("文件读取失败"));
reader.readAsText(file);
});
},
};
// ================ 日志管理模块 ================
const Logger = {
LEVELS: { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 },
currentLevel: 1,
maxLogs: 100,
logs: [],
init(level = "INFO") {
this.currentLevel = this.LEVELS[level] || this.LEVELS.INFO;
},
log(msg, level = "INFO") {
const levelValue = this.LEVELS[level] || this.LEVELS.INFO;
if (levelValue < this.currentLevel) return;
const logDiv = document.getElementById("fix_log");
if (!logDiv) return;
const colors = {
DEBUG: "#999",
INFO: "#0ff",
WARN: "#ff0",
ERROR: "#f00",
SUCCESS: "#0f0",
CACHE: "#f0f",
};
const entry = document.createElement("div");
entry.className = "log-e";
const time = new Date().toLocaleTimeString("zh-CN", { hour12: false });
entry.innerHTML = `
<span style="color:${colors[level] || "#0ff"};">[${time}] [${level}]</span>
${Utils.sanitizeHTML(msg)}
`;
logDiv.insertBefore(entry, logDiv.firstChild);
while (logDiv.children.length > this.maxLogs) {
logDiv.removeChild(logDiv.lastChild);
}
const consoleMethod =
level === "ERROR" ? "error" : level === "WARN" ? "warn" : "log";
console[consoleMethod](`[传智助手] ${msg}`);
this.logs.push({ time, level, msg });
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
},
debug(msg) {
this.log(msg, "DEBUG");
},
info(msg) {
this.log(msg, "INFO");
},
warn(msg) {
this.log(msg, "WARN");
},
error(msg) {
this.log(msg, "ERROR");
},
success(msg) {
this.log(msg, "SUCCESS");
},
cache(msg) {
this.log(msg, "CACHE");
},
export() {
const content = this.logs
.map((log) => `[${log.time}] [${log.level}] ${log.msg}`)
.join("\n");
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `chuanzhi_logs_${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
},
clear() {
const logDiv = document.getElementById("fix_log");
if (logDiv) logDiv.innerHTML = "";
this.logs = [];
},
};
// ================ 请求限流器 ================
const RateLimiter = {
requests: [],
maxRequests: 10,
timeWindow: 60000,
async throttle() {
const now = Date.now();
this.requests = this.requests.filter((t) => now - t < this.timeWindow);
if (this.requests.length >= this.maxRequests) {
const waitTime = this.timeWindow - (now - this.requests[0]);
Logger.warn(`API限流:等待 ${Math.ceil(waitTime / 1000)} 秒`);
await Utils.sleep(waitTime);
}
this.requests.push(Date.now());
},
};
// ================ API 客户端模块 ================
const APIClient = {
async requestWithRetry(options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await this.request(options);
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.pow(2, i) * 1000;
Logger.warn(`请求失败,${delay}ms 后重试 (${i + 1}/${maxRetries})`);
await Utils.sleep(delay);
}
}
},
request(options) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
...options,
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve(response);
} else {
reject(
new Error(`HTTP ${response.status}: ${response.statusText}`)
);
}
},
onerror: (error) =>
reject(new Error(`网络错误: ${error.error || "未知错误"}`)),
ontimeout: () => reject(new Error("请求超时")),
});
});
},
async queryBank(bank, question, options, type) {
await RateLimiter.throttle();
let url = bank.url;
const params = {
token: bank.token || "",
title: question,
options: options,
type: type,
};
if (bank.method === "GET") {
const query = new URLSearchParams(params);
url += (url.includes("?") ? "&" : "?") + query.toString();
}
const response = await this.request({
method: bank.method || "GET",
url: url,
headers: { "Content-Type": "application/json" },
data: bank.method === "POST" ? JSON.stringify(params) : undefined,
timeout: 10000,
});
const data = JSON.parse(response.responseText);
let answer = null;
if (data.code === 0 && data.data) {
answer = data.data.answer || data.data;
} else if (data.answer) {
answer = data.answer;
} else if (data.data) {
answer = data.data;
}
if (answer && typeof answer === "string" && answer.length > 0) {
return answer;
}
throw new Error("未找到答案");
},
async queryAI(config, question) {
await RateLimiter.throttle();
const provider = AI_MODELS[config.ai_provider] || AI_MODELS.openai;
const requestData = provider.formatRequest(config, question);
// 构建请求头
const headers = {
"Content-Type": "application/json",
};
// 根据不同的认证类型设置请求头
if (provider.authType === "Bearer") {
headers.Authorization = `Bearer ${config.ai_key}`;
} else if (provider.authType === "x-api-key") {
headers["x-api-key"] = config.ai_key;
}
// 添加额外的请求头
if (provider.extraHeaders) {
Object.assign(headers, provider.extraHeaders(config));
}
// 构建请求URL
let url = config.ai_url;
if (provider.buildUrl) {
url = provider.buildUrl(config);
}
const response = await this.request({
method: "POST",
url: url,
headers: headers,
data: JSON.stringify(requestData),
timeout: 30000,
});
const data = JSON.parse(response.responseText);
return provider.parseResponse(data);
},
};
// ================ 题目处理模块 ================
const QuestionProcessor = {
config: null,
elementCache: new WeakMap(),
init(config) {
this.config = config;
},
detectQuestions() {
const selectors = [
".questionItem",
".question-item-box",
".question-item",
".item-box",
];
for (const sel of selectors) {
const questions = document.querySelectorAll(sel);
if (questions.length > 0) {
Logger.debug(`使用选择器: ${sel}`);
return Array.from(questions);
}
}
return [];
},
extractQuestionText(element) {
const selectors = [
".question-title-box .myEditorTxt",
".stem",
".title",
".question-title",
".question-stem",
];
for (const sel of selectors) {
const el = element.querySelector(sel);
if (el && el.innerText.trim()) {
return el.innerText.trim();
}
}
const lines = element.innerText.split("\n").filter((l) => l.trim());
return lines[0] || "";
},
extractOptions(element) {
const optionElements = element.querySelectorAll(
".option, .radio_item, label"
);
const options = [];
optionElements.forEach((opt) => {
const text = opt.innerText.trim();
if (text) options.push(text);
});
return options.join("###");
},
detectQuestionType(element) {
const text = element.innerText;
if (
text.includes("单选") ||
element.querySelectorAll('input[type="radio"]').length > 0
) {
return "0";
}
if (
text.includes("多选") ||
element.querySelectorAll('input[type="checkbox"]').length > 0
) {
return "1";
}
if (text.includes("判断")) {
return "3";
}
if (
text.includes("填空") ||
element.querySelectorAll('input[type="text"]').length > 0
) {
return "2";
}
return "0";
},
async processQuestion(element, num, total) {
try {
if (element.querySelector(".answer-mark")) {
Logger.debug(`第${num}题已处理,跳过`);
return { status: "skipped", num };
}
const questionText = this.extractQuestionText(element);
if (!questionText || questionText.length < 5) {
Logger.warn(`第${num}题无法提取题目文本`);
return { status: "failed", num, reason: "无法提取题目" };
}
Logger.info(`第${num}题: ${questionText.substring(0, 40)}...`);
const cached = CacheManager.get(questionText);
if (cached) {
this.fillAnswer(element, cached, "缓存", num);
Logger.cache(`第${num}题 [缓存] ${cached}`);
return { status: "success", num, source: "cache", answer: cached };
}
const options = this.extractOptions(element);
const type = this.detectQuestionType(element);
const enabledBanks = this.config.banks.filter((b) => b.enabled);
if (enabledBanks.length > 0) {
for (const bank of enabledBanks) {
try {
const answer = await APIClient.queryBank(
bank,
questionText,
options,
type
);
CacheManager.set(questionText, answer);
this.fillAnswer(element, answer, bank.name, num);
Logger.success(`第${num}题 [${bank.name}] ${answer}`);
return {
status: "success",
num,
source: bank.name,
answer,
};
} catch (error) {
Logger.debug(
`第${num}题 [${bank.name}] 未找到: ${error.message}`
);
}
}
}
if (this.config.ai_enabled && this.config.ai_key) {
const providerName =
AI_MODELS[this.config.ai_provider]?.name || "AI";
Logger.info(`第${num}题 使用${providerName}答题`);
const answer = await APIClient.queryAI(this.config, questionText);
CacheManager.set(questionText, answer);
this.fillAnswer(element, answer, providerName, num);
Logger.success(`第${num}题 [${providerName}] ${answer}`);
return { status: "success", num, source: providerName, answer };
}
Logger.error(`第${num}题 所有方式均未找到答案`);
return { status: "failed", num, reason: "未找到答案" };
} catch (error) {
Logger.error(`第${num}题 处理异常: ${error.message}`);
return { status: "error", num, error: error.message };
}
},
fillAnswer(element, answer, source, num) {
const mark = document.createElement("div");
mark.className = "answer-mark";
mark.textContent = `✅ [${source}] 答案: ${answer}`;
const titleBox = element.querySelector(
".question-title-box, .stem, .title"
);
if (titleBox) {
titleBox.appendChild(mark);
} else {
element.insertBefore(mark, element.firstChild);
}
const answers = answer
.split("#")
.map((a) => a.trim())
.filter((a) => a);
this.fillChoiceAnswer(element, answers);
this.fillBlankAnswer(element, answers);
},
fillChoiceAnswer(element, answers) {
const options = element.querySelectorAll("label, .option, .radio_item");
options.forEach((option) => {
let optionText = (option.innerText || "").trim();
optionText = Utils.normalizeText(optionText);
answers.forEach((ans) => {
const cleanAns = Utils.normalizeText(ans);
if (
optionText.includes(cleanAns) ||
cleanAns.includes(optionText) ||
optionText === cleanAns
) {
setTimeout(() => {
option.click();
const input = option.querySelector("input");
if (input) {
input.checked = true;
input.dispatchEvent(new Event("change", { bubbles: true }));
}
option.dispatchEvent(
new MouseEvent("click", {
bubbles: true,
cancelable: true,
})
);
}, 100);
}
});
});
},
fillBlankAnswer(element, answers) {
const inputs = element.querySelectorAll('input[type="text"], textarea');
inputs.forEach((input, i) => {
if (answers[i]) {
input.value = answers[i];
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
}
});
},
};
// ================ 样式定义 ================
GM_addStyle(`
#FIX_PANEL {
position: fixed;
top: 10px;
right: 10px;
background: linear-gradient(135deg, rgba(0,0,0,0.95), rgba(20,20,20,0.95));
color: #0f0;
border: 3px solid #0f0;
border-radius: 15px;
padding: 0;
z-index: 999999999;
box-shadow: 0 0 60px rgba(0,255,0,0.5);
font-family: 'Consolas', 'Monaco', monospace;
width: 450px;
min-width: 300px;
max-width: 800px;
height: auto;
min-height: 200px;
max-height: 90vh;
overflow: auto;
cursor: move;
}
@media (max-width: 768px) {
#FIX_PANEL {
width: 90% !important;
max-width: 350px;
}
#FIX_CFG {
width: 90% !important;
padding: 15px;
}
}
#panel_header {
background: linear-gradient(135deg, #0f0, #00aa00);
color: #000;
padding: 12px 20px;
border-radius: 12px 12px 0 0;
cursor: move;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
}
#panel_content {
padding: 20px;
overflow-y: auto;
max-height: calc(90vh - 60px);
}
#minimize_btn {
background: #ff0;
color: #000;
border: none;
width: 25px;
height: 25px;
border-radius: 50%;
cursor: pointer;
font-weight: bold;
font-size: 18px;
line-height: 1;
transition: all 0.3s;
}
#minimize_btn:hover {
background: #ffff00;
transform: scale(1.1);
}
#FIX_PANEL.minimized {
width: 200px !important;
height: 50px !important;
}
#FIX_PANEL.minimized #panel_content {
display: none;
}
#fix_log {
max-height: 40vh;
overflow-y: auto;
margin: 15px 0;
padding-right: 5px;
}
#fix_log::-webkit-scrollbar,
#FIX_PANEL::-webkit-scrollbar,
#FIX_CFG::-webkit-scrollbar {
width: 6px;
}
#fix_log::-webkit-scrollbar-thumb,
#FIX_PANEL::-webkit-scrollbar-thumb,
#FIX_CFG::-webkit-scrollbar-thumb {
background: #0f0;
border-radius: 3px;
}
.log-e {
margin: 8px 0;
padding: 10px;
background: rgba(0, 255, 0, 0.08);
border-radius: 8px;
border-left: 4px solid #0f0;
font-size: 13px;
line-height: 1.5;
word-break: break-all;
}
.cfg-btn {
background: linear-gradient(135deg, #0f0, #00cc00) !important;
color: #000 !important;
padding: 12px 20px !important;
border: none !important;
border-radius: 10px !important;
cursor: pointer !important;
font-weight: bold !important;
font-size: 15px !important;
margin: 8px 0 !important;
width: 100%;
transition: all 0.3s;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.cfg-btn:hover {
background: linear-gradient(135deg, #00ff00, #0f0) !important;
box-shadow: 0 0 25px rgba(0,255,0,0.6);
transform: translateY(-2px);
}
.cfg-btn:active {
transform: translateY(0);
}
.cfg-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
#FIX_CFG {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: linear-gradient(135deg, rgba(0,0,0,0.98), rgba(20,20,20,0.98));
color: #0f0;
border: 5px solid #0f0;
border-radius: 20px;
padding: 30px;
padding-top: 50px;
z-index: 9999999999;
width: 600px;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 0 100px rgba(0,255,0,0.8);
}
#cfg_close_btn {
position: absolute;
top: 15px;
right: 15px;
width: 35px;
height: 35px;
border-radius: 50%;
background: linear-gradient(135deg, #f00, #c00);
color: #fff;
border: none;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
z-index: 10;
font-weight: bold;
box-shadow: 0 2px 8px rgba(255,0,0,0.3);
}
#cfg_close_btn:hover {
background: linear-gradient(135deg, #ff0000, #ff3333);
transform: scale(1.1) rotate(90deg);
box-shadow: 0 4px 12px rgba(255,0,0,0.5);
}
#cfg_close_btn:active {
transform: scale(0.95) rotate(90deg);
}
.cfg-section {
background: rgba(0,255,0,0.05);
padding: 20px;
border-radius: 12px;
margin: 15px 0;
border: 2px solid rgba(0,255,0,0.3);
}
.cfg-section h3 {
margin: 0 0 15px 0;
font-size: 18px;
color: #00ff00;
border-bottom: 2px solid #0f0;
padding-bottom: 10px;
}
#FIX_CFG input[type="text"],
#FIX_CFG input[type="password"],
#FIX_CFG select {
width: 100%;
padding: 12px;
margin: 8px 0;
background: rgba(0,0,0,0.6);
border: 2px solid #0f0;
border-radius: 8px;
color: #0f0;
font-size: 14px;
font-family: 'Consolas', monospace;
transition: all 0.3s;
box-sizing: border-box;
}
#FIX_CFG input[type="text"]:focus,
#FIX_CFG input[type="password"]:focus,
#FIX_CFG select:focus {
outline: none;
border-color: #00ff00;
box-shadow: 0 0 15px rgba(0,255,0,0.4);
}
#FIX_CFG input[type="checkbox"] {
width: 18px;
height: 18px;
margin-right: 10px;
cursor: pointer;
}
#FIX_CFG label {
display: flex;
align-items: center;
margin: 10px 0;
cursor: pointer;
font-size: 15px;
}
#FIX_CFG select {
cursor: pointer;
}
#FIX_CFG select option {
background: #000;
color: #0f0;
}
.bank-item {
background: rgba(0,0,0,0.4);
padding: 15px;
border-radius: 10px;
margin: 10px 0;
border: 2px solid rgba(0,255,0,0.2);
transition: all 0.3s;
position: relative;
}
.bank-item.disabled {
opacity: 0.5;
}
.bank-item:hover {
border-color: rgba(0,255,0,0.5);
}
.bank-item-close {
position: absolute;
top: 10px;
right: 10px;
width: 25px;
height: 25px;
border-radius: 50%;
background: linear-gradient(135deg, #f00, #c00);
color: #fff;
border: none;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
font-weight: bold;
box-shadow: 0 2px 5px rgba(255,0,0,0.3);
}
.bank-item-close:hover {
background: linear-gradient(135deg, #ff0000, #ff3333);
transform: scale(1.1) rotate(90deg);
box-shadow: 0 3px 8px rgba(255,0,0,0.5);
}
.answer-mark {
background: linear-gradient(135deg, rgba(0,255,0,0.2), rgba(0,200,0,0.2)) !important;
border: 2px solid #0f0 !important;
padding: 12px !important;
margin: 12px 0 !important;
border-radius: 10px !important;
color: #0f0 !important;
font-weight: bold !important;
font-size: 15px !important;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 20px;
flex-wrap: wrap;
}
.btn-group button {
flex: 1;
min-width: 120px;
}
#progress_bar {
width: 100%;
height: 6px;
background: rgba(0,255,0,0.2);
border-radius: 3px;
margin: 10px 0;
overflow: hidden;
}
#progress_fill {
height: 100%;
background: linear-gradient(90deg, #0f0, #00ff00);
width: 0%;
transition: width 0.3s;
}
.stats-box {
background: rgba(0,255,0,0.1);
padding: 10px;
border-radius: 8px;
margin: 10px 0;
font-size: 13px;
}
.stats-item {
display: flex;
justify-content: space-between;
padding: 5px 0;
}
.ai-provider-hint {
background: rgba(0,0,0,0.75);
padding: 10px 12px;
border-radius: 6px;
margin: 10px 0;
border-left: 3px solid #0f0;
font-size: 13px;
line-height: 1.7;
color: #c8ffc8;
}
.ai-provider-hint strong {
color: #ffff66;
font-weight: bold;
}
.ai-provider-hint code {
display: inline-block;
padding: 3px 8px;
margin: 3px 4px 0 0;
border-radius: 6px;
background: rgba(0,0,0,0.95);
border: 1px solid #ffff66;
color: #ffff66;
font-size: 13px;
font-weight: bold;
font-family: "Consolas","Monaco",monospace;
text-shadow: 0 0 5px rgba(255,255,102,0.7);
}
`);
// ================ UI 管理模块 ================
const UIManager = {
config: null,
processing: false,
init(config) {
this.config = config;
this.createPanel();
this.createConfigDialog();
this.bindEvents();
this.makeDraggable();
},
createPanel() {
const panel = document.createElement("div");
panel.id = "FIX_PANEL";
panel.innerHTML = `
<div id="panel_header">
<span style="font-size:18px;">📊 传智满分助手 v13.6</span>
<button id="minimize_btn" title="最小化/还原">−</button>
</div>
<div id="panel_content">
<button id="open_cfg" class="cfg-btn">⚙️ 配置中心</button>
<button id="start_answer" class="cfg-btn" style="background: linear-gradient(135deg, #ff0, #ffcc00) !important;">
▶️ 开始答题
</button>
<div class="stats-box" id="stats_box">
<div class="stats-item">
<span>缓存大小:</span>
<span id="cache_size">0</span>
</div>
<div class="stats-item">
<span>启用题库:</span>
<span id="enabled_banks">0</span>
</div>
<div class="stats-item">
<span>AI模型:</span>
<span id="ai_status">未启用</span>
</div>
</div>
<div id="progress_bar">
<div id="progress_fill"></div>
</div>
<div id="fix_log"></div>
<div style="text-align:center;color:#ff0;font-size:15px;margin-top:12px;text-shadow: 0 0 8px #ff0;" id="fix_status">
等待开始...
</div>
</div>
`;
document.body.appendChild(panel);
this.updateStats();
},
createConfigDialog() {
const cfg = document.createElement("div");
cfg.id = "FIX_CFG";
// 构建AI提供商选项
const aiProviderOptions = Object.entries(AI_MODELS)
.map(
([key, model]) =>
`<option value="${key}" ${
this.config.ai_provider === key ? "selected" : ""
}>${model.name}</option>`
)
.join("");
// HTML 结构
cfg.innerHTML = `
<button id="cfg_close_btn" title="关闭">✕</button>
<div style="font-size:24px;text-align:center;margin-bottom:20px;text-shadow: 0 0 10px #0f0;">
⚙️ 配置中心
</div>
<div class="cfg-section">
<h3>📚 题库配置</h3>
<div id="banks_list"></div>
<button id="add_bank" class="cfg-btn" style="background: linear-gradient(135deg, #00ccff, #0099ff) !important;">
➕ 添加新题库
</button>
</div>
<div class="cfg-section">
<h3>🤖 AI配置 (全模型支持)</h3>
<label>
<input type="checkbox" id="ai_sw" ${
this.config.ai_enabled ? "checked" : ""
}>
启用AI答题(题库找不到时使用)
</label>
<label style="font-size: 14px; margin-top: 10px;">AI提供商:</label>
<select id="ai_provider">
${aiProviderOptions}
</select>
<label style="font-size: 14px; margin-top: 10px;">API Key:</label>
<div style="position: relative;">
<input type="password" id="ai_k" placeholder="输入API Key" value="${
this.config.ai_key
}">
<span id="toggle_ai_key" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); cursor: pointer; color: #0f0; font-size: 12px;">
显示
</span>
</div>
<label style="font-size: 14px; margin-top: 10px;">模型名称 (手动输入):</label>
<input type="text" id="ai_model" value="${
this.config.ai_model
}" placeholder="例如: gpt-4o-mini">
<div id="ai_url_section">
<label style="font-size: 14px; margin-top: 10px;">API地址 (URL):</label>
<input type="text" id="ai_u" placeholder="API 请求地址" value="${
this.config.ai_url
}">
<div class="ai-provider-hint" style="margin-top:5px; font-size:11px; padding:5px;">
<strong>提示:</strong> 使用 Gemini 官方接口时, URL 中需保留 <code>{model}</code> 占位符, 由脚本自动替换为上方模型名称。
</div>
</div>
<button id="test_ai" class="cfg-btn" style="background: linear-gradient(135deg, #ffff66, #ffcc00) !important; margin-top: 10px;">
🔍 测试AI连通性
</button>
</div>
<div class="cfg-section">
<h3>🔧 高级设置</h3>
<label>
日志级别:
<select id="log_level" style="margin-left: 10px;">
<option value="DEBUG">调试</option>
<option value="INFO" selected>信息</option>
<option value="WARN">警告</option>
<option value="ERROR">错误</option>
</select>
</label>
<div class="btn-group" style="margin-top: 15px;">
<button id="clear_cache" class="cfg-btn" style="background: linear-gradient(135deg, #ff6600, #ff8800) !important;">
清空缓存
</button>
<button id="clear_log" class="cfg-btn" style="background: linear-gradient(135deg, #9900ff, #aa00ff) !important;">
清空日志
</button>
</div>
<div class="btn-group">
<button id="export_config" class="cfg-btn" style="background: linear-gradient(135deg, #0099ff, #00aaff) !important;">
导出配置
</button>
<button id="export_log" class="cfg-btn" style="background: linear-gradient(135deg, #ff0099, #ff00aa) !important;">
导出日志
</button>
</div>
</div>
<div class="btn-group">
<button id="save_cfg" class="cfg-btn">💾 保存配置</button>
</div>
`;
document.body.appendChild(cfg);
this.renderBanksList();
},
renderBanksList() {
const list = document.getElementById("banks_list");
if (!list) return;
list.innerHTML = this.config.banks
.map(
(bank, index) => `
<div class="bank-item ${
!bank.enabled ? "disabled" : ""
}" data-index="${index}">
<button class="bank-item-close" data-index="${index}" title="删除">✕</button>
<label>
<input type="checkbox" class="bank-toggle" data-index="${index}" ${
bank.enabled ? "checked" : ""
}>
<strong>${Utils.sanitizeHTML(bank.name)}</strong>
</label>
<input type="text" class="bank-name" placeholder="题库名称" value="${Utils.sanitizeHTML(
bank.name
)}" data-index="${index}">
<input type="text" class="bank-url" placeholder="题库URL" value="${Utils.sanitizeHTML(
bank.url
)}" data-index="${index}">
<input type="password" class="bank-token" placeholder="Token/Key(如有)" value="${Utils.sanitizeHTML(
bank.token || ""
)}" data-index="${index}">
</div>
`
)
.join("");
this.bindBankEvents();
},
bindBankEvents() {
document.querySelectorAll(".bank-toggle").forEach((cb) => {
cb.onchange = (e) => {
const idx = parseInt(e.target.dataset.index);
this.config.banks[idx].enabled = e.target.checked;
this.renderBanksList();
};
});
document.querySelectorAll(".bank-name").forEach((input) => {
input.onchange = (e) => {
const idx = parseInt(e.target.dataset.index);
this.config.banks[idx].name = e.target.value;
};
});
document.querySelectorAll(".bank-url").forEach((input) => {
input.onchange = (e) => {
const idx = parseInt(e.target.dataset.index);
this.config.banks[idx].url = e.target.value;
};
});
document.querySelectorAll(".bank-token").forEach((input) => {
input.onchange = (e) => {
const idx = parseInt(e.target.dataset.index);
this.config.banks[idx].token = e.target.value;
};
});
document.querySelectorAll(".bank-item-close").forEach((btn) => {
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const idx = parseInt(e.target.dataset.index);
if (confirm(`确定删除题库"${this.config.banks[idx].name}"?`)) {
this.config.banks.splice(idx, 1);
this.renderBanksList();
Logger.info("已删除题库");
}
};
});
},
bindEvents() {
// 最小化按钮
document.getElementById("minimize_btn").onclick = () => {
const panel = document.getElementById("FIX_PANEL");
panel.classList.toggle("minimized");
document.getElementById("minimize_btn").textContent =
panel.classList.contains("minimized") ? "+" : "−";
};
// 打开配置
document.getElementById("open_cfg").onclick = () => {
document.getElementById("FIX_CFG").style.display = "block";
};
// 配置弹窗右上角关闭按钮
document.getElementById("cfg_close_btn").onclick = () => {
document.getElementById("FIX_CFG").style.display = "none";
};
// 保存配置
document.getElementById("save_cfg").onclick = () => {
this.saveConfig();
};
// 开始答题
document.getElementById("start_answer").onclick = () => {
this.startAnswering();
};
// 添加题库
document.getElementById("add_bank").onclick = () => {
this.config.banks.push({
name: "新题库",
enabled: true,
url: "https://",
method: "GET",
token: "",
});
this.renderBanksList();
Logger.info("已添加新题库");
};
// AI提供商切换
document.getElementById("ai_provider").onchange = (e) => {
const provider = e.target.value;
const providerConfig = AI_MODELS[provider];
const urlInput = document.getElementById("ai_u");
const modelInput = document.getElementById("ai_model");
this.config.ai_provider = provider;
modelInput.value = providerConfig.defaultModel;
modelInput.placeholder = `例如: ${providerConfig.defaultModel}`;
urlInput.value = providerConfig.endpoint;
Logger.info(`已切换厂商: ${providerConfig.name}, 请确认模型名称`);
};
// 显示/隐藏AI Key
document.getElementById("toggle_ai_key").onclick = (e) => {
const input = document.getElementById("ai_k");
input.type = input.type === "password" ? "text" : "password";
e.target.textContent = input.type === "password" ? "显示" : "隐藏";
};
// 清空缓存
document.getElementById("clear_cache").onclick = () => {
if (confirm("确定清空所有缓存?")) {
CacheManager.clear();
this.updateStats();
Logger.success("缓存已清空");
}
};
// 清空日志
document.getElementById("clear_log").onclick = () => {
Logger.clear();
Logger.success("日志已清空");
};
// 导出配置
document.getElementById("export_config").onclick = () => {
ConfigManager.export();
Logger.success("配置已导出");
};
// 导出日志
document.getElementById("export_log").onclick = () => {
Logger.export();
Logger.success("日志已导出");
};
// 测试 AI 按钮
document.getElementById("test_ai").onclick = () => {
this.testAIConfig();
};
// ESC键关闭配置
document.addEventListener("keydown", (e) => {
if (
e.key === "Escape" &&
document.getElementById("FIX_CFG").style.display === "block"
) {
document.getElementById("FIX_CFG").style.display = "none";
}
});
},
async testAIConfig() {
try {
const ai_enabled = document.getElementById("ai_sw").checked;
const ai_key = document.getElementById("ai_k").value.trim();
const ai_provider = document.getElementById("ai_provider").value;
let ai_url = document.getElementById("ai_u").value.trim();
const ai_model = document.getElementById("ai_model").value.trim();
if (!ai_enabled) {
alert("请先勾选【启用AI答题】再测试。");
return;
}
if (!ai_key || !ai_model) {
alert("请先填写 API Key 和 模型名称。");
return;
}
if (!ai_url) {
ai_url = AI_MODELS[ai_provider].endpoint;
}
const tempConfig = {
ai_enabled: true,
ai_provider,
ai_key,
ai_url,
ai_model,
};
Logger.info("正在测试 AI 配置,请稍等...");
const res = await APIClient.queryAI(tempConfig, "只需回复: OK");
const preview = (res || "").toString().slice(0, 50);
Logger.success("AI 测试成功, 返回: " + preview);
alert("AI 测试成功!\n返回内容(前50字):\n" + preview);
} catch (err) {
Logger.error("AI 测试失败: " + err.message);
alert("AI 测试失败:\n" + err.message);
}
},
saveConfig() {
// 读取配置
this.config.ai_enabled = document.getElementById("ai_sw").checked;
this.config.ai_provider = document.getElementById("ai_provider").value;
this.config.ai_key = document.getElementById("ai_k").value.trim();
this.config.ai_url = document.getElementById("ai_u").value.trim();
this.config.ai_model = document.getElementById("ai_model").value.trim();
this.config.logLevel = document.getElementById("log_level").value;
// 设置默认URL(如果为空)
if (!this.config.ai_url) {
const provider = AI_MODELS[this.config.ai_provider];
this.config.ai_url = provider.endpoint;
}
// 验证配置
const errors = ConfigManager.validate(this.config);
if (errors.length > 0) {
alert("配置错误:\n\n" + errors.join("\n"));
return;
}
// 保存配置
ConfigManager.save(this.config);
document.getElementById("FIX_CFG").style.display = "none";
Logger.success("配置已保存");
Logger.init(this.config.logLevel);
QuestionProcessor.init(this.config);
this.updateStats();
if (this.config.ai_enabled) {
const providerName =
AI_MODELS[this.config.ai_provider]?.name || "AI";
Logger.info(
`AI已启用: ${providerName} (模型: ${this.config.ai_model})`
);
}
},
updateStats() {
document.getElementById("cache_size").textContent =
CacheManager.getSize();
document.getElementById("enabled_banks").textContent =
this.config.banks.filter((b) => b.enabled).length;
const aiStatus = document.getElementById("ai_status");
if (this.config.ai_enabled) {
const providerName =
AI_MODELS[this.config.ai_provider]?.name || "未知";
aiStatus.textContent = `${providerName}`;
aiStatus.style.color = "#0f0";
} else {
aiStatus.textContent = "未启用";
aiStatus.style.color = "#666";
}
},
updateStatus(msg) {
const status = document.getElementById("fix_status");
if (status) status.textContent = msg;
},
updateProgress(current, total) {
const percent = (current / total) * 100;
document.getElementById("progress_fill").style.width = percent + "%";
this.updateStatus(
`处理中: ${current}/${total} (${percent.toFixed(1)}%)`
);
},
async startAnswering() {
if (this.processing) {
Logger.warn("答题进行中,请勿重复点击");
return;
}
const questions = QuestionProcessor.detectQuestions();
if (questions.length === 0) {
Logger.error("未检测到题目");
alert("未检测到题目!\n\n请刷新页面后重试");
return;
}
this.processing = true;
const startBtn = document.getElementById("start_answer");
startBtn.disabled = true;
startBtn.textContent = "⏸️ 处理中...";
Logger.success(`开始处理 ${questions.length} 道题目`);
const results = {
success: 0,
failed: 0,
skipped: 0,
error: 0,
};
for (let i = 0; i < questions.length; i++) {
try {
const result = await QuestionProcessor.processQuestion(
questions[i],
i + 1,
questions.length
);
if (result.status === "success") results.success++;
else if (result.status === "skipped") results.skipped++;
else if (result.status === "failed") results.failed++;
else if (result.status === "error") results.error++;
this.updateProgress(i + 1, questions.length);
await Utils.sleep(800);
} catch (error) {
Logger.error(`第${i + 1}题处理异常: ${error.message}`);
results.error++;
}
}
this.processing = false;
startBtn.disabled = false;
startBtn.textContent = "▶️ 开始答题";
Logger.success(
`处理完成!成功: ${results.success}, 跳过: ${results.skipped}, 失败: ${results.failed}, 错误: ${results.error}`
);
setTimeout(() => {
alert(
`答题完成!\n\n` +
`成功: ${results.success}\n` +
`跳过: ${results.skipped}\n` +
`失败: ${results.failed}\n` +
`错误: ${results.error}\n\n` +
`请检查后提交试卷`
);
}, 500);
},
makeDraggable() {
const panel = document.getElementById("FIX_PANEL");
const header = document.getElementById("panel_header");
let isDragging = false;
let currentX, currentY, initialX, initialY;
header.addEventListener("mousedown", (e) => {
if (e.target.id === "minimize_btn") return;
initialX = e.clientX - panel.offsetLeft;
initialY = e.clientY - panel.offsetTop;
if (e.target === header || e.target.parentElement === header) {
isDragging = true;
panel.style.cursor = "grabbing";
}
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
const maxX = window.innerWidth - panel.offsetWidth;
const maxY = window.innerHeight - panel.offsetHeight;
currentX = Math.max(0, Math.min(currentX, maxX));
currentY = Math.max(0, Math.min(currentY, maxY));
panel.style.left = currentX + "px";
panel.style.top = currentY + "px";
panel.style.right = "auto";
}
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
panel.style.cursor = "move";
}
});
},
};
// ================ 防检测 ================
function applyAntiDetection() {
// 只在写卷页面启用
if (!window.location.href.includes("/writePaper/")) return;
try {
// 拦截可见性 / 焦点事件,防止被监控切屏
["visibilitychange", "webkitvisibilitychange", "mozvisibilitychange", "msvisibilitychange", "blur", "focus"].forEach((e) => {
window.addEventListener(
e,
(ev) => ev.stopImmediatePropagation(),
true
);
});
// 兼容地处理 document.hidden:如果是可配置的才去重写
const desc =
Object.getOwnPropertyDescriptor(document, "hidden") ||
Object.getOwnPropertyDescriptor(Document.prototype || {}, "hidden");
if (!desc || desc.configurable) {
Object.defineProperty(document, "hidden", {
get: () => false,
configurable: true,
});
Logger.debug("已重写 document.hidden 属性");
} else {
// 某些浏览器中该属性不可配置,强行重写会报错,这里直接跳过即可
Logger.debug("document.hidden 为不可配置属性,跳过重写");
}
// hasFocus 直接覆盖即可,通常不会抛错
if (typeof document.hasFocus === "function") {
document.hasFocus = () => true;
Logger.debug("已重写 document.hasFocus");
}
Logger.debug("防检测已启用(兼容模式)");
} catch (e) {
// 所有异常都吃掉,只打日志,不让 init 整体失败
Logger.warn("防检测启用失败, 已降级处理: " + e.message);
}
}
// ================ 主初始化 ================
async function init() {
try {
let config = ConfigManager.load();
config = ConfigManager.decrypt(config);
Logger.init(config.logLevel || "INFO");
QuestionProcessor.init(config);
UIManager.init(config);
applyAntiDetection();
Logger.success("脚本加载完成 v13.6");
const enabledBanks = config.banks.filter((b) => b.enabled && b.token);
if (enabledBanks.length > 0) {
Logger.info(`已配置 ${enabledBanks.length} 个题库`);
} else {
Logger.warn("未配置题库");
}
if (config.ai_enabled && config.ai_key) {
const providerName =
AI_MODELS[config.ai_provider]?.name || "AI";
Logger.info(`AI已启用: ${providerName} (${config.ai_model})`);
}
await Utils.sleep(2000);
const questions = QuestionProcessor.detectQuestions();
if (questions.length > 0) {
Logger.success(`检测到 ${questions.length} 道题目`);
UIManager.updateStatus(
`检测到 ${questions.length} 道题,点击开始答题`
);
} else {
Logger.info("等待题目加载...");
}
} catch (error) {
console.error("[传智助手] 初始化失败:", error);
alert(`脚本初始化失败:${error.message}`);
}
}
// ================ 启动 ================
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
setTimeout(init, 100);
}
// 全局错误处理
window.addEventListener("error", (e) => {
Logger.error(`全局错误: ${e.message}`);
console.error(e);
});
window.addEventListener("unhandledrejection", (e) => {
Logger.error(`未处理的Promise错误: ${e.reason}`);
console.error(e);
});
})();