// ==UserScript==
// @name 华南理工作业互评增强版 v6.0
// @namespace http://tampermonkey.net/
// @version 6.0
// @description 4种模式:默认满分/AI自动评分/导出题目数据/导入评分结果。支持10+供应商,双重冷却防刷,跳过已评,max_tokens=5000兼容思考模型
// @author WanderLandWalker
// @match http://1024.se.scut.edu.cn/*
// @match https://1024.se.scut.edu.cn/*
// @grant none
// @license GPL-3.0
// ==/UserScript==
(function () {
'use strict';
try {
var PROVIDERS = [
{ name: 'DeepSeek', url: 'https://api.deepseek.com/v1', models: ['deepseek-chat', 'deepseek-reasoner'] },
{ name: 'OpenAI', url: 'https://api.openai.com/v1', models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'] },
{ name: '通义千问(阿里)', url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', models: ['qwen-plus', 'qwen-turbo', 'qwen-max', 'qwen-long'] },
{ name: '硅基流动', url: 'https://api.siliconflow.cn/v1', models: ['deepseek-ai/DeepSeek-V3', 'deepseek-ai/DeepSeek-R1', 'Qwen/Qwen2.5-72B-Instruct'] },
{ name: '智谱(GLM)', url: 'https://open.bigmodel.cn/api/paas/v4', models: ['glm-4-flash', 'glm-4', 'glm-4-plus'] },
{ name: '月之暗面(Kimi)', url: 'https://api.moonshot.cn/v1', models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'] },
{ name: '豆包(字节)', url: 'https://ark.cn-beijing.volces.com/api/v3', models: ['doubao-1.5-pro-32k', 'doubao-1.5-lite-32k'] },
{ name: '百川', url: 'https://api.baichuan-ai.com/v1', models: ['Baichuan4'] },
{ name: '零一万物', url: 'https://api.lingyiwanwu.com/v1', models: ['yi-large', 'yi-medium'] },
{ name: '自定义', url: '', models: [] }
];
var AI_PROMPT = '你是大学操作系统课程的作业评分助教。根据参考答案对学生回答评分。满分100分,根据回答质量给分:核心思路正确完整90-100,基本正确有小错70-89,部分正确有明显错误50-69,大部分错误30-49,完全错误0-29。评语50字以内中文。重要要求:必须且只能返回纯JSON格式,绝不能包含任何Markdown标记(如```json),绝不能包含任何解释性文字!格式严格为:{"score":分数,"comment":"评语"}';
var P = document.createElement('div');
P.id = 'gp44';
var providerOpts = '';
for (var pi = 0; pi < PROVIDERS.length; pi++) providerOpts += '';
P.innerHTML = ''
+''
+'
互评助手 v6.0
'
+''
+'
就绪
'
+'
评分模式
'
+'
'
+'
默认满分
'
+'
AI自动评分
'
+'
导出题目数据
'
+'
导入评分结果
'
+'
'
+'
默认满分:所有学生直接打100分,自动遍历
'
+'
'
+'
'
+'
'
+'
'
+'
'
+'
'
+'
'
+'
'
+'
'
+'
'
+'
选供应商 → 填Key → 点获取模型
'
+'
'
+'
'
+'
'
+'
'
+'
'
+'
'
+'
或从文件导入:
'
+'
'
+'
'
+'
'
+' 跳过已评过的学生'
+' '
+'
'
+'
'
+' '
+' '
+'
'
+'
'
+'
'
+'
运行日志
'
+'
'
+'
';
document.body.appendChild(P);
var $log=document.getElementById('gp-lg'),$body=document.getElementById('gp-body'),$ai=document.getElementById('gp-ai'),$imp=document.getElementById('gp-imp');
var mode='full',imported=[],exportDirHandle=null,exportDirName='';
function setStatus(text,ok){
var dot=document.getElementById('gp-dot'),txt=document.getElementById('gp-st-text');
if(dot)dot.style.background=(ok===false)?'#f38ba8':'#a6e3a1';
if(txt)txt.textContent=text;
}
function log(m,c){c=c||'i';var d=document.createElement('div');d.className=c;d.textContent='['+new Date().toLocaleTimeString()+'] '+m;$log.appendChild(d);$log.scrollTop=$log.scrollHeight;console.log('[互评] '+m)}
function save(k,v){try{localStorage.setItem('gp44_'+k,typeof v==='string'?v:JSON.stringify(v))}catch(e){}}
function load(k){try{return localStorage.getItem('gp44_'+k)}catch(e){return null}}
function loadJ(k){try{var s=localStorage.getItem('gp44_'+k);return s?JSON.parse(s):null}catch(e){return null}}
function del(k){try{localStorage.removeItem('gp44_'+k)}catch(e){}}
function getSelText(id){var el=document.getElementById(id);return(el&&el.selectedIndex>=0)?el.options[el.selectedIndex].text:''}
function getSelValue(id){var el=document.getElementById(id);return el?el.value:''}
function getModel(){var v=document.getElementById('gp-mdl').value.trim();return v||load('gp-mdl')||'deepseek-chat'}
function shouldSkip(){return document.getElementById('gp-skip').checked}
function onProviderChange(){
var idx=parseInt(getSelValue('gp-provider')),p=PROVIDERS[idx];if(!p)return;
save('gp-provider',idx);
// 只填入默认URL,不保存(用户手动改才保存)
document.getElementById('gp-url').value=p.url;
var inp=document.getElementById('gp-mdl'),sel=document.getElementById('gp-mdl-sel');
sel.innerHTML='';
if(p.models.length){
for(var i=0;i0}
var noSubmit=text.indexOf('该学生未提交作业')>=0;
var hasFile=!!(document.querySelector('#downfile')||document.querySelector('input[value*="下载"]'));
if(!hasFile&&!noSubmit&&qType==='综合题')hasFile=true;
var mainEl=document.querySelector('.main')||document.body;
var pageText=mainEl.innerText||'';
var alreadyGraded = pageText.indexOf('您评定的成绩') >= 0 || pageText.indexOf('评定时间') >= 0 || pageText.indexOf('评定成功') >= 0;
if(!alreadyGraded){
var gradeText=document.body.innerText||'';
if(gradeText.indexOf('评定时间') >= 0 || gradeText.indexOf('您评定的成绩') >= 0 || gradeText.indexOf('评定成功') >= 0) alreadyGraded=true
}
if(!alreadyGraded){
var submitted=loadJ('submitted')||{};
var key=(getSelText('MainContent_dropTitleList')+'|||'+getSelText('MainContent_dropStudent'));
if(submitted[key])alreadyGraded=true
}
return{qType:qType,qText:qText,refAns:refAns,stuAns:stuAns,hasAns:hasAns,hasFile:hasFile,noSubmit:noSubmit,alreadyGraded:alreadyGraded}
}
function setScore(s,c){
var se=document.getElementById('MainContent_dropScore'),ce=document.getElementById('MainContent_txtRemark');
if(se){se.value=String(s);se.dispatchEvent(new Event('change',{bubbles:true}))}
if(ce){ce.value=c;ce.dispatchEvent(new Event('input',{bubbles:true}))}
}
function markSubmitted(){
var submitted=loadJ('submitted')||{};
var key=getSelText('MainContent_dropTitleList')+'|||'+getSelText('MainContent_dropStudent');
submitted[key]=Date.now();
save('submitted',submitted)
}
function clickSubmit(){
var now=Date.now();
var lastSubmit=load('lastSubmitTime');
var cooldown=15500;
if(lastSubmit&&(now-parseInt(lastSubmit))|]/g,'_');
var student=getSelText('MainContent_dropStudent').replace(/[\t\/\\:*?"<>|]/g,'_');
var ct=r.headers.get('Content-Type')||'',ext='.doc';
if(ct.indexOf('pdf')>=0)ext='.pdf';else if(ct.indexOf('zip')>=0)ext='.zip';else if(ct.indexOf('rar')>=0)ext='.rar';
var fn=title+'_'+student+ext;
var u=URL.createObjectURL(blob),a=document.createElement('a');a.href=u;a.download=fn;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(u);
log('已下载: '+fn+' ('+(blob.size/1024).toFixed(1)+'KB)','o');cb(true);
})
}).catch(function(e){log('下载异常: '+e.message,'e');cb(false)})
}
async function startTask(t){
del('lastOp');
if(t==='export'){
if(window.showDirectoryPicker){try{log('请选择保存文件夹...','i');var h=await window.showDirectoryPicker({mode:'readwrite'});exportDirName='互评导出_'+new Date().toISOString().slice(0,19).replace(/[:\-T]/g,'');exportDirHandle=await h.getDirectoryHandle(exportDirName,{create:true});log('保存到: '+h.name+'/'+exportDirName,'o') }catch(e){log('已取消','w');setStatus('已取消',false);return}}
else log('浏览器不支持选文件夹,用默认下载','w')
}
var ts=document.getElementById('MainContent_dropTitleList'),ss=document.getElementById('MainContent_dropStudent');
var st={task:t,tIdx:ts?ts.selectedIndex:0,sIdx:ss?ss.selectedIndex:0,phase:'work',data:[]};
save('task',t);save('state',st);log('启动 ['+t+']','o');setStatus('运行中...',true);doStep()
}
function doStep(){
var t=load('task'),s=loadJ('state');if(!t||!s)return;
if(s.phase==='work'){
var pg=readPage();if(!pg)return;
if(pg.alreadyGraded&&shouldSkip()){
log(getSelText('MainContent_dropStudent')+' 已评过,跳过','w');
skipAdvance(s);return
}
if(t==='full')doFull(s);else if(t==='ai')doAI(s);else if(t==='export')doExport(s);else if(t==='import')doImport(s)
}else if(s.phase==='advance')doAdvance(s)
}
function skipAdvance(s){
var ts=document.getElementById('MainContent_dropTitleList'),ss=document.getElementById('MainContent_dropStudent');if(!ts||!ss)return;
if(s.sIdx+1 '+ss.options[s.sIdx].text,'w');
__doPostBack('ctl00$MainContent$dropStudent','')
}else if(s.tIdx+1 下一题 '+ts.options[s.tIdx].text,'w');
__doPostBack('ctl00$MainContent$dropTitleList','')
}else{
if(s.task==='export'&&s.data&&s.data.length)finishExport(s.data);
del('task');del('state');log('全部完成!','o');setStatus('全部完成!',true)
}
}
function waitAndRun(fn,s){fn(s)}
function doFull(s){
var pg=readPage();if(!pg)return;var stu=getSelText('MainContent_dropStudent');
if(pg.alreadyGraded&&shouldSkip()){log(stu+' 已评过,跳过','w');s.phase='advance';save('state',s);doAdvance(s);return}
waitAndRun(doFullSubmit,s)
}
function doFullSubmit(s){
var pg=readPage();if(!pg)return;if(pg.alreadyGraded&&shouldSkip()){doAdvance(s);return}
var stu=getSelText('MainContent_dropStudent');
if(pg.noSubmit){setScore(0,'该学生未提交作业。');log(stu+' -> 0分','w')}else{setScore(100,'回答得很好,100分。');log(stu+' -> 100分','o')}
s.phase='advance';save('state',s);clickSubmit()
}
function doAI(s){
var pg=readPage();if(!pg)return;var stu=getSelText('MainContent_dropStudent');
if(pg.alreadyGraded&&shouldSkip()){log(stu+' 已评过,跳过','w');s.phase='advance';save('state',s);doAdvance(s);return}
if(pg.noSubmit){setScore(0,'该学生未提交作业。');s.phase='advance';save('state',s);clickSubmit();return}
if(pg.qType==='综合题'&&pg.hasFile&&!pg.hasAns){s.phase='advance';save('state',s);doAdvance(s);return}
if(!pg.hasAns){setScore(0,'未提交答案。');s.phase='advance';save('state',s);clickSubmit();return}
doAICall(s)
}
function doAICall(s){
var pg=readPage();if(!pg)return;if(pg.alreadyGraded&&shouldSkip()){doAdvance(s);return}
var stu=getSelText('MainContent_dropStudent');
var url=document.getElementById('gp-url').value.trim(),key=document.getElementById('gp-key').value.trim(),mdl=getModel();
if(!key){log('请填写API Key','e');return}
log('AI评分: '+stu+' ['+mdl+']...','i');
callAI(url,key,mdl,AI_PROMPT,pg).then(function(r){
if(r){setScore(r.score,r.comment);log('-> '+r.score+'分','o');s.phase='advance';save('state',s);clickSubmit()}
else{log('AI打分失败,已停止任务,请查看日志排查。','e');setStatus('AI错误,已停止',false);del('task');del('state')}
})
}
function doExport(s){
var pg=readPage();if(!pg)return;
if(pg.alreadyGraded&&shouldSkip()){log(getSelText('MainContent_dropStudent')+' 已评过,跳过','w');s.phase='advance';save('state',s);doAdvance(s);return}
s.data=s.data||[];
s.data.push({title:getSelText('MainContent_dropTitleList'),student:getSelText('MainContent_dropStudent'),qType:pg.qType,qText:pg.qText,refAns:pg.refAns,stuAns:pg.stuAns,hasAns:pg.hasAns,noSubmit:pg.noSubmit,hasFile:pg.hasFile});
save('state',s);log('导出: '+getSelText('MainContent_dropStudent'),'i');setStatus('导出中 '+s.data.length+'条',true);
if(pg.qType==='综合题'&&pg.hasFile&&!pg.noSubmit){downloadStudentFile(function(){s.phase='advance';save('state',s);doAdvance(s)})}
else{s.phase='advance';save('state',s);doAdvance(s)}
}
function doImport(s){
if(!imported.length){log('请先导入','e');del('task');del('state');return}
var title=getSelText('MainContent_dropTitleList'),student=getSelText('MainContent_dropStudent'),pg=readPage();
if(pg&&pg.alreadyGraded&&shouldSkip()){log(student+' 已评过,跳过','w');s.phase='advance';save('state',s);doAdvance(s);return}
var matched=null;
for(var i=0;i 0 && j.choices[0].message){
reply = j.choices[0].message.content || '';
} else if (j.response) {
reply = j.response;
} else if (j.data && j.data.response) {
reply = j.data.response;
}
if(!reply || reply.trim() === '') {
log('AI未返回有效内容!服务器回包:\n' + JSON.stringify(j), 'e');
return null;
}
reply=reply.replace(/```json\s*/g,'').replace(/```\s*/g,'').trim();
var score = -1;
var comment = "回答正确。";
try {
var p = JSON.parse(reply);
if (p.score !== undefined) {
score = parseInt(p.score);
comment = p.comment || comment;
}
} catch(e) {
var nm = reply.match(/"?score"?\s*[::]\s*(\d+)/i) || reply.match(/分数\s*[::]\s*(\d+)/i) || reply.match(/^(\d{1,3})$/);
var cm = reply.match(/"?comment"?\s*[::]\s*"([^"]+)"/i) || reply.match(/评语\s*[::]\s*"([^"]+)"/i);
if(nm) {
score = parseInt(nm[1]);
if(cm) comment = cm[1];
}
}
if (score >= 0 && score <= 100) {
return {score: score, comment: String(comment).substring(0,50)};
} else {
log('解析失败!AI返回:\n' + reply, 'e');
return null;
}
}catch(e){
log('API调用异常:'+e.message,'e');
return null;
}
}
function doAdvance(s){
var ts=document.getElementById('MainContent_dropTitleList'),ss=document.getElementById('MainContent_dropStudent');if(!ts||!ss)return;
if(s.sIdx+1导出完成! 共 '+data.length+' 条 (可评 '+gradable.length+' 条)
'
+'- 点下方 [复制Prompt] 按钮
'
+'- 打开任意AI(ChatGPT/DeepSeek/通义等)
'
+'- 粘贴发送,AI返回评分JSON
'
+'- 回到本脚本选 [导入评分结果]
'
+'- 粘贴AI返回的JSON → 点开始
'
+''
+''
+'';
document.getElementById('gp-copy-btn').onclick=function(e){
e.preventDefault();var ta=document.getElementById('gp-copy-ta');ta.select();
navigator.clipboard.writeText(ta.value).then(function(){
document.getElementById('gp-copy-btn').textContent='已复制! 去粘贴给AI吧';document.getElementById('gp-copy-btn').className='btn btn-purple';
log('已复制到剪贴板!','o');
setTimeout(function(){document.getElementById('gp-copy-btn').textContent='复制Prompt到剪贴板';document.getElementById('gp-copy-btn').className='btn btn-green'},3000)
}).catch(function(){ta.style.height='200px';ta.select();log('自动复制失败,请手动Ctrl+C','w')})
};
document.getElementById('gp-toggle-text').onclick=function(e){
e.preventDefault();var ta=document.getElementById('gp-copy-ta');
ta.style.height=(ta.style.height==='0px'||ta.style.height==='')?'250px':'0px';ta.style.overflow=(ta.style.height==='0px')?'hidden':'auto'
};
log('====================','o');log('导出完成! 共 '+data.length+' 条','o');log('可评分: '+gradable.length+' | 跳过: '+skipped.length,'o');
if(exportDirHandle)log('文件已保存到: '+exportDirName+'/','o');else log('文件已下载','o');
log('↑ 点绿色按钮复制Prompt给AI','o');log('====================','o');setStatus('完成! 复制Prompt给AI',true)
}
async function checkResume(){var t=load('task'),s=loadJ('state');if(!t||!s)return;log('恢复任务: '+t,'w');setStatus('恢复运行...',true);mode=t;
var btns=document.querySelectorAll('#gp-modes .m');for(var i=0;i默认满分:所有学生直接打100分,自动遍历',ai:'AI自动评分:选供应商→填Key→获取模型→开始,每15秒一题',export:'导出流程:开始→遍历→完成后复制Prompt给AI→AI返回JSON→导入',import:'导入流程:粘贴AI返回的JSON→点导入→开始自动填入'};
h.innerHTML=hints[mode]||''}})(modeBtns[i])}
document.getElementById('gp-go').onclick=function(e){e.preventDefault();startTask(mode)};
document.getElementById('gp-stop').onclick=function(e){e.preventDefault();del('task');del('state');exportDirHandle=null;log('已停止','w');setStatus('已停止',false)};
document.getElementById('gp-exp').onclick=function(e){e.preventDefault();startTask('export')};
document.getElementById('gp-provider').onchange=function(e){e.preventDefault();onProviderChange()};
document.getElementById('gp-fetch').onclick=function(e){e.preventDefault();fetchModels()};
document.getElementById('gp-mdl').oninput=function(){save('gp-mdl',this.value)};
document.getElementById('gp-mdl-sel').onclick=function(e){e.preventDefault();if(this.value){document.getElementById('gp-mdl').value=this.value;save('gp-mdl',this.value)}};
document.getElementById('gp-mdl-sel').onchange=function(){if(this.value){document.getElementById('gp-mdl').value=this.value;save('gp-mdl',this.value)}};
document.getElementById('gp-ai1').onclick=function(e){e.preventDefault();var pg=readPage();if(!pg)return;var url=document.getElementById('gp-url').value.trim(),key=document.getElementById('gp-key').value.trim(),mdl=getModel();
if(!key){log('请填写API Key','e');return}log('AI评当前题 ['+mdl+']...','i');callAI(url,key,mdl,AI_PROMPT,pg).then(function(r){if(r){setScore(r.score,r.comment);log('-> '+r.score+'分','o')}else log('AI失败','e')})};
document.getElementById('gp-file').onchange=function(e){var f=e.target.files[0];if(!f)return;var rd=new FileReader();rd.onload=function(ev){try{imported=JSON.parse(ev.target.result);log('导入成功: '+imported.length+'条','o')}catch(e){log('JSON格式错误','e')}};rd.readAsText(f)};
document.getElementById('gp-paste-btn').onclick=function(e){e.preventDefault();var t=document.getElementById('gp-paste').value.trim();if(!t){log('请先粘贴JSON','e');return}
try{imported=JSON.parse(t);log('粘贴导入: '+imported.length+'条','o');document.getElementById('gp-paste-btn').textContent='已导入 '+imported.length+' 条!';setTimeout(function(){document.getElementById('gp-paste-btn').textContent='导入粘贴的评分'},2000)}catch(e){log('JSON格式错误','e')}};
var skipEl=document.getElementById('gp-skip');
skipEl.onchange=function(){save('gp-skip',this.checked?'1':'0')};
var savedSkip=load('gp-skip');
skipEl.checked=(savedSkip!=='0');
document.getElementById('gp-url').onchange=function(){save('gp-url',this.value)};
var savedUrl = load('gp-url');
var savedMdl = load('gp-mdl');
var sp=load('gp-provider');
if(sp!==null){
document.getElementById('gp-provider').value=sp;
onProviderChange();
} else {
onProviderChange();
}
if(savedUrl) { document.getElementById('gp-url').value = savedUrl; save('gp-url', savedUrl); }
if(savedMdl) { document.getElementById('gp-mdl').value = savedMdl; save('gp-mdl', savedMdl); }
var sk=load('gp-key');if(sk)document.getElementById('gp-key').value=sk;
document.getElementById('gp-key').onchange=function(){save('gp-key',this.value)};
// 清除API Key按钮
document.getElementById('gp-clear-key').onclick=function(e){
e.preventDefault();
del('gp-key');
document.getElementById('gp-key').value='';
log('API Key已清除','o');
};
// 隐私提示
if(!load('privacy_acknowledged')){
var ack=confirm('隐私提示:\n\nAI评分模式会将【题目、参考答案、学生回答】发送到你选择的AI供应商服务器。\n\n请确认你了解此风险后继续使用。\n\n点击"确定"不再提示,点"取消"暂不使用AI功能。');
if(ack)save('privacy_acknowledged','1');
}
log('脚本已加载 v6.0','o');checkResume();
}catch(err){console.error('[互评脚本] 错误:',err);alert('[互评脚本] 出错: '+err.message)}
})();