vjudge: handle problem resync & fix uoj

This commit is contained in:
undefined committed 2025-10-26 20:27:46 +08:00
1 parent f6a9dd3557
commit e8573d7061
10 files changed
+34 -32

No files matched your search

+1 -1
View File
@@ -325,7 +325,7 @@ class LocalStorageService {
async get(target: string, path?: string) { async get(target: string, path?: string) {
target = resolve(this.dir, convertPath(target)); target = resolve(this.dir, convertPath(target));
if (!existsSync(target)) throw new Error('File not found'); if (!existsSync(target)) throw new Error(`File not found: ${target}`);
if (path) await copyFile(target, path); if (path) await copyFile(target, path);
return createReadStream(target); return createReadStream(target);
} }
+14 -13
View File
@@ -80,12 +80,13 @@ class AccountService {
} }
} }
async sync(target: string, resync = false, list: string) { async sync(target: string, fromPage = 0, list: string) {
let page = 1; let page = Math.max(fromPage, 1);
let pids = await this.api.listProblem(page, resync, list); let pids: string[] = [];
const [domainId, namespaceId] = target.split('.'); const [domainId, namespaceId] = target.split('.');
while (pids.length) { do {
logger.info(`${domainId}: Syncing page ${page}`); pids = await this.api.listProblem(page, fromPage, list);
logger.info(`${target}: Syncing page ${page}`);
for (const id of pids) { for (const id of pids) {
if (id.startsWith('LIST::')) { if (id.startsWith('LIST::')) {
await this.addProblemList(id.split('::')[1]); await this.addProblemList(id.split('::')[1]);
@@ -110,16 +111,16 @@ class AccountService {
await ProblemModel.addTestdata(domainId, docId, key, res.data[key]); await ProblemModel.addTestdata(domainId, docId, key, res.data[key]);
} }
if (res.solution) await SolutionModel.add(domainId, docId, 1, res.solution); if (res.solution) await SolutionModel.add(domainId, docId, 1, res.solution);
logger.info(`${domainId}: problem ${docId}(${pid}) sync done -> ${targetPid}(${docId})`); logger.info(`${domainId}: problem ${pid} sync done -> ${targetPid}(${docId})`);
} finally { } finally {
delete syncing[`${domainId}/${pid}`]; delete syncing[`${domainId}/${pid}`];
} }
await sleep(5000); await sleep(5000);
} }
page++; page++;
if (this.stopped) return; if (this.stopped) return page - 1;
pids = await this.api.listProblem(page, resync, list); } while (pids.length);
} return page - 2;
} }
async login() { async login() {
@@ -141,11 +142,11 @@ class AccountService {
this.listUpdated = false; this.listUpdated = false;
for (const listName of this.problemLists) { for (const listName of this.problemLists) {
for (const mount of mounts) { for (const mount of mounts) {
if (!mount.syncDone?.[listName]) await this.sync(mount._id, false, listName); const from = typeof mount.syncDone?.[listName] === 'number' ? mount.syncDone?.[listName] : 0;
else await this.sync(mount._id, true, listName); const page = await this.sync(mount._id, from, listName);
await collMount.updateOne({ _id: mount._id }, { $set: { [`syncDone.${listName}`]: true } }); await collMount.updateOne({ _id: mount._id }, { $set: { [`syncDone.${listName}`]: page } });
mount.syncDone ||= {}; mount.syncDone ||= {};
mount.syncDone[listName] = true; mount.syncDone[listName] = page;
} }
} }
} while (this.listUpdated); } while (this.listUpdated);
+2 -2
View File
@@ -17,7 +17,7 @@ export interface RemoteAccount {
export interface VjudgeMount { export interface VjudgeMount {
_id: string; // domainId, or `${domainId}.${namespace}` _id: string; // domainId, or `${domainId}.${namespace}`
mount: string; mount: string;
syncDone: Record<string, boolean>; syncDone: Record<string, number>;
} }
declare module 'hydrooj' { declare module 'hydrooj' {
interface Collections { interface Collections {
@@ -38,7 +38,7 @@ export interface IBasicProvider {
solution?: string; solution?: string;
}>; }>;
entryProblemLists?: string[]; entryProblemLists?: string[];
listProblem: (page: number, resync: boolean, listId: string) => Promise<string[]>; listProblem: (page: number, resyncFrom: number, listId: string) => Promise<string[]>;
submitProblem: (id: string, lang: string, code: string, info: any, next: NextFunction, end: NextFunction) => Promise<string | void>; submitProblem: (id: string, lang: string, code: string, info: any, next: NextFunction, end: NextFunction) => Promise<string | void>;
waitForSubmission: (id: string, next: NextFunction, end: NextFunction) => Promise<void>; waitForSubmission: (id: string, next: NextFunction, end: NextFunction) => Promise<void>;
checkStatus?: (onCheckFunc: boolean) => Promise<void>; checkStatus?: (onCheckFunc: boolean) => Promise<void>;
+5 -3
View File
@@ -230,10 +230,12 @@ export default class CodeforcesProvider extends BasicFetcher implements IBasicPr
// TL;DR; add `gym` to this list to enable codeforces gym // TL;DR; add `gym` to this list to enable codeforces gym
entryProblemLists = ['main']; entryProblemLists = ['main'];
async listProblem(page: number, resync = false, listName: string) { async listProblem(page: number, resyncFrom: number, listName: string) {
if (resync && page > 1) return [];
if (resync && listName.startsWith('GYM')) return [];
if (listName.startsWith('GYM') && page > 1) return []; if (listName.startsWith('GYM') && page > 1) return [];
if (resyncFrom) {
if (page > resyncFrom || listName.startsWith('GYM')) return [];
page = 1;
}
const { document } = await this.html(listName === 'main' const { document } = await this.html(listName === 'main'
? `/problemset/page/${page}` ? `/problemset/page/${page}`
: listName === 'gym' : listName === 'gym'
+1 -2
View File
@@ -123,8 +123,7 @@ export default class CSGOJProvider extends BasicFetcher implements IBasicProvide
}; };
} }
async listProblem(page: number, resync = false) { async listProblem(page: number) {
if (resync && page > 1) return [];
const offset = (page - 1) * 100; const offset = (page - 1) * 100;
const result = await this const result = await this
.get(`/csgoj/problemset/problemset_ajax?search=&sort=problem_id&order=asc&offset=${offset}&limit=100`) .get(`/csgoj/problemset/problemset_ajax?search=&sort=problem_id&order=asc&offset=${offset}&limit=100`)
+1 -2
View File
@@ -130,8 +130,7 @@ export default class HDUOJProvider extends BasicFetcher implements IBasicProvide
}; };
} }
async listProblem(page: number, resync = false) { async listProblem(page: number) {
if (resync && page > 1) return [];
const { text } = await this.get(`/listproblem.php?vol=${page}`); const { text } = await this.get(`/listproblem.php?vol=${page}`);
const $dom = new JSDOM(text); const $dom = new JSDOM(text);
const ProblemTable = $dom.window.document.querySelector('.table_text'); const ProblemTable = $dom.window.document.querySelector('.table_text');
+1 -2
View File
@@ -161,8 +161,7 @@ export default class POJProvider extends BasicFetcher implements IBasicProvider
}; };
} }
async listProblem(page: number, resync = false) { async listProblem(page: number) {
if (resync && page > 1) return [];
const { text } = await this.get(`/problemlist?volume=${page}`); const { text } = await this.get(`/problemlist?volume=${page}`);
const $dom = new JSDOM(text); const $dom = new JSDOM(text);
return Array.from($dom.window.document.querySelectorAll('.a>tbody>tr[align="center"]')) return Array.from($dom.window.document.querySelectorAll('.a>tbody>tr[align="center"]'))
+1 -2
View File
@@ -81,8 +81,7 @@ langs: ${JSON.stringify(langs)}`),
}; };
} }
async listProblem(page: number, resync = false) { async listProblem(page: number) {
if (resync) return [];
const { document } = await this.html(`/problems/classical/sort=0,start=${page * 50 - 50}`); const { document } = await this.html(`/problems/classical/sort=0,start=${page * 50 - 50}`);
const index = document.querySelector('ul.pagination').querySelector('li.active').children[0].innerHTML.trim(); const index = document.querySelector('ul.pagination').querySelector('li.active').children[0].innerHTML.trim();
if (index !== page.toString()) return []; if (index !== page.toString()) return [];
+7 -3
View File
@@ -24,7 +24,12 @@ const MAPPING = {
export default class UOJProvider extends BasicFetcher implements IBasicProvider { export default class UOJProvider extends BasicFetcher implements IBasicProvider {
constructor(public account: RemoteAccount, private save: (data: any) => Promise<void>) { constructor(public account: RemoteAccount, private save: (data: any) => Promise<void>) {
super(account, 'https://uoj.ac', 'form', logger); // UOJ WAF start blocking vjudge User-Agent
super(account, 'https://uoj.ac', 'form', logger, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
},
});
} }
csrf: string; csrf: string;
@@ -131,8 +136,7 @@ export default class UOJProvider extends BasicFetcher implements IBasicProvider
}; };
} }
async listProblem(page: number, resync = false) { async listProblem(page: number) {
if (resync && page > 1) return [];
const { text } = await this.get(`/problems?page=${page}`); const { text } = await this.get(`/problems?page=${page}`);
const $dom = new JSDOM(text); const $dom = new JSDOM(text);
const index = $dom.window.document.querySelector('ul.pagination>li.active>a').innerHTML; const index = $dom.window.document.querySelector('ul.pagination>li.active>a').innerHTML;
+1 -2
View File
@@ -84,8 +84,7 @@ export default class YACSProvider extends BasicFetcher implements IBasicProvider
}; };
} }
async listProblem(page: number, resync = false) { async listProblem(page: number) {
if (resync && page > 1) return [];
const data = await this.getData(`/problem?pi=${page}`); const data = await this.getData(`/problem?pi=${page}`);
return data.problemList return data.problemList
.filter((problem) => problem.contest.status === '榜单已公布') .filter((problem) => problem.contest.status === '榜单已公布')