Conversation
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
sync: dev to extern-contrib
vcode.php 用 imagettftext(15px, Vera.ttf) 在固定位置绘制 4 位数字, 每个字形在不同验证码之间逐像素相同,因此模板匹配可以直接读出来, 不需要把图片发给任何模型。 背景是一种随机色,文字是它的反色,噪点是第三种随机色:取出现次数 最多的颜色作为背景即可定位文字颜色。跳过 1 像素黑边,否则接近白色 的背景会让黑边被当成文字。切分出的字形与 10 个模板逐一比对,除了 奖励命中的笔画,也惩罚模板之外的墨点——否则带噪点的 0 会和 9 打平。 只有切出恰好 4 个字形、且每个字形的最佳得分领先第二名 4 分以上时 才填入,否则留空由用户填写:错误答案会触发 vfail,把验证码升级成 8 位字母数字,比留空更糟。 实测 300 张生成样本 227 正确 0 错误,40 张真实验证码填入 31 张, 14 张人工标注样本 13 正确 1 放弃 0 错误。因为不会填错,AutoCaptcha 恢复默认开启,同时移除 captchaSolve 依赖与相关 @connect。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
验证码为空的拦截原来只在"提交"的处理函数里。检查未通过时页面会留下 "强制提交"按钮,而真正发出请求的是它:这段时间里用户可以点击验证码 图片换一张(会清空输入框)或手动清空,再点"强制提交"就会带着空验证码 发出请求,触发本要避免的 vfail 升级。 把拦截提取为 CaptchaIsMissing,在实际发出请求的 PassCheck 处理函数 开头也调用一次,并在拦截时重新隐藏"强制提交"按钮。 Reported-by: chatgpt-codex-connector Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
回退路径本身可以解析出真实题号,问题在 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 <noreply@anthropic.com>
冷却等待期间验证码输入框和刷新按钮仍可交互。如果用户在这 3 秒里清空了 输入框或刷新了图片,下一次重试会用空的 GetCaptchaParameter() 发出请求, 而空答案会让服务端把本 session 的 4 位验证码换成 8 位。 在循环每次 POST 之前调用 CaptchaIsMissing()(它自己会提示并恢复按钮), 并用 Handled 标志让外层直接返回,不覆盖它设置的提示。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
contest.php 或 submit.php 的 fetch 被 reject(网络错误)时,异常会一路穿出 SubmitToEndedContestProblem、穿出 .then 回调、穿出 PassCheck 的 async 监听器 ——整条链上没有任何 catch。结果 ShowSubmitStatus 和恢复按钮的两行都不会执行, 提交按钮永远停在"正在提交...",而错误框在监听器开头已经被设成 display: none, 用户什么提示都看不到。这正是本 PR 声称要修掉的那个症状。 把两处网络请求都包进 try/catch,失败时返回错误信息交给外层显示。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ack-1017 fix: 比赛结束后回退提交不再被静默丢弃
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat: 验证码识别不出时自动换一张
Reviewer's GuideThis integration release merges the accumulated dev fixes through 3.6.6 into master, primarily improving captcha-aware submissions, ended-contest recovery, compressed test-data retrieval with an NSC numeric mode, and several UI edge cases; metadata is updated while functional browser verification remains outstanding. Sequence diagram for captcha-aware code submissionsequenceDiagram
participant User
participant SubmitPage
participant Captcha as vcode.php
participant Submit as submit.php
User->>SubmitPage: Open submitpage.php
SubmitPage->>SubmitPage: NativeCaptchaShown
opt Captcha required
SubmitPage->>Captcha: fetch
Captcha-->>SubmitPage: ImageBlob
SubmitPage->>SubmitPage: SolveCaptcha
SubmitPage-->>User: Display image and optional answer
end
User->>SubmitPage: Click Submit
SubmitPage->>SubmitPage: CaptchaIsMissing
SubmitPage->>Submit: POST source and GetCaptchaParameter
alt Captcha error
Submit-->>SubmitPage: 验证码错误
SubmitPage->>Captcha: RefreshCaptcha
SubmitPage-->>User: Request corrected captcha
else Accepted or other response
Submit-->>SubmitPage: Submission response
end
Sequence diagram for ended-contest submission recoverysequenceDiagram
participant User
participant SubmitPage
participant Contest as contest.php
participant Submit as submit.php
User->>SubmitPage: Submit contest solution
SubmitPage->>Submit: POST cid and pid
Submit-->>SubmitPage: 没有这个比赛
SubmitPage->>SubmitPage: SubmitToEndedContestProblem
SubmitPage->>Contest: fetch contest page
Contest-->>SubmitPage: Contest problem table
SubmitPage->>SubmitPage: Resolve original PID
loop Cooldown retries up to 5 attempts
SubmitPage->>Submit: POST original problem and source
alt Accepted or redirected
Submit-->>SubmitPage: Submission created
else 请勿重复提交
SubmitPage->>SubmitPage: Wait 3 seconds
else Network or server failure
Submit-->>SubmitPage: Error message
end
end
SubmitPage-->>User: Show submission status
Flow diagram for test-data decoding modesflowchart TD
Start[Get data] --> Extract[ExtractData]
Extract --> Base93[Base93Decode]
Base93 --> Format{Payload format}
Format -->|gzip| Gzip[GzipDecode]
Format -->|NSC1 or NSC3| Numeric[NumberStreamDecode]
Gzip --> Display[DecodeBytesForDisplay]
Numeric --> Display
Display --> Results[Show decoded data]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="XMOJ.user.js" line_range="4630" />
<code_context>
"referrer": location.href,
"method": "POST",
- "body": (SearchParams.get("id") != null ? "id=" + SearchParams.get("id") : "cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch
+ "body": (SearchParams.get("id") != null ? "id=" + SearchParams.get("id") : "cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + GetCaptchaParameter()
}).then(async (Response) => {
if (Response.redirected) {
</code_context>
<issue_to_address>
**issue (bug_risk):** The `PassCheck` handler awaits the direct `submit.php` fetch without a `try/catch`. When that request or `Response.text()` rejects because of a network failure, the async listener exits before restoring `Submit.disabled`, its label, or the error message, leaving the submission UI stuck on “正在提交...”.
**Triggers:** When the direct submission request fails at the network or response-reading stage.
**Suggested fix:** Wrap the direct submission fetch and response parsing in `try/catch`, then show the failure and restore the submit controls in the error path.
</issue_to_address>
### Comment 2
<location path="XMOJ.user.js" line_range="6036" />
<code_context>
+vector<T> tk(const string&s){int N=s.size(),Z=1<<16;vector<int>h(Z,-1),p(1<<15,-1);vector<T>v;v.reserve(N/5+1);auto H=[&](int i){return ((uint32_t)(uint8_t)s[i]*63001u+(uint32_t)(uint8_t)s[i+1]*251u+(uint8_t)s[i+2])&(Z-1);};auto I=[&](int i){if(i+2<N){int z=H(i);p[i&32767]=h[z];h[z]=i;}};auto M=[&](int i){int l=0,d=0;if(i+2>=N)return make_pair(0,0);int c=h[H(i)],m=min(258,N-i),q=0;const char*a=s.data()+i;while(c>=0&&i-c<=32768){int lim=l>=8?64:128;if(q++>=lim)break;const char*b=s.data()+c;if(b[0]!=a[0]||b[1]!=a[1]||b[2]!=a[2]){c=p[c&32767];continue;}if(l>=3&&(b[l]!=a[l]||memcmp(a,b,l))){c=p[c&32767];continue;}int z=l>=3?l+1:3;while(z+8<=m){uint64_t x,y;memcpy(&x,a+z,8);memcpy(&y,b+z,8);if(x!=y)break;z+=8;}while(z<m&&a[z]==b[z])z++;if(z>l)l=z,d=i-c;if(z>=128||z==m)break;c=p[c&32767];}return make_pair(l,d);};for(int i=0;i<N;){pair<int,int>u=M(i);int l=u.first,d=u.second;I(i);if(l>=3&&l<16&&i+1<N){pair<int,int>q=M(i+1);if(q.first>l){v.push_back(T((uint8_t)s[i++],0));continue;}}if(l>=3){v.push_back(T(l,d));for(int j=1;j<l;j++)I(i+j);i+=l;}else v.push_back(T((uint8_t)s[i++],0));}return v;}
</code_context>
<issue_to_address>
**issue (bug_risk):** The generated gzip compressor's match search reads `b[l]` and `memcmp(a, b, l)` without verifying that the candidate match at `c` has at least `l` remaining bytes. A shorter candidate near the end of the input is therefore compared past its valid range, and the subsequent match extension can use invalid bytes, producing corrupted gzip payloads or undefined behavior in the generated program.
**Triggers:** When gzip mode processes repetitive input with a previously considered match that is shorter than the current best match, especially near the end of the input.
**Suggested fix:** Track each candidate's remaining length and reject candidates shorter than `l` before accessing `b[l]` or comparing `l` bytes.
```suggestion
vector<T> tk(const string&s){int N=s.size(),Z=1<<16;vector<int>h(Z,-1),p(1<<15,-1);vector<T>v;v.reserve(N/5+1);auto H=[&](int i){return ((uint32_t)(uint8_t)s[i]*63001u+(uint32_t)(uint8_t)s[i+1]*251u+(uint8_t)s[i+2])&(Z-1);};auto I=[&](int i){if(i+2<N){int z=H(i);p[i&32767]=h[z];h[z]=i;}};auto M=[&](int i){int l=0,d=0;if(i+2>=N)return make_pair(0,0);int c=h[H(i)],m=min(258,N-i),q=0;const char*a=s.data()+i;while(c>=0&&i-c<=32768){int lim=l>=8?64:128;if(q++>=lim)break;const char*b=s.data()+c;int r=min(m,N-c);if(b[0]!=a[0]||b[1]!=a[1]||b[2]!=a[2]){c=p[c&32767];continue;}if(l>=3&&(r<=l||b[l]!=a[l]||memcmp(a,b,l))){c=p[c&32767];continue;}int z=l>=3?l+1:3;while(z+8<=r){uint64_t x,y;memcpy(&x,a+z,8);memcpy(&y,b+z,8);if(x!=y)break;z+=8;}while(z<r&&a[z]==b[z])z++;if(z>l)l=z,d=i-c;if(z>=128||z==m)break;c=p[c&32767];}return make_pair(l,d);};for(int i=0;i<N;){pair<int,int>u=M(i);int l=u.first,d=u.second;I(i);if(l>=3&&l<16&&i+1<N){pair<int,int>q=M(i+1);if(q.first>l){v.push_back(T((uint8_t)s[i++],0));continue;}}if(l>=3){v.push_back(T(l,d));for(int j=1;j<l;j++)I(i+j);i+=l;}else v.push_back(T((uint8_t)s[i++],0));}return v;}
```
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and the change automates captcha handling and substantially rewrites submission fallback and data decoding, so a defect could submit incorrect or duplicate code, consume captcha attempts, or display reconstructed input incorrectly. Reverting stops future behavior but cannot retract submissions already created or captcha attempts already consumed.
Blocking findings: XMOJ.user.js:4630, XMOJ.user.js:6036
| "referrer": location.href, | ||
| "method": "POST", | ||
| "body": (SearchParams.get("id") != null ? "id=" + SearchParams.get("id") : "cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch | ||
| "body": (SearchParams.get("id") != null ? "id=" + SearchParams.get("id") : "cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + GetCaptchaParameter() |
There was a problem hiding this comment.
issue (bug_risk): The PassCheck handler awaits the direct submit.php fetch without a try/catch. When that request or Response.text() rejects because of a network failure, the async listener exits before restoring Submit.disabled, its label, or the error message, leaving the submission UI stuck on “正在提交...”.
Triggers: When the direct submission request fails at the network or response-reading stage.
Suggested fix: Wrap the direct submission fetch and response parsing in try/catch, then show the failure and restore the submit controls in the error path.
| struct W{string o;uint64_t b;int n;W():b(0),n(0){}void p(uint32_t v,int k){b|=(uint64_t)v<<n;n+=k;while(n>=8)o+=char(b),b>>=8,n-=8;}void a(){if(n)o+=char(b),b=0,n=0;}string f(){a();return o;}};uint32_t R(uint32_t x,int n){uint32_t y=0;while(n--)y=y*2+(x&1),x>>=1;return y;}struct T{uint16_t l,d;T(int L=0,int D=0):l(L),d(D){}}; | ||
| int Lb[]={3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258},Le[]={0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0},Db[]={1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577},De[]={0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; | ||
| int li(int x){int i=0;while(i<28&&x>=Lb[i+1])i++;return i;}int di(int x){int i=0;while(i<29&&x>=Db[i+1])i++;return i;} | ||
| vector<T> tk(const string&s){int N=s.size(),Z=1<<16;vector<int>h(Z,-1),p(1<<15,-1);vector<T>v;v.reserve(N/5+1);auto H=[&](int i){return ((uint32_t)(uint8_t)s[i]*63001u+(uint32_t)(uint8_t)s[i+1]*251u+(uint8_t)s[i+2])&(Z-1);};auto I=[&](int i){if(i+2<N){int z=H(i);p[i&32767]=h[z];h[z]=i;}};auto M=[&](int i){int l=0,d=0;if(i+2>=N)return make_pair(0,0);int c=h[H(i)],m=min(258,N-i),q=0;const char*a=s.data()+i;while(c>=0&&i-c<=32768){int lim=l>=8?64:128;if(q++>=lim)break;const char*b=s.data()+c;if(b[0]!=a[0]||b[1]!=a[1]||b[2]!=a[2]){c=p[c&32767];continue;}if(l>=3&&(b[l]!=a[l]||memcmp(a,b,l))){c=p[c&32767];continue;}int z=l>=3?l+1:3;while(z+8<=m){uint64_t x,y;memcpy(&x,a+z,8);memcpy(&y,b+z,8);if(x!=y)break;z+=8;}while(z<m&&a[z]==b[z])z++;if(z>l)l=z,d=i-c;if(z>=128||z==m)break;c=p[c&32767];}return make_pair(l,d);};for(int i=0;i<N;){pair<int,int>u=M(i);int l=u.first,d=u.second;I(i);if(l>=3&&l<16&&i+1<N){pair<int,int>q=M(i+1);if(q.first>l){v.push_back(T((uint8_t)s[i++],0));continue;}}if(l>=3){v.push_back(T(l,d));for(int j=1;j<l;j++)I(i+j);i+=l;}else v.push_back(T((uint8_t)s[i++],0));}return v;} |
There was a problem hiding this comment.
issue (bug_risk): The generated gzip compressor's match search reads b[l] and memcmp(a, b, l) without verifying that the candidate match at c has at least l remaining bytes. A shorter candidate near the end of the input is therefore compared past its valid range, and the subsequent match extension can use invalid bytes, producing corrupted gzip payloads or undefined behavior in the generated program.
Triggers: When gzip mode processes repetitive input with a previously considered match that is shorter than the current best match, especially near the end of the input.
Suggested fix: Track each candidate's remaining length and reject candidates shorter than l before accessing b[l] or comparing l bytes.
| vector<T> tk(const string&s){int N=s.size(),Z=1<<16;vector<int>h(Z,-1),p(1<<15,-1);vector<T>v;v.reserve(N/5+1);auto H=[&](int i){return ((uint32_t)(uint8_t)s[i]*63001u+(uint32_t)(uint8_t)s[i+1]*251u+(uint8_t)s[i+2])&(Z-1);};auto I=[&](int i){if(i+2<N){int z=H(i);p[i&32767]=h[z];h[z]=i;}};auto M=[&](int i){int l=0,d=0;if(i+2>=N)return make_pair(0,0);int c=h[H(i)],m=min(258,N-i),q=0;const char*a=s.data()+i;while(c>=0&&i-c<=32768){int lim=l>=8?64:128;if(q++>=lim)break;const char*b=s.data()+c;if(b[0]!=a[0]||b[1]!=a[1]||b[2]!=a[2]){c=p[c&32767];continue;}if(l>=3&&(b[l]!=a[l]||memcmp(a,b,l))){c=p[c&32767];continue;}int z=l>=3?l+1:3;while(z+8<=m){uint64_t x,y;memcpy(&x,a+z,8);memcpy(&y,b+z,8);if(x!=y)break;z+=8;}while(z<m&&a[z]==b[z])z++;if(z>l)l=z,d=i-c;if(z>=128||z==m)break;c=p[c&32767];}return make_pair(l,d);};for(int i=0;i<N;){pair<int,int>u=M(i);int l=u.first,d=u.second;I(i);if(l>=3&&l<16&&i+1<N){pair<int,int>q=M(i+1);if(q.first>l){v.push_back(T((uint8_t)s[i++],0));continue;}}if(l>=3){v.push_back(T(l,d));for(int j=1;j<l;j++)I(i+j);i+=l;}else v.push_back(T((uint8_t)s[i++],0));}return v;} | |
| vector<T> tk(const string&s){int N=s.size(),Z=1<<16;vector<int>h(Z,-1),p(1<<15,-1);vector<T>v;v.reserve(N/5+1);auto H=[&](int i){return ((uint32_t)(uint8_t)s[i]*63001u+(uint32_t)(uint8_t)s[i+1]*251u+(uint8_t)s[i+2])&(Z-1);};auto I=[&](int i){if(i+2<N){int z=H(i);p[i&32767]=h[z];h[z]=i;}};auto M=[&](int i){int l=0,d=0;if(i+2>=N)return make_pair(0,0);int c=h[H(i)],m=min(258,N-i),q=0;const char*a=s.data()+i;while(c>=0&&i-c<=32768){int lim=l>=8?64:128;if(q++>=lim)break;const char*b=s.data()+c;int r=min(m,N-c);if(b[0]!=a[0]||b[1]!=a[1]||b[2]!=a[2]){c=p[c&32767];continue;}if(l>=3&&(r<=l||b[l]!=a[l]||memcmp(a,b,l))){c=p[c&32767];continue;}int z=l>=3?l+1:3;while(z+8<=r){uint64_t x,y;memcpy(&x,a+z,8);memcpy(&y,b+z,8);if(x!=y)break;z+=8;}while(z<r&&a[z]==b[z])z++;if(z>l)l=z,d=i-c;if(z>=128||z==m)break;c=p[c&32767];}return make_pair(l,d);};for(int i=0;i<N;){pair<int,int>u=M(i);int l=u.first,d=u.second;I(i);if(l>=3&&l<16&&i+1<N){pair<int,int>q=M(i+1);if(q.first>l){v.push_back(T((uint8_t)s[i++],0));continue;}}if(l>=3){v.push_back(T(l,d));for(int j=1;j<l;j++)I(i+j);i+=l;}else v.push_back(T((uint8_t)s[i++],0));}return v;} |
Update to release 3.7.0
Deploying xmoj-script-dev-channel with
|
| Latest commit: |
fce1af6
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ce2b43f8.xmoj-script-dev-channel.pages.dev |
What does this PR aim to accomplish?:
Merge
devintomasterto publish all changes since stable 3.6.0, covering 3.6.1–3.6.6, as stable 3.7.0. The repository's UpdateToRelease workflow prepares the next stable version (expected 3.7.0).How does this PR accomplish the above?:
Use a merge commit to preserve development history. Includes the fixes and improvements from #1006, #1013, #1015, #1019, #1021, and #1023. No additional application changes are introduced by this integration PR.
Verification before opening:
git merge-tree --write-tree origin/master origin/devsucceeds without conflicts.node --checkpasses for the dev userscript.Update.jsonparses successfully; package and userscript versions are both 3.6.6.5f348ffaab00f0d97dd7d9b26cc827b2bc4b8576passed.By submitting this pull request, I confirm the following:
git rebase) Not applicable: preserve dev history for this branch integration.Summary by Sourcery
Merge the accumulated development fixes and feature improvements into the stable 3.7.0 release.
New Features:
Bug Fixes:
Enhancements:
Chores:
Summary by cubic
Publishes the accumulated 3.6.1–3.6.6 development updates as stable 3.7.0 by merging
devintomaster; this merge carries only the existingdevhistory.What's included
获取数据Base64 payload with gzip + Base93 compression, and adds an NSC3 numeric mode with an optional line-break toggle.获取数据controls even when the server doesn't render the apply-data area.AutoCaptchasetting that fills 4-digit captchas locally and retries unclear ones.Caveats
Written for commit fce1af6. Summary will update on new commits.