// ==UserScript==
// @name 🤖 党旗飘飘 党校智能答题助手 v1.1.5
// @namespace http://tampermonkey.net/
// @version 1.1.5
// @description 自动答题(反馈群:612441267)
// @author lakay666
// @match https://wsdx.hzau.edu.cn/jjfz/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect 150.158.119.55
// @connect wsdx.hzau.edu.cn
// @connect api.moonshot.cn
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const SERVER_URL = 'http://150.158.119.55:3001';
const KIMI_API_URL = 'https://api.moonshot.cn/v1/chat/completions';
let KIMI_API_KEY = GM_getValue('kimi_api_key', '');
const wait = ms => new Promise(r => setTimeout(r, ms));
const log = (...args) => console.log('[党校助手]', ...args);
GM_registerMenuCommand('🔑 设置本地Kimi Key(留空=用服务器)', () => {
const key = prompt('请输入 Kimi API Key(sk-开头,留空使用服务器默认Key):', KIMI_API_KEY);
if (key === '') { GM_setValue('kimi_api_key', ''); KIMI_API_KEY = ''; alert('✅ 已切换到服务器模式'); }
else if (key && key.startsWith('sk-')) { GM_setValue('kimi_api_key', key); KIMI_API_KEY = key; alert('✅ 本地Key已保存'); }
else if (key) { alert('❌ Key 格式错误'); }
});
function gmFetch(url, options = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: options.method || 'GET', url,
headers: { 'Content-Type': 'application/json', ...options.headers },
data: options.body ? JSON.stringify(options.body) : undefined,
timeout: 15000,
onload: res => { try { resolve(JSON.parse(res.responseText)); } catch { resolve(res.responseText); } },
onerror: reject,
ontimeout: () => reject(new Error('超时'))
});
});
}
function getUserInfo() {
try {
const uaId = document.cookie.split(';').find(c => c.includes('ua_id'));
if (!uaId) return null;
const raw = decodeURIComponent(uaId.split('=')[1]);
const match = raw.match(/eyJ[\w+/=]+/);
if (!match) return null;
const json = JSON.parse(atob(match[0]));
return { platform_id: String(json.user_id), user_name: json.user_name };
} catch (e) { return null; }
}
async function localKimiAnswer(title, options, type) {
const prompt = '请回答以下' + type + '。\n题目:' + title + '\n' +
(type === '填空题' ? '请直接输出答案文字,不要解释。' :
type === '判断题' ? '请只输出"正确"或"错误"两个汉字。' :
'选项:\n' + options.map((o,i) => String.fromCharCode(65+i) + '. ' + o).join('\n') + '\n要求:' + (type === '多选题' ? '输出所有正确选项字母如AB' : '输出一个字母如C'));
const res = await gmFetch(KIMI_API_URL, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + KIMI_API_KEY },
body: { model: 'moonshot-v1-8k', messages: [{ role: 'user', content: prompt }], temperature: 0.1, max_tokens: type === '填空题' ? 50 : 10 }
});
if (res.error) throw new Error(res.error.message);
const raw = res.choices?.[0]?.message?.content?.trim() || '';
if (type === '填空题') return raw;
if (type === '判断题') {
const r = raw.replace(/[。!,\s]/g, '');
if (r.includes('正确') || r.includes('对')) return '正确';
if (r.includes('错误') || r.includes('错')) return '错误';
return '正确';
}
if (type === '多选题') return raw.replace(/[^A-D]/g, '').toUpperCase();
return raw.replace(/[^A-D]/g, '').toUpperCase()[0] || '';
}
// ==================== 页面判断 ====================
const href = location.href;
const isExamPage = href.includes('/lesson/exam') || href.includes('/exam_center/end_exam');
const isResultPage = href.includes('/lesson/exam/result') || href.includes('/exam_center/result');
const isRecordPage = href.includes('/exam_center/record');
const isShowPage = href.includes('/exam_center/show?rid=') || href.includes('/exam_center/end_show?rid=');
if (isExamPage) initExamPage();
else if (isResultPage) initResultPage();
else if (isRecordPage || isShowPage) initRecordPage();
// ==================== 答题页 ====================
async function initExamPage() {
const user = getUserInfo();
const panel = document.createElement('div');
panel.id = 'hzau-panel';
panel.style.cssText = 'position:fixed;top:20px;right:20px;z-index:999999;background:#fff;border:2px solid #e74c3c;border-radius:8px;padding:15px;width:280px;box-shadow:0 4px 15px rgba(0,0,0,0.3);font-family:"Microsoft YaHei",sans-serif;font-size:13px;';
let infoHtml = '';
if (user) {
try {
const info = await gmFetch(SERVER_URL + '/api/user/info?platform_id=' + user.platform_id + '&user_name=' + encodeURIComponent(user.user_name));
infoHtml = '
👤 ' + user.user_name + ' | 剩余: ' + (info.answer_times ?? '?') + ' 次
';
} catch (e) {
infoHtml = '⚠️ 服务器连接失败
';
}
} else {
infoHtml = '⚠️ 未检测到登录信息
';
}
panel.innerHTML = '🤖 党校助手 v1.1.5
' + infoHtml + '';
document.body.appendChild(panel);
document.getElementById('hzau-auto').onclick = async () => {
const btn = document.getElementById('hzau-auto');
btn.disabled = true; btn.textContent = '⏳ 运行中...';
try { await runAutoAnswer(true); } catch (e) { alert(e.message); }
btn.disabled = false; btn.textContent = '🚀 全自动答题';
};
document.getElementById('hzau-submit').onclick = clickSubmit;
}
function killInterference() {
const maxId = setTimeout(() => {}, 0);
for (let i = 0; i < maxId; i++) { try { clearTimeout(i); clearInterval(i); } catch(e) {} }
}
async function runAutoAnswer(autoSubmit) {
const status = document.getElementById('hzau-status');
const user = getUserInfo();
if (!user) { alert('未检测到登录信息,请先登录党校平台'); return; }
const lessonId = new URLSearchParams(location.search).get('lesson_id') || '';
const allUls = document.querySelectorAll('ul.exam_ul');
const allLis = [];
allUls.forEach(ul => { ul.querySelectorAll('li').forEach(li => { const t = li.textContent.trim(); if (/^\d+$/.test(t)) allLis.push({ num: parseInt(t), el: li, qid: li.id }); }); });
const total = allLis.length;
log('共 ' + total + ' 题 | ' + (KIMI_API_KEY ? '本地AI优先' : '服务器模式'));
for (let i = 0; i < allLis.length; i++) {
const { num, el } = allLis[i];
status.textContent = '第 ' + num + '/' + total + ' 题...';
el.click();
await waitForQuestionLoad();
const q = extractQuestion();
log('第' + num + '题 [' + q.type + '] ' + q.title.slice(0, 30));
let answer = null;
let source = '';
// 1. 本地AI(带超时保护)
if (KIMI_API_KEY) {
try {
answer = await Promise.race([
localKimiAnswer(q.title, q.options, q.type),
new Promise((_, reject) => setTimeout(() => reject(new Error('本地AI超时')), 8000))
]);
source = '🤖本地AI';
} catch (e) { log(' 本地AI: ' + e.message); answer = null; }
}
// 2. 服务器(带超时保护)
if (!answer) {
try {
const res = await Promise.race([
gmFetch(SERVER_URL + '/api/exam/answer', {
method: 'POST',
body: { platform_id: user.platform_id, user_name: user.user_name, lesson_id: lessonId, type: q.type, title: q.title, options: q.options }
}),
new Promise((_, reject) => setTimeout(() => reject(new Error('服务器超时')), 10000))
]);
if (res.error) { alert(res.error); return; }
answer = res.answer; source = '🖥️服务器';
status.textContent = '第 ' + num + '/' + total + ' - 剩余' + (res.remaining ?? '?') + '次';
} catch (e) { log(' 服务器: ' + e.message); continue; }
}
// 判断题转换
if (q.type === '判断题' && answer && /^[A-D]$/.test(answer)) {
const labels = document.querySelectorAll('.exam_cont_left .answer_list li label');
if (labels.length === 2) {
const idx = answer.charCodeAt(0) - 65;
const text = labels[idx]?.textContent.trim() || '';
if (text.includes('正确') || text.includes('对')) answer = '正确';
else if (text.includes('错误') || text.includes('错')) answer = '错误';
}
}
log(' ' + source + ': ' + answer);
killInterference();
await selectAnswer(q.type, answer);
await wait(300);
}
status.textContent = '✅ 完成 ' + total + ' 题';
if (autoSubmit) { status.textContent = '正在交卷...'; await wait(1000); clickSubmit(); }
}
function extractQuestion() {
const c = document.querySelector('.exam_cont_left');
const typeEl = c.querySelector('.e_cont_title span');
let type = typeEl?.textContent?.trim() || '未知';
if (!type || type === '未知') {
if (c.querySelector('.summary_question')) type = '填空题';
}
let title = c.querySelector('.exam_h2')?.innerText?.trim() || '';
title = title.replace(/^\d+\.\s*/, '').replace(/\u00A0/g, ' ');
let optEls = c.querySelectorAll('.answer_list li, .answer_list_box li');
const options = [];
optEls.forEach(li => {
const label = li.querySelector('label');
let text = label ? label.innerText.trim() : li.innerText.trim();
text = text.replace(/^\s+/, '').trim();
if (text) options.push(text);
});
return { type, title, options };
}
async function selectAnswer(type, answer) {
const c = document.querySelector('.exam_cont_left');
if (!c) return;
// 填空题
if (type === '填空题') {
const input = c.querySelector('.summary_question, input[type="text"], textarea');
if (input) {
input.focus();
input.value = '';
input.dispatchEvent(new Event('input', { bubbles: true }));
await wait(100);
input.value = answer;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('blur', { bubbles: true }));
}
const saveBtn = [...document.querySelectorAll('a')].find(a => a.textContent.trim() === '保存');
if (saveBtn) { saveBtn.click(); saveBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); }
await wait(600);
const nextBtn = document.getElementById('next_question');
if (nextBtn && !nextBtn.classList.contains('dis_click')) nextBtn.click();
return;
}
// 判断题
if (type === '判断题') {
c.querySelectorAll('label').forEach(label => {
const text = label.textContent.trim();
if ((answer === '正确' && (text.includes('正确') || text.includes('对'))) ||
(answer === '错误' && (text.includes('错误') || text.includes('错')))) {
label.click();
}
});
return;
}
// 单选题
if (type !== '多选题') {
let items = c.querySelectorAll('.answer_mul');
if (items.length === 0) items = c.querySelectorAll('.answer_list li');
const idx = answer.charCodeAt(0) - 65;
if (idx >= 0 && idx < items.length) {
const label = items[idx].querySelector('label');
if (label) label.click();
}
return;
}
// 多选题
const items = c.querySelectorAll('.answer_mul');
if (items.length === 0) return;
const answers = answer.split('');
const timestamp = Date.now();
items.forEach((item, i) => {
const input = item.querySelector('input[type="checkbox"]');
if (input) { input.checked = false; input.name = 'multi_' + timestamp + '_' + i; }
});
for (let idx = items.length - 1; idx >= 0; idx--) {
const letter = String.fromCharCode(65 + idx);
if (answers.includes(letter)) {
const item = items[idx];
const input = item.querySelector('input');
const label = item.querySelector('label');
input.name = 'multi_' + timestamp + '_fix_' + idx;
input.checked = true;
input.dispatchEvent(new Event('change', { bubbles: true }));
if (label) { label.classList.add('layui-form-checked'); label.click(); }
await wait(600);
}
}
}
function waitForQuestionLoad() {
return new Promise(resolve => {
let resolved = false;
const c = document.querySelector('.exam_cont_left');
if (c) {
const obs = new MutationObserver(() => {
if (!document.querySelector('.layui-layer-loading') && (c.querySelector('.exam_h2') || c.querySelector('.summary_question') || c.querySelector('.answer_list li')) && !resolved) {
resolved = true; obs.disconnect(); resolve();
}
});
obs.observe(c, { childList: true, subtree: true });
}
setTimeout(() => { if (!resolved) resolve(); }, 2000);
});
}
function clickSubmit() {
const btn = document.getElementById('submit_exam') || document.querySelector('.exam_a_sub');
if (btn) { btn.click(); setTimeout(confirmSubmitDialog, 500); return true; }
for (const el of document.querySelectorAll('a, button')) {
if (/^交卷$|^提交$/.test(el.textContent.trim())) { el.click(); setTimeout(confirmSubmitDialog, 500); return true; }
}
return false;
}
function confirmSubmitDialog() {
const btn = document.querySelector('.public_submit');
if (btn) { btn.click(); return true; }
for (const el of document.querySelectorAll('a, button')) {
if (el.textContent.includes('我要提交') || el.textContent.includes('确认')) { el.click(); return true; }
}
return false;
}
// ==================== 自动收录 ====================
function initResultPage() {
log('📚 自动收录...');
setTimeout(async () => {
const btn = Array.from(document.querySelectorAll('a, button')).find(el => el.textContent.includes('详情') || (el.href || '').includes('show'));
if (btn) { btn.click(); await wait(2500); }
await saveQuestions();
}, 2000);
}
function initRecordPage() {
log('📚 自动收录...');
setTimeout(async () => { await saveQuestions(); }, 2000);
}
async function saveQuestions() {
const questions = [];
document.querySelectorAll('.error_sub').forEach((el, idx) => {
const h3 = el.querySelector('h3');
let title = h3 ? h3.innerText.trim() : '';
const typeMatch = title.match(/【(.+?)】/);
const type = typeMatch ? typeMatch[1] : '未知';
title = title.replace(/^\d+[、,.]\s*/, '').replace(/【.+?】/, '').trim();
const optEls = (el.querySelector('.exam_result2, .exam_result_box2') || el).querySelectorAll('li');
const options = [];
optEls.forEach(li => {
const text = li.innerText.trim().replace(/^[A-D][、,]?\s*/, '').trim();
if (text) options.push(String.fromCharCode(65 + options.length) + '. ' + text);
});
let answer = '';
if (type === '填空题') {
const contEl = el.querySelector('.sub_cont');
if (contEl) answer = contEl.textContent.trim();
} else {
const ansEl = el.querySelector('.sub_color');
if (ansEl) { const m = ansEl.textContent.match(/正确答案[::]\s*([A-D]+|正确|错误)/); answer = m ? m[1] : ''; }
if (!answer) optEls.forEach((li, i) => { if (li.classList.contains('result_cut') || li.querySelector('.checked')) answer += String.fromCharCode(65 + i); });
}
if (title && answer) questions.push({ qid: 'a_' + idx + '_' + Date.now(), type, title, options, answer });
});
if (questions.length > 0) {
try {
const res = await gmFetch(SERVER_URL + '/save-batch', { method: 'POST', body: { questions } });
log('收录: 新增' + (res.saved || 0) + ' 跳过' + (res.skipped || 0));
} catch (e) { log('收录失败:', e); }
}
}
})();