diff --git a/Update.json b/Update.json index 35811b63..c9667873 100644 --- a/Update.json +++ b/Update.json @@ -3716,6 +3716,17 @@ } ], "Notes": "优化“获取数据”功能:页面缺少数据申请区域时会自动创建控件;默认使用 gzip + Base93 精确保留任意输入;新增约 310 MB/s 的 NSC3 高速数值模式,同时压缩 long long 数值与行结构并对混合内容原样回退;新增“保留换行”选项,默认精确保留,使用 cin >> 等空白不敏感读取时可关闭,从而避免大量随机行长占用输出;生成程序现在先向 stderr 输出并刷新数据,再通过 abort() 可靠触发 RE,同时兼容读取旧版异常包装的结果。" + }, + "3.6.4": { + "UpdateDate": 1787460024276, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1019, + "Description": "fix: 在提交界面显示并提交验证码" + } + ], + "Notes": "### 修复提交界面不显示验证码\n\n评测队列繁忙时 XMOJ 会启用图片验证码,但提交界面是脚本自行渲染的,服务端的验证码字段被丢弃,导致提交静默失败。现在提交界面会显示验证码图片与输入框(点击图片可更换),并在提交时携带验证码。\n\n另新增 `AutoCaptcha` 开关(默认开启):在浏览器本地用模板匹配识别 4 位数字验证码,不联网、不调用任何 AI 服务。把握不大时会留空由您填写,不会填错。" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index 3cb45de7..37322f82 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.6.3 +// @version 3.6.4 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen @@ -551,6 +551,34 @@ let _earlyObs = null; })(); const CaptchaSiteKey = "0x4AAAAAAALBT58IhyDViNmv"; +// XMOJ turns on its own image captcha (vcode.php) whenever the judge queue is busy. submitpage.php +// renders the field at >10 pending solutions but submit.php only enforces it at >50, so the two can +// disagree; see GetCaptchaParameter below for how that gap is handled. +// vcode.php draws 4 digits with imagettftext(size 15, Vera.ttf) at a fixed origin, so every glyph is +// pixel identical between challenges and template matching reads them exactly. That beats sending the +// image to a vision model: llava-1.5-7b, llama-4-scout and mistral-small-3.1 were all measured at 0/7 +// on real captchas even after preprocessing, and a confidently wrong guess is worse than an empty box +// because it burns the attempt and trips vfail. Glyphs below are Vera.ttf rendered at 20px, which is +// the 15px cap height GD produces; '#' would be ink, stored as 1 bits row by row. +const CaptchaGlyphs = { + "0": "001111000|011111110|011000110|111000111|110000011|110000011|110011011|110011011|110000011|110000011|110000011|111000111|011000110|011111110|001111000", + "1": "00111000|11111000|11011000|00011000|00011000|00011000|00011000|00011000|00011000|00011000|00011000|00011000|00011000|11111111|11111111", + "2": "011111100|111111110|100000111|000000011|000000011|000000011|000000111|000000110|000001100|000011000|000110000|001100000|011000000|111111111|111111111", + "3": "011111100|111111110|100000111|000000011|000000011|000000111|000111110|000111100|000000110|000000011|000000011|000000011|100000111|111111110|011111100", + "4": "0000011100|0000011100|0000111100|0000101100|0001101100|0001001100|0011001100|0110001100|0110001100|1100001100|1111111111|1111111111|0000001100|0000001100|0000001100", + "5": "011111110|011111110|011000000|011000000|011000000|011111100|011111110|010000111|000000011|000000011|000000011|000000011|100000110|111111110|011111000", + "6": "000111100|001111110|011100010|011000000|110000000|110000000|110111100|111111110|111000111|110000011|110000011|110000011|011000111|011111110|001111100", + "7": "111111111|111111111|000000110|000000110|000000110|000001100|000001100|000001100|000011000|000011000|000011000|000110000|000110000|000110000|001100000", + "8": "001111100|011111110|111000111|110000011|110000011|011000110|001111100|011111110|011000110|110000011|110000011|110000011|111000111|011111110|001111100", + "9": "001111100|011111110|111000110|110000011|110000011|110000011|111000111|011111111|001111011|000000011|000000011|000000110|010001110|011111100|001111000" +}; +// How far ahead the best matching glyph has to score before its digit is trusted. Measured over 300 +// generated and 40 live captchas, 4 is the point where every wrong answer turns into a decline: it +// still fills in about three quarters of them and never once guessed wrong. +const CaptchaMinMargin = 4; +// Settings that start off rather than on. Both UtilityEnabled and the settings list seed missing +// values, so they have to agree or whichever runs first decides the default. +const DefaultOffSettings = ["DebugMode", "SuperDebug", "ReplaceXM"]; // 0.53.0 leaks its minified helper variables (m, r, o, ...) into the global scope from every // chunk file, so whichever chunk happens to be evaluated last clobbers the others and the // editor randomly fails to load (microsoft/monaco-editor#5015). 0.52.2 ships a single bundle @@ -1491,8 +1519,7 @@ let TidyTable = (Table) => { let UtilityEnabled = (Name) => { try { if (localStorage.getItem("UserScript-Setting-" + Name) == null) { - const defaultOffItems = ["DebugMode", "SuperDebug", "ReplaceXM"]; - localStorage.setItem("UserScript-Setting-" + Name, defaultOffItems.includes(Name) ? "false" : "true"); + localStorage.setItem("UserScript-Setting-" + Name, DefaultOffSettings.includes(Name) ? "false" : "true"); } return localStorage.getItem("UserScript-Setting-" + Name) == "true"; } catch (e) { @@ -2807,7 +2834,7 @@ async function main() { CheckBox.type = "checkbox"; CheckBox.id = Data[i].ID; if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == null) { - localStorage.setItem("UserScript-Setting-" + Data[i].ID, "true"); + localStorage.setItem("UserScript-Setting-" + Data[i].ID, DefaultOffSettings.includes(Data[i].ID) ? "false" : "true"); } if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == "false") { CheckBox.checked = false; @@ -2855,6 +2882,8 @@ async function main() { }, {"ID": "DownloadPlayback", "Type": "A", "Name": "回放视频增加下载功能"}, { "ID": "ImproveACRate", "Type": "A", "Name": "自动提交已AC题目以提高AC率" }, {"ID": "AutoO2", "Type": "F", "Name": "代码提交界面自动选择O2优化"}, { + "ID": "AutoCaptcha", "Type": "A", "Name": "自动识别提交界面的验证码(本地识别,把握不大时留空由您填写)" + }, { "ID": "Beautify", "Type": "F", "Name": "美化界面", "Children": [{ "ID": "NewTopBar", "Type": "F", "Name": "使用新的顶部导航栏" }, { @@ -4084,6 +4113,13 @@ async function main() { } } else if (location.pathname == "/submitpage.php") { document.title = "提交代码: " + (SearchParams.get("id") != null ? "题目" + Number(SearchParams.get("id")) : "比赛" + Number(SearchParams.get("cid"))); + // submitpage.php only renders the vcode field while the judge queue is busy, and the + // custom page below throws the server markup away, so read it before that happens. + // The queue is idle almost all of the time, so the only other way to reach this state + // is to flood the judge; localStorage UserScript-ForceCaptcha=true forces it instead. + const NativeCaptchaShown = document.querySelector("input[name='vcode']") != null || + document.querySelector("img#vcode") != null || + localStorage.getItem("UserScript-ForceCaptcha") === "true"; document.querySelector("body > div > div.mt-3").innerHTML = `
Loading...
@@ -4092,6 +4128,12 @@ async function main() {