diff --git a/README.md b/README.md index 0fae4ee29..1f6630583 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,10 @@ Hydro 同时支持 VJudge,这意味着你可以直接在系统内导入其他 - [UOJ](https://uoj.ac):国内知名 OJ,国家集训队常用; - [SPOJ](https://www.spoj.com):国内连接很不稳定,不推荐; - [洛谷](https://www.luogu.com.cn):使用此功能需要向洛谷购买授权; +- [HDUOJ](https://acm.hdu.edu.cn):杭州电子科技大学在线评测系统,其中包含多校训练题; - [CSGOJ](https://cpc.csgrandeur.cn):广东省赛与湖南省赛赛题评测平台; - [POJ](https://poj.org):较为古董,服务器稳定性差; +- [YACS](https://iai.sh.cn)上海市计算机学会竞赛平台; - HUSTOJ:理论上支持所有 HUSTOJ 驱动的系统,但由于各个系统中 UI 有差异,通常需要手动适配。 ### 多赛制支持 diff --git a/packages/hydrooj/src/handler/judge.ts b/packages/hydrooj/src/handler/judge.ts index 02523cb2b..83c96a03a 100644 --- a/packages/hydrooj/src/handler/judge.ts +++ b/packages/hydrooj/src/handler/judge.ts @@ -73,7 +73,7 @@ export async function next(body: Partial & { rdoc: RecordDoc }) if (!rdoc) return null; } const { $set, $push } = processPayload(rdoc, body); - rdoc = await record.update(body.rdoc.domainId, body.rid, $set, $push, {}, body.addProgress ? { progress: body.addProgress } : {}); + rdoc = await record.update(rdoc.domainId, body.rid, $set, $push, {}, body.addProgress ? { progress: body.addProgress } : {}); bus.broadcast('record/change', rdoc, $set, $push, body); return rdoc; } diff --git a/packages/vjudge/src/model.ts b/packages/vjudge/src/model.ts index 564e1ed49..a7319fd86 100644 --- a/packages/vjudge/src/model.ts +++ b/packages/vjudge/src/model.ts @@ -42,8 +42,8 @@ class AccountService { async judge(task) { const rdoc = await RecordModel.get(task.domainId, task.rid); task = Object.assign(rdoc, task); - const next = (payload) => JudgeHandler.next({ ...payload, rid: task.rid }); - const end = (payload) => JudgeHandler.end({ ...payload, rid: task.rid }); + const next = (payload) => JudgeHandler.next({ ...payload, rid: task.rid, rdoc }); + const end = (payload) => JudgeHandler.end({ ...payload, rid: task.rid, rdoc }); await next({ status: STATUS.STATUS_FETCHED }); try { const langConfig = SettingModel.langs[task.lang]; diff --git a/packages/vjudge/src/providers/index.ts b/packages/vjudge/src/providers/index.ts index 5dca1bd38..9b3b6d8cf 100644 --- a/packages/vjudge/src/providers/index.ts +++ b/packages/vjudge/src/providers/index.ts @@ -9,6 +9,7 @@ import luogu from './luogu'; import poj from './poj'; import spoj from './spoj'; import uoj from './uoj'; +import yacs from './yacs'; const vjudge: Record = { codeforces, @@ -23,5 +24,6 @@ const vjudge: Record = { ybt, ybtbas, hduoj, + yacs, }; export default vjudge; diff --git a/packages/vjudge/src/providers/yacs.ts b/packages/vjudge/src/providers/yacs.ts new file mode 100644 index 000000000..5f6d6a56a --- /dev/null +++ b/packages/vjudge/src/providers/yacs.ts @@ -0,0 +1,160 @@ +/* eslint-disable no-await-in-loop */ +import { JSDOM } from 'jsdom'; +import { + Logger, md5, sleep, STATUS, yaml, +} from 'hydrooj'; +import { BasicFetcher } from '../fetch'; +import { IBasicProvider, RemoteAccount } from '../interface'; +import { VERDICT } from '../verdict'; + +const logger = new Logger('remote/yacs'); + +export default class YACSProvider extends BasicFetcher implements IBasicProvider { + constructor(public account: RemoteAccount, private save: (data: any) => Promise) { + super(account, 'https://api.iai.sh.cn', 'form', logger); + } + + async getData(url: string) { + const { text } = await this.get(new URL(url, 'https://iai.sh.cn').toString()); + const { window: { document } } = new JSDOM(text); + return JSON.parse(document.querySelector('script#__NEXT_DATA__').innerHTML).props.pageProps; + } + + token: string; + + get loggedIn(): Promise { + return new Promise((resolve) => { + this.get('/user/fetchByToken').query({ token: this.token }) + .end((err, { status }) => resolve(status === 200)); + }); + } + + async ensureLogin() { + if (await this.loggedIn) return true; + logger.info('retry login'); + try { + const { body: { token } } = await this.post('/user/login') + .send({ + username: this.account.handle, + password: /^[a-f0-9]{32}$/.test(this.account.password) + ? this.account.password + : md5(`${this.account.password}yacs`), + }); + this.token = token; + } catch (e) { } + return await this.loggedIn; + } + + async getProblem(id: string) { + logger.info(id); + const { problem } = await this.getData(`/problem/${id.split('P')[1]}`); + const tag = []; + if (problem.level) tag.push(problem.level); + if (problem.contest) tag.push(problem.contest.name); + let content = ''; + if (problem.background.trim()) content += `## 题目背景\n\n${problem.background.trim()}\n\n`; + content += `## 题目描述\n\n${problem.description.trim()}\n\n`; + content += `## 输入格式\n\n${problem.inputFormat.trim()}\n\n`; + content += `## 输出格式\n\n${problem.outputFormat.trim()}\n\n`; + content += problem.exampleList.map((sample, index) => { + const sampleId = index + 1; + let ret = ''; + ret += `\`\`\`input${sampleId}\n${sample.input.trim()}\n\`\`\`\n\n`; + ret += `\`\`\`output${sampleId}\n${sample.output.trim()}\n\`\`\`\n\n`; + if (sample.note && sample.note.trim()) ret += `## 样例解释 ${sampleId}\n\n${sample.note.trim()}\n\n`; + return ret; + }).join(''); + content += `## 数据范围\n\n${problem.dataRange.trim()}\n\n`; + if (problem.source.trim()) content += `## 来源\n\n${problem.source.trim()}\n\n`; + return { + title: problem.title, + data: { + 'config.yaml': Buffer.from(yaml.dump({ + time: `${problem.limitTime}ms`, + memory: `${problem.limitMemory}m`, + type: 'remote_judge', + subType: 'yacs', + target: id, + })), + }, + files: {}, + tag, + content, + }; + } + + async listProblem(page: number, resync = false) { + if (resync && page > 1) return []; + const data = await this.getData(`/problem?pi=${page}`); + return data.problemList + .filter((problem) => problem.contest.status === '榜单已公布') + .map((problem) => `P${problem.id}`); + } + + async submitProblem(id: string, lang: string, code: string, info, next, end) { + if (!['C++', 'Python 3.6'].includes(lang)) { + end({ status: STATUS.STATUS_COMPILE_ERROR, message: `Language not supported: ${lang}` }); + return null; + } + const { body } = await this.post('/submission/submit') + .set('Yacs-Token', this.token) + .send({ + code, + language: lang, + problemId: +id.split('P')[1], + }); + return `${body.id}`; + } + + async waitForSubmission(id: string, next, end) { + const done = {}; + // eslint-disable-next-line no-constant-condition + while (true) { + await sleep(3000); + const { submission } = await this.getData(`/submission/${id}`); + const status = VERDICT[submission.finalStatus]; + if (status === STATUS.STATUS_COMPILE_ERROR) { + return await end({ + status, + compilerText: submission.finalErrorInfo, + score: 0, + time: 0, + memory: 0, + }); + } + const cases = []; + let taskId = 0; + for (const task of submission.finalTaskList) { + taskId++; + let pointId = 0; + for (const point of (task as any).dataPointList) { + pointId++; + if (done[`${taskId}.${pointId}`]) continue; + if (VERDICT[point.status] === STATUS.STATUS_JUDGING) continue; + done[`${taskId}.${pointId}`] = true; + cases.push({ + id: pointId, + subtaskId: taskId, + score: point.scoreGet, + time: 0, + memory: 0, + status: VERDICT[point.status], + }); + } + } + await next({ + status, + score: submission.finalScoreGet, + cases, + }); + if (status !== STATUS.STATUS_JUDGING) { + return await end({ + status, + score: submission.finalScoreGet, + time: submission.finalTimeUsage, + memory: submission.finalMemoryUsage / 1024, + }); + } + } + } +} diff --git a/packages/vjudge/src/verdict.ts b/packages/vjudge/src/verdict.ts index d0be54f21..32bfac51b 100644 --- a/packages/vjudge/src/verdict.ts +++ b/packages/vjudge/src/verdict.ts @@ -39,6 +39,16 @@ export const VERDICT = new Proxy>({ SUBMIT_FAILED_PERM: STATUS.STATUS_SYSTEM_ERROR, SUBMIT_FAILED_TEMP: STATUS.STATUS_SYSTEM_ERROR, + // YACS + 正在评测: STATUS.STATUS_JUDGING, + 答案正确: STATUS.STATUS_ACCEPTED, + 编译失败: STATUS.STATUS_COMPILE_ERROR, + 部分正确: STATUS.STATUS_WRONG_ANSWER, + 运行超时: STATUS.STATUS_TIME_LIMIT_EXCEEDED, + 内存超出: STATUS.STATUS_MEMORY_LIMIT_EXCEEDED, + 暂未公布: STATUS.STATUS_SYSTEM_ERROR, + 评测机故障: STATUS.STATUS_SYSTEM_ERROR, + // HustOJ WAITING: STATUS.STATUS_WAITING, 等待: STATUS.STATUS_WAITING,