From 586d9169927557fe62e371d3f250de602bb3b779 Mon Sep 17 00:00:00 2001 From: boomzero Date: Sat, 19 Sep 2026 08:23:12 +0800 Subject: [PATCH 01/15] =?UTF-8?q?fix:=20=E6=AF=94=E8=B5=9B=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E5=90=8E=E5=9B=9E=E9=80=80=E6=8F=90=E4=BA=A4=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=A2=AB=E9=9D=99=E9=BB=98=E4=B8=A2=E5=BC=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回退路径本身可以解析出真实题号,问题在 POST 之后:内层 fetch 只把响应 console.log 掉,从不判断成功与否,随后外层无条件覆盖成"提交失败"。日志里 那次回退实际收到的是 XMOJ 的提交冷却页(`请勿重复提交`,HUSTOJ 的 $OJ_SUBMIT_COOLDOWN_TIME 默认 5 秒,XMOJ 渲染成页面而非 302),响应被丢掉, 所以状态里没有任何提交记录。 - 把回退逻辑抽成 SubmitToEndedContestProblem,返回 {Success, Message} - 只把 redirected 当成功信号,成功后 return,不再被外层覆盖 - 遇到 `请勿重复提交` 等冷却过去后重试(3 秒一次,最多 5 次) - 其余失败从响应的 .jumbotron 取服务端原文,不再显示通用报错 - 遇到 `验证码错误` 立即刷新验证码并停止重试(答案已被消耗) - 题号改用 /\d+/ 提取,原 substring(2, 6) 会把 5 位题号截成 4 位 - 补齐缺失的 GetCaptchaParameter(),并在解析失败时恢复提交按钮 Closes #1017 Co-Authored-By: Claude Opus 5 --- XMOJ.user.js | 168 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 65 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 37322f82..cb9f331c 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -4473,6 +4473,102 @@ async function main() { RefreshCaptcha(""); } + const ShowSubmitStatus = (Message) => { + ErrorElement.style.display = "block"; + ErrorMessage.style.color = "red"; + try { _xmoj_disposeErrorMessageEditors(); } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + ErrorMessage.innerText = Message; + console.log(Message); + }; + + // Credit: https://github.com/boomzero/quicksubmit/blob/main/index.ts + // Also licensed under GPL-3.0 + // The contest is over, so submit.php refuses the cid+pid submission. Look up the + // real problem number on the contest page and submit to that problem instead. + // Returns {Success, Message}; Success means a submission record was created. + async function SubmitToEndedContestProblem(Source, O2Switch, ReportStatus) { + const ContestID = new URL(location.href).searchParams.get("cid"); + const ProblemNumber = new URL(location.href).searchParams.get("pid"); + const ContestResponse = await fetch("https://www.xmoj.tech/contest.php?cid=" + ContestID); + const ContestPage = await ContestResponse.text(); + if (ContestResponse.status !== 200 || ContestPage.indexOf("比赛尚未开始或私有,不能查看题目。") !== -1) { + console.error("Failed to get contest page!"); + return {Success: false, Message: "无法读取比赛页面,未能找到原题题号!"}; + } + let RealPID = undefined; + try { + const ContestDocument = new DOMParser().parseFromString(ContestPage, "text/html"); + const ProblemTable = ContestDocument.querySelector("#problemset > tbody"); + if (ProblemTable === null) { + console.error("Failed to find the problem list of the contest!"); + return {Success: false, Message: "无法解析比赛题目列表,未能找到原题题号!"}; + } + const ContestProblems = []; + for (let i = 0; i < ProblemTable.rows.length; i++) { + // The 题号 cell is padded with newlines and tabs; a fixed substring(2, 6) + // truncates any problem number that is not exactly four digits. + const ProblemNumberMatch = ProblemTable.rows[i].children[1].textContent.match(/\d+/); + ContestProblems.push(ProblemNumberMatch === null ? "" : ProblemNumberMatch[0]); + } + RealPID = ContestProblems[ProblemNumber]; + if (UtilityEnabled("DebugMode")) { + console.log("Contest Problems:", ContestProblems); + console.log("Real PID:", RealPID); + } + } catch (e) { + console.error(e); + return {Success: false, Message: "无法解析比赛题目列表,未能找到原题题号!"}; + } + if (RealPID === undefined || RealPID === "") { + return {Success: false, Message: "无法确定原题题号,请手动前往原题提交!"}; + } + // XMOJ rejects anything submitted within a few seconds of the previous submission + // with 请勿重复提交, so wait the cooldown out instead of silently dropping the code. + for (let Attempt = 0; Attempt < 5; Attempt++) { + ReportStatus("比赛已结束, 正在尝试向题目 " + RealPID + " 提交"); + const SubmitResponse = await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": location.href, + "method": "POST", + "body": "id=" + RealPID + "&language=1&" + "source=" + encodeURIComponent(Source) + O2Switch + GetCaptchaParameter() + }); + if (SubmitResponse.redirected) { + location.href = SubmitResponse.url; + return {Success: true, Message: ""}; + } + const SubmitPage = await SubmitResponse.text(); + if (UtilityEnabled("DebugMode")) { + console.log("Direct submission response:", SubmitPage); + } + // Retrying cannot help here: the answer that was sent has already been spent. + if (SubmitPage.indexOf("验证码错误") !== -1) { + await RefreshCaptcha(""); + document.querySelector("#vcode").focus(); + return {Success: false, Message: "验证码错误!请填写上方的验证码后重新提交。"}; + } + if (SubmitPage.indexOf("请勿重复提交") === -1) { + let ServerMessage = ""; + try { + const MessageElement = new DOMParser().parseFromString(SubmitPage, "text/html").querySelector(".jumbotron"); + if (MessageElement !== null) ServerMessage = MessageElement.textContent.trim(); + } catch (e) { + console.error(e); + } + return {Success: false, Message: "向题目 " + RealPID + " 提交失败!" + (ServerMessage === "" ? "请关闭脚本后重试!" : ServerMessage)}; + } + ReportStatus("提交过于频繁, 3 秒后重新尝试向题目 " + RealPID + " 提交"); + await new Promise((Resolve) => setTimeout(Resolve, 3000)); + } + return {Success: false, Message: "向题目 " + RealPID + " 提交失败!提交过于频繁,请稍后手动重试!"}; + } + PassCheck.addEventListener("click", async () => { // This is the request that actually reaches submit.php, so the captcha is checked // here as well as in the 提交 handler above. @@ -4515,76 +4611,18 @@ async function main() { document.querySelector("#vcode").focus(); return; } - if (text.indexOf("没有这个比赛!") !== -1 && new URL(location.href).searchParams.get("pid") !== null) { - // Credit: https://github.com/boomzero/quicksubmit/blob/main/index.ts - // Also licensed under GPL-3.0 - const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + new URL(location.href).searchParams.get("cid")); - const res = await contestReq.text(); - if ( - contestReq.status !== 200 || - res.indexOf("比赛尚未开始或私有,不能查看题目。") !== -1 - ) { - console.error(`Failed to get contest page!`); - return; - } - const parser = new DOMParser(); - const dom = parser.parseFromString(res, "text/html"); - const contestProblems = []; - const rows = (dom.querySelector( - "#problemset > tbody", - )).rows; - for (let i = 0; i < rows.length; i++) { - contestProblems.push( - rows[i].children[1].textContent.substring(2, 6).replaceAll( - "\t", - "", - ), - ); - } - rPID = contestProblems[new URL(location.href).searchParams.get("pid")]; - if (UtilityEnabled("DebugMode")) { - console.log("Contest Problems:", contestProblems); - console.log("Real PID:", rPID); - } - ErrorElement.style.display = "block"; - ErrorMessage.style.color = "red"; - try { _xmoj_disposeErrorMessageEditors(); } catch (e) { - console.error(e); - if (UtilityEnabled("DebugMode")) { - SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); - } - } - ErrorMessage.innerText = "比赛已结束, 正在尝试向题目 " + rPID + " 提交"; - console.log("比赛已结束, 正在尝试向题目 " + rPID + " 提交"); - let o2Switch = "&enable_O2=on"; - if (!document.querySelector("#enable_O2").checked) o2Switch = ""; - await fetch("https://www.xmoj.tech/submit.php", { - "headers": { - "content-type": "application/x-www-form-urlencoded" - }, - "referrer": location.href, - "method": "POST", - "body": "id=" + rPID + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + GetCaptchaParameter() - }).then(async (Response) => { - if (Response.redirected) { - location.href = Response.url; - } - console.log(await Response.text()); - }); - - } if (UtilityEnabled("DebugMode")) { console.log("Submission failed! Response:", text); } - ErrorElement.style.display = "block"; - ErrorMessage.style.color = "red"; - try { _xmoj_disposeErrorMessageEditors(); } catch (e) { - console.error(e); - if (UtilityEnabled("DebugMode")) { - SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + let FailMessage = "提交失败!请关闭脚本后重试!"; + if (text.indexOf("没有这个比赛!") !== -1 && SearchParams.get("pid") !== null) { + const FallbackResult = await SubmitToEndedContestProblem(CodeMirrorElement.getValue(), o2Switch, ShowSubmitStatus); + if (FallbackResult.Success) { + return; } + FailMessage = FallbackResult.Message; } - ErrorMessage.innerText = "提交失败!请关闭脚本后重试!"; + ShowSubmitStatus(FailMessage); Submit.disabled = false; Submit.value = "提交"; } From e32a469e83c64165ef9f157f051826003b0aee12 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 00:24:36 +0000 Subject: [PATCH 02/15] 3.6.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8537fedb..c50f9fb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.6.4", + "version": "3.6.5", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From af887e02f0f8576133846c7f39b0865dc9063288 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 00:24:42 +0000 Subject: [PATCH 03/15] Update version info to 3.6.5 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index c9667873..1690bebb 100644 --- a/Update.json +++ b/Update.json @@ -3727,6 +3727,17 @@ } ], "Notes": "### 修复提交界面不显示验证码\n\n评测队列繁忙时 XMOJ 会启用图片验证码,但提交界面是脚本自行渲染的,服务端的验证码字段被丢弃,导致提交静默失败。现在提交界面会显示验证码图片与输入框(点击图片可更换),并在提交时携带验证码。\n\n另新增 `AutoCaptcha` 开关(默认开启):在浏览器本地用模板匹配识别 4 位数字验证码,不联网、不调用任何 AI 服务。把握不大时会留空由您填写,不会填错。" + }, + "3.6.5": { + "UpdateDate": 1789777477231, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1021, + "Description": "fix: 比赛结束后回退提交不再被静默丢弃" + } + ], + "Notes": "修复比赛结束后自动向原题提交的回退功能。此前回退提交的响应被丢弃,遇到提交冷却时会静默失败并显示\"提交失败\",状态页里也没有任何提交记录。" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index cb9f331c..1e73d749 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.6.4 +// @version 3.6.5 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From ab272eed614320bbba146059f970eb6a7c105f5f Mon Sep 17 00:00:00 2001 From: boomzero Date: Sat, 19 Sep 2026 08:28:50 +0800 Subject: [PATCH 04/15] =?UTF-8?q?fix:=20=E5=9B=9E=E9=80=80=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=AF=8F=E6=AC=A1=E9=87=8D=E8=AF=95=E5=89=8D=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E6=A3=80=E6=9F=A5=E9=AA=8C=E8=AF=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 冷却等待期间验证码输入框和刷新按钮仍可交互。如果用户在这 3 秒里清空了 输入框或刷新了图片,下一次重试会用空的 GetCaptchaParameter() 发出请求, 而空答案会让服务端把本 session 的 4 位验证码换成 8 位。 在循环每次 POST 之前调用 CaptchaIsMissing()(它自己会提示并恢复按钮), 并用 Handled 标志让外层直接返回,不覆盖它设置的提示。 Co-Authored-By: Claude Opus 5 --- XMOJ.user.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 1e73d749..e8fbf499 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -4530,6 +4530,11 @@ async function main() { // XMOJ rejects anything submitted within a few seconds of the previous submission // with 请勿重复提交, so wait the cooldown out instead of silently dropping the code. for (let Attempt = 0; Attempt < 5; Attempt++) { + // The captcha field stays editable while we fetch contest.php and while we wait + // out a cooldown, so re-check it before every POST rather than trusting the + // check the 提交 handler did. Sending a blank answer would burn the session. + // CaptchaIsMissing() already shows its own message and restores the button. + if (CaptchaIsMissing()) return {Success: false, Handled: true, Message: ""}; ReportStatus("比赛已结束, 正在尝试向题目 " + RealPID + " 提交"); const SubmitResponse = await fetch("https://www.xmoj.tech/submit.php", { "headers": { @@ -4617,7 +4622,7 @@ async function main() { let FailMessage = "提交失败!请关闭脚本后重试!"; if (text.indexOf("没有这个比赛!") !== -1 && SearchParams.get("pid") !== null) { const FallbackResult = await SubmitToEndedContestProblem(CodeMirrorElement.getValue(), o2Switch, ShowSubmitStatus); - if (FallbackResult.Success) { + if (FallbackResult.Success || FallbackResult.Handled) { return; } FailMessage = FallbackResult.Message; From ad0eb05e6fd30a53fa5ef969069b1ada72e600f0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 00:29:55 +0000 Subject: [PATCH 05/15] Update time and description of 3.6.5 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 1690bebb..48b78ebd 100644 --- a/Update.json +++ b/Update.json @@ -3729,7 +3729,7 @@ "Notes": "### 修复提交界面不显示验证码\n\n评测队列繁忙时 XMOJ 会启用图片验证码,但提交界面是脚本自行渲染的,服务端的验证码字段被丢弃,导致提交静默失败。现在提交界面会显示验证码图片与输入框(点击图片可更换),并在提交时携带验证码。\n\n另新增 `AutoCaptcha` 开关(默认开启):在浏览器本地用模板匹配识别 4 位数字验证码,不联网、不调用任何 AI 服务。把握不大时会留空由您填写,不会填错。" }, "3.6.5": { - "UpdateDate": 1789777477231, + "UpdateDate": 1789777790191, "Prerelease": true, "UpdateContents": [ { From 157e9b2f444eaee4fd17327139f8f24c6f153a18 Mon Sep 17 00:00:00 2001 From: boomzero Date: Sat, 19 Sep 2026 08:32:59 +0800 Subject: [PATCH 06/15] =?UTF-8?q?fix:=20=E5=9B=9E=E9=80=80=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E7=9A=84=E7=BD=91=E7=BB=9C=E8=AF=B7=E6=B1=82=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E4=B8=8D=E5=86=8D=E8=AE=A9=E6=8F=90=E4=BA=A4=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=8D=A1=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit contest.php 或 submit.php 的 fetch 被 reject(网络错误)时,异常会一路穿出 SubmitToEndedContestProblem、穿出 .then 回调、穿出 PassCheck 的 async 监听器 ——整条链上没有任何 catch。结果 ShowSubmitStatus 和恢复按钮的两行都不会执行, 提交按钮永远停在"正在提交...",而错误框在监听器开头已经被设成 display: none, 用户什么提示都看不到。这正是本 PR 声称要修掉的那个症状。 把两处网络请求都包进 try/catch,失败时返回错误信息交给外层显示。 Co-Authored-By: Claude Opus 5 --- XMOJ.user.js | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index e8fbf499..1f59a730 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -4494,8 +4494,17 @@ async function main() { async function SubmitToEndedContestProblem(Source, O2Switch, ReportStatus) { const ContestID = new URL(location.href).searchParams.get("cid"); const ProblemNumber = new URL(location.href).searchParams.get("pid"); - const ContestResponse = await fetch("https://www.xmoj.tech/contest.php?cid=" + ContestID); - const ContestPage = await ContestResponse.text(); + // A rejected fetch here would unwind all the way out of the click handler, which + // has no catch, leaving 提交 stuck on 正在提交... with the error box still hidden. + let ContestResponse = undefined; + let ContestPage = ""; + try { + ContestResponse = await fetch("https://www.xmoj.tech/contest.php?cid=" + ContestID); + ContestPage = await ContestResponse.text(); + } catch (e) { + console.error(e); + return {Success: false, Message: "无法读取比赛页面,未能找到原题题号!"}; + } if (ContestResponse.status !== 200 || ContestPage.indexOf("比赛尚未开始或私有,不能查看题目。") !== -1) { console.error("Failed to get contest page!"); return {Success: false, Message: "无法读取比赛页面,未能找到原题题号!"}; @@ -4536,19 +4545,25 @@ async function main() { // CaptchaIsMissing() already shows its own message and restores the button. if (CaptchaIsMissing()) return {Success: false, Handled: true, Message: ""}; ReportStatus("比赛已结束, 正在尝试向题目 " + RealPID + " 提交"); - const SubmitResponse = await fetch("https://www.xmoj.tech/submit.php", { - "headers": { - "content-type": "application/x-www-form-urlencoded" - }, - "referrer": location.href, - "method": "POST", - "body": "id=" + RealPID + "&language=1&" + "source=" + encodeURIComponent(Source) + O2Switch + GetCaptchaParameter() - }); - if (SubmitResponse.redirected) { - location.href = SubmitResponse.url; - return {Success: true, Message: ""}; + let SubmitPage = ""; + try { + const SubmitResponse = await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": location.href, + "method": "POST", + "body": "id=" + RealPID + "&language=1&" + "source=" + encodeURIComponent(Source) + O2Switch + GetCaptchaParameter() + }); + if (SubmitResponse.redirected) { + location.href = SubmitResponse.url; + return {Success: true, Message: ""}; + } + SubmitPage = await SubmitResponse.text(); + } catch (e) { + console.error(e); + return {Success: false, Message: "向题目 " + RealPID + " 提交失败!网络错误,请稍后重试!"}; } - const SubmitPage = await SubmitResponse.text(); if (UtilityEnabled("DebugMode")) { console.log("Direct submission response:", SubmitPage); } From 514bd86044c7c140f3028a08d80aa95319220350 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 00:33:35 +0000 Subject: [PATCH 07/15] Update time and description of 3.6.5 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 48b78ebd..942f091a 100644 --- a/Update.json +++ b/Update.json @@ -3729,7 +3729,7 @@ "Notes": "### 修复提交界面不显示验证码\n\n评测队列繁忙时 XMOJ 会启用图片验证码,但提交界面是脚本自行渲染的,服务端的验证码字段被丢弃,导致提交静默失败。现在提交界面会显示验证码图片与输入框(点击图片可更换),并在提交时携带验证码。\n\n另新增 `AutoCaptcha` 开关(默认开启):在浏览器本地用模板匹配识别 4 位数字验证码,不联网、不调用任何 AI 服务。把握不大时会留空由您填写,不会填错。" }, "3.6.5": { - "UpdateDate": 1789777790191, + "UpdateDate": 1789778010947, "Prerelease": true, "UpdateContents": [ { From ba08aac5b19f2ae394137a43c1dbc9de1c26e38e Mon Sep 17 00:00:00 2001 From: boomzero Date: Sat, 19 Sep 2026 08:52:32 +0800 Subject: [PATCH 08/15] =?UTF-8?q?feat:=20=E9=AA=8C=E8=AF=81=E7=A0=81?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E4=B8=8D=E5=87=BA=E6=97=B6=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=8D=A2=E4=B8=80=E5=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- XMOJ.user.js | 79 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 1f59a730..da150b80 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -576,6 +576,8 @@ const CaptchaGlyphs = { // 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; +// With about three quarters of challenges read, five images leave well under a 1% chance of giving up. +const CaptchaMaxAttempts = 5; // 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"]; @@ -4395,40 +4397,51 @@ async function main() { document.querySelector("#CaptchaElement").style.display = "block"; CaptchaInput.value = ""; SetCaptchaStatus(StatusMessage || ""); - let ImageBlob; - try { - const CaptchaResponse = await fetch("https://www.xmoj.tech/vcode.php?" + Math.random(), {cache: "no-store"}); - ImageBlob = await CaptchaResponse.blob(); - } catch (e) { - console.error(e); - SetCaptchaStatus("验证码加载失败,请点击图片重试"); - return; - } - if (RequestID !== CaptchaRequestID) return; - if (CaptchaObjectURL !== null) URL.revokeObjectURL(CaptchaObjectURL); - CaptchaObjectURL = URL.createObjectURL(ImageBlob); - document.querySelector("#CaptchaImage").src = CaptchaObjectURL; - if (!UtilityEnabled("AutoCaptcha")) return; - let CaptchaLength = 4; - try { - CaptchaLength = await GetCaptchaLength(ImageBlob); - } catch (e) { - console.error(e); - } - if (CaptchaLength !== 4) { - SetCaptchaStatus("本次为 " + CaptchaLength + " 位字母验证码,请手动输入"); - return; - } - const Answer = await SolveCaptcha(ImageBlob); - if (RequestID !== CaptchaRequestID) return; - if (Answer === null) { - SetCaptchaStatus("这张看不太准,请手动输入,或点击图片换一张"); - return; + // Only a submitted answer counts against the session, so fetching another image is + // free: when the solver declines one it is cheaper to ask for a fresh challenge than + // to make the user type it. Each fetch replaces the answer the session expects, so + // this stops as soon as the user starts typing against the image on screen. + for (let Attempt = 1; Attempt <= CaptchaMaxAttempts; Attempt++) { + if (Attempt > 1) { + await new Promise((Resolve) => setTimeout(Resolve, 300)); + if (RequestID !== CaptchaRequestID || CaptchaInput.value !== "") return; + SetCaptchaStatus("这张看不太准,正在自动换一张(" + Attempt + "/" + CaptchaMaxAttempts + ")"); + } + let ImageBlob; + try { + const CaptchaResponse = await fetch("https://www.xmoj.tech/vcode.php?" + Math.random(), {cache: "no-store"}); + ImageBlob = await CaptchaResponse.blob(); + } catch (e) { + console.error(e); + if (RequestID === CaptchaRequestID) SetCaptchaStatus("验证码加载失败,请点击图片重试"); + return; + } + if (RequestID !== CaptchaRequestID) return; + if (CaptchaObjectURL !== null) URL.revokeObjectURL(CaptchaObjectURL); + CaptchaObjectURL = URL.createObjectURL(ImageBlob); + document.querySelector("#CaptchaImage").src = CaptchaObjectURL; + if (!UtilityEnabled("AutoCaptcha")) return; + let CaptchaLength = 4; + try { + CaptchaLength = await GetCaptchaLength(ImageBlob); + } catch (e) { + console.error(e); + } + if (CaptchaLength !== 4) { + SetCaptchaStatus("本次为 " + CaptchaLength + " 位字母验证码,请手动输入"); + return; + } + const Answer = await SolveCaptcha(ImageBlob); + if (RequestID !== CaptchaRequestID) return; + // Never overwrite what the user has already started typing. + if (CaptchaInput.value !== "") return; + if (Answer !== null) { + CaptchaInput.value = Answer; + SetCaptchaStatus("已自动识别,若与图片不符请手动修改"); + return; + } } - // Never overwrite what the user has already started typing. - if (CaptchaInput.value !== "") return; - CaptchaInput.value = Answer; - SetCaptchaStatus("已自动识别,若与图片不符请手动修改"); + SetCaptchaStatus("连续几张都看不太准,请手动输入,或点击图片换一张"); }; document.querySelector("#CaptchaImage").addEventListener("click", () => { RefreshCaptcha(""); From c2b21da5057743e167929384ef7223bad8fdc813 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 00:52:58 +0000 Subject: [PATCH 09/15] 3.6.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c50f9fb0..e6af0b78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.6.5", + "version": "3.6.6", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From 05e1956415377c48a066bafbb0edb5c3384f1837 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 00:53:04 +0000 Subject: [PATCH 10/15] Update version info to 3.6.6 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 942f091a..f6875e83 100644 --- a/Update.json +++ b/Update.json @@ -3738,6 +3738,17 @@ } ], "Notes": "修复比赛结束后自动向原题提交的回退功能。此前回退提交的响应被丢弃,遇到提交冷却时会静默失败并显示\"提交失败\",状态页里也没有任何提交记录。" + }, + "3.6.6": { + "UpdateDate": 1789779179257, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1023, + "Description": "feat: 验证码识别不出时自动换一张" + } + ], + "Notes": "自动识别验证码时,若当前这张看不准,脚本会自动换一张重试(最多 5 张),无需手动点击图片。" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index da150b80..6ec9bc7c 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.6.5 +// @version 3.6.6 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From 2544311ca83a25484727b86a472b7e5db77106dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 00:55:01 +0000 Subject: [PATCH 11/15] Update time and description of 3.6.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index f6875e83..f85af226 100644 --- a/Update.json +++ b/Update.json @@ -3740,7 +3740,7 @@ "Notes": "修复比赛结束后自动向原题提交的回退功能。此前回退提交的响应被丢弃,遇到提交冷却时会静默失败并显示\"提交失败\",状态页里也没有任何提交记录。" }, "3.6.6": { - "UpdateDate": 1789779179257, + "UpdateDate": 1789779296094, "Prerelease": true, "UpdateContents": [ { From 62e9c7f142fc2088b1136e0ab48b81c800dffc0d Mon Sep 17 00:00:00 2001 From: boomzero Date: Sat, 19 Sep 2026 08:55:17 +0800 Subject: [PATCH 12/15] =?UTF-8?q?fix:=20=E6=8D=A2=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81=E6=9C=9F=E9=97=B4=E9=94=81=E5=AE=9A=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E6=A1=86=EF=BC=8C=E9=81=BF=E5=85=8D=E6=8C=89=E6=97=A7=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=A1=AB=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- XMOJ.user.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 6ec9bc7c..2f74ae62 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -4400,26 +4400,36 @@ async function main() { // Only a submitted answer counts against the session, so fetching another image is // free: when the solver declines one it is cheaper to ask for a fresh challenge than // to make the user type it. Each fetch replaces the answer the session expects, so - // this stops as soon as the user starts typing against the image on screen. + // this stops if the user starts typing against the image on screen before the next one. for (let Attempt = 1; Attempt <= CaptchaMaxAttempts; Attempt++) { if (Attempt > 1) { await new Promise((Resolve) => setTimeout(Resolve, 300)); if (RequestID !== CaptchaRequestID || CaptchaInput.value !== "") return; SetCaptchaStatus("这张看不太准,正在自动换一张(" + Attempt + "/" + CaptchaMaxAttempts + ")"); } + // From the moment vcode.php is requested the image on screen no longer matches the + // session, so anything typed from it would be a guaranteed wrong answer. The box is + // locked until the new image replaces it. A newer call that takes over leaves the + // lock to be released by that call rather than by this one. + CaptchaInput.readOnly = true; let ImageBlob; try { const CaptchaResponse = await fetch("https://www.xmoj.tech/vcode.php?" + Math.random(), {cache: "no-store"}); ImageBlob = await CaptchaResponse.blob(); } catch (e) { console.error(e); - if (RequestID === CaptchaRequestID) SetCaptchaStatus("验证码加载失败,请点击图片重试"); + if (RequestID === CaptchaRequestID) { + CaptchaInput.readOnly = false; + SetCaptchaStatus("验证码加载失败,请点击图片重试"); + } return; } if (RequestID !== CaptchaRequestID) return; if (CaptchaObjectURL !== null) URL.revokeObjectURL(CaptchaObjectURL); CaptchaObjectURL = URL.createObjectURL(ImageBlob); document.querySelector("#CaptchaImage").src = CaptchaObjectURL; + CaptchaInput.value = ""; + CaptchaInput.readOnly = false; if (!UtilityEnabled("AutoCaptcha")) return; let CaptchaLength = 4; try { From abff30484f56188c5c0c767c518a99dcb4d53984 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 00:55:54 +0000 Subject: [PATCH 13/15] Update time and description of 3.6.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index f85af226..37752240 100644 --- a/Update.json +++ b/Update.json @@ -3740,7 +3740,7 @@ "Notes": "修复比赛结束后自动向原题提交的回退功能。此前回退提交的响应被丢弃,遇到提交冷却时会静默失败并显示\"提交失败\",状态页里也没有任何提交记录。" }, "3.6.6": { - "UpdateDate": 1789779296094, + "UpdateDate": 1789779349314, "Prerelease": true, "UpdateContents": [ { From c6856529ae730c4ba117e082c4b41914d40cac88 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 01:07:44 +0000 Subject: [PATCH 14/15] 3.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e6af0b78..454e50c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.6.6", + "version": "3.7.0", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From 6085c4e6b61e00ab8292193e91308e431bd2cd84 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Sep 2026 01:07:46 +0000 Subject: [PATCH 15/15] Update to release 3.7.0 --- Update.json | 31 +++++++++++++++++++++++++++++++ XMOJ.user.js | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 37752240..3bc3bf70 100644 --- a/Update.json +++ b/Update.json @@ -3749,6 +3749,37 @@ } ], "Notes": "自动识别验证码时,若当前这张看不准,脚本会自动换一张重试(最多 5 张),无需手动点击图片。" + }, + "3.7.0": { + "UpdateDate": 1789780064668, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 1006, + "Description": "Fix #1004" + }, + { + "PR": 1013, + "Description": "fix: handle PID 0 submission rows" + }, + { + "PR": 1015, + "Description": "feat: compress ApplyData payloads with gzip and Base93" + }, + { + "PR": 1019, + "Description": "fix: 在提交界面显示并提交验证码" + }, + { + "PR": 1021, + "Description": "fix: 比赛结束后回退提交不再被静默丢弃" + }, + { + "PR": 1023, + "Description": "feat: 验证码识别不出时自动换一张" + } + ], + "Notes": "

