From 586d9169927557fe62e371d3f250de602bb3b779 Mon Sep 17 00:00:00 2001 From: boomzero Date: Sat, 19 Sep 2026 08:23:12 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=E6=AF=94=E8=B5=9B=E7=BB=93=E6=9D=9F?= =?UTF-8?q?=E5=90=8E=E5=9B=9E=E9=80=80=E6=8F=90=E4=BA=A4=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=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 2/7] 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 3/7] 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 4/7] =?UTF-8?q?fix:=20=E5=9B=9E=E9=80=80=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=AF=8F=E6=AC=A1=E9=87=8D=E8=AF=95=E5=89=8D=E9=87=8D=E6=96=B0?= =?UTF-8?q?=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 5/7] 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 6/7] =?UTF-8?q?fix:=20=E5=9B=9E=E9=80=80=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E7=9A=84=E7=BD=91=E7=BB=9C=E8=AF=B7=E6=B1=82=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E8=AE=A9=E6=8F=90=E4=BA=A4=E6=8C=89=E9=92=AE?= =?UTF-8?q?=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 7/7] 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": [ {