3.7.0 — 自上一正式版 3.6.0 以来的完整更新

\n

本次正式版整合 3.6.1、3.6.2、3.6.3、3.6.4、3.6.5 和 3.6.6 的全部功能更新与修复。

\n

3.6.1:代码对比显示修复(#1006,修复 #1004)

\n
  • 修复 Monaco 代码对比区域的高度布局,为合并编辑器容器设置高度,恢复对比内容的正常显示。
\n

3.6.2:提交记录布局修复(#1013)

\n
  • 修复题目 ID 为 0、缺少重新提交链接时提交记录的排版错误,避免相关处理打断页面。
  • 安全构造提交记录中的链接,并将缺失链接的判断限定在需要重新提交链接的处理逻辑中。
\n

3.6.3:获取数据功能升级(#1015)

\n
    \n
  • 页面没有数据申请区域时自动创建获取数据控件。
  • \n
  • 默认模式采用 gzip + Base93 压缩传输,逐字节保留任意输入,支持文本、Unicode 和混合内容;无效 UTF-8 以可逆的逐字节转义形式显示。
  • \n
  • 新增可选 NSC3 高速数值模式,压缩 long long 整数及每行数值个数;规范数值输入可恢复空格、空行、换行与末尾换行,非规范或混合内容原样回退。
  • \n
  • 新增“保留换行”选项,默认开启。关闭后,规范数值输入恢复为单行空格分隔数据,降低随机行长带来的输出开销;仅适用于 cin 或 scanf 等不依赖行边界的读取方式,不适用于 getline 或按行解析。
  • \n
  • 界面增加各模式的说明与适用范围,帮助选择通用模式或高速数值模式。
  • \n
  • 生成的数据获取程序先向 stderr 输出并刷新,再通过 abort() 触发运行错误以返回数据;同时兼容读取旧版异常包装结果。
  • \n
\n

3.6.4:提交验证码支持(#1019)

\n
    \n
  • 修复评测队列繁忙、服务端启用验证码时,脚本提交界面丢失验证码字段而导致提交失败的问题。
  • \n
  • 显示验证码图片与输入框,支持点击图片更换、按回车提交,并在提交请求中携带验证码。
  • \n
  • 新增 AutoCaptcha 设置,支持在浏览器本地通过模板匹配识别四位数字验证码,不调用外部 AI 服务。识别不确定时交由用户输入;非四位数字类型提示手动填写。
  • \n
  • 普通提交与强制提交均检查验证码是否为空,并在实际发送请求前再次检查,避免用户中途清空或刷新图片后仍发送空验证码。
  • \n
  • 若服务端在页面加载后才要求验证码,或返回验证码错误,显示并刷新验证码、恢复提交按钮并提示重新填写。
  • \n
\n

3.6.5:比赛结束后回退提交修复(#1021)

\n
    \n
  • 修复比赛结束后向原题回退提交时响应被忽略、提交静默丢失的问题。
  • \n
  • 从比赛题目列表中解析完整原题编号,避免固定截取四位数字造成题号错误。
  • \n
  • 正确处理成功跳转、提交冷却与服务端错误;遇到重复提交限制时按三秒间隔重试,最多尝试五次,并显示进度及失败原因。
  • \n
  • 每次回退请求前重新检查验证码并携带当前输入;验证码失效时刷新图片并要求重新填写。
  • \n
  • 处理比赛页面读取、题目列表解析及提交网络错误,恢复按钮状态,避免一直停留在“正在提交”。
  • \n
\n

3.6.6:验证码自动更换与重试(#1023)

\n
    \n
  • 启用自动识别后,若当前验证码识别不确定,自动更换图片重试,最多尝试五张;仍不确定时提示手动输入或点击换图。
  • \n
  • 换图请求期间暂时锁定输入框,避免用户根据旧图片填写;图片加载完成或请求失败后恢复输入。
  • \n
  • 防止过期异步请求覆盖较新的验证码状态,并避免自动识别覆盖用户已经输入的内容。
  • \n
  • 验证码图片加载失败时给出明确提示,支持点击图片重试。
  • \n
" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index 2f74ae62..494f9a4e 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.6.6 +// @version 3.7.0 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen