mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
Merge branch 'master' into milkdown
This commit is contained in:
commit
08c0d34420
137 files changed
+2793
-1519
No files matched your search
+22
-20
@@ -18,51 +18,53 @@
|
|||||||
"benchmark": "cross-env BENCHMARK=true mocha",
|
"benchmark": "cross-env BENCHMARK=true mocha",
|
||||||
"lint": "eslint packages --ext ts --fix",
|
"lint": "eslint packages --ext ts --fix",
|
||||||
"lint:ci": "eslint packages --ext ts",
|
"lint:ci": "eslint packages --ext ts",
|
||||||
"lint:ui": "yarn workspace @hydrooj/ui-default eslint --ext .js,.ts,.jsx,.tsx . --fix",
|
"lint:ui": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx . --fix",
|
||||||
"lint:ui:ci": "yarn workspace @hydrooj/ui-default eslint --ext .js,.ts,.jsx,.tsx .",
|
"lint:ui:ci": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx .",
|
||||||
"typedoc": "cross-env NODE_OPTIONS=--max_old_space_size=8192 typedoc",
|
"typedoc": "cross-env NODE_OPTIONS=--max_old_space_size=8192 typedoc",
|
||||||
"debug:all": "node --trace-warnings --async-stack-traces --trace-deprecation build/start --debug --template",
|
"debug:all": "node --trace-warnings --async-stack-traces --trace-deprecation build/start --debug --template",
|
||||||
"debug": "node --trace-warnings --async-stack-traces --trace-deprecation node_modules/hydrooj/bin/hydrooj --debug --template",
|
"debug": "node --trace-warnings --async-stack-traces --trace-deprecation node_modules/hydrooj/bin/hydrooj --debug --template",
|
||||||
"start:all": "node build/start",
|
"start:all": "node build/start",
|
||||||
"start": "node_modules/.bin/hydrooj",
|
"start": "node_modules/.bin/hydrooj",
|
||||||
"postinstall": "node build/hack && node build/prepare.js && rm -rf node_modules/esbuild-loader/node_modules/esbuild"
|
"postinstall": "node build/hack && node build/prepare.js"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/autocannon": "^7.6.1",
|
"@types/autocannon": "^7.6.1",
|
||||||
"@types/cross-spawn": "^6.0.2",
|
"@types/cross-spawn": "^6.0.2",
|
||||||
"@types/mocha": "^9.1.0",
|
"@types/mocha": "^9.1.1",
|
||||||
"@types/node": "^17.0.23",
|
"@types/node": "^17.0.32",
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "5.14.0",
|
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
||||||
"@typescript-eslint/parser": "5.14.0",
|
"@typescript-eslint/parser": "^5.23.0",
|
||||||
"autocannon": "^7.8.1",
|
"autocannon": "^7.9.0",
|
||||||
"cac": "^6.7.12",
|
"cac": "^6.7.12",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"esbuild": "0.14.3",
|
"esbuild": "0.14.3",
|
||||||
"eslint": "^8.12.0",
|
"eslint": "^8.15.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "16.1.1",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
"eslint-import-resolver-typescript": "2.5.0",
|
"eslint-import-resolver-typescript": "^2.7.1",
|
||||||
"eslint-import-resolver-webpack": "^0.13.2",
|
"eslint-import-resolver-webpack": "^0.13.2",
|
||||||
"eslint-plugin-import": "2.25.4",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.1.0",
|
||||||
"globby": "11.1.0",
|
"globby": "11.1.0",
|
||||||
"latest-version": "^6.0.0",
|
"latest-version": "^6.0.0",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^10.0.0",
|
||||||
"mongo-mock": "^4.1.0",
|
|
||||||
"mongodb": "^3.7.3",
|
"mongodb": "^3.7.3",
|
||||||
"mongodb-memory-server": "^8.4.2",
|
"mongodb-memory-server": "^8.5.2",
|
||||||
"ora": "5.4.1",
|
"ora": "5.4.1",
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.7",
|
||||||
"supertest": "^6.2.2",
|
"supertest": "^6.2.3",
|
||||||
"typedoc": "^0.22.13",
|
"typedoc": "^0.22.15",
|
||||||
"typescript": "4.6.2"
|
"typescript": "^4.6.4"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"esbuild": "0.14.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"preferUnplugged": true,
|
"preferUnplugged": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"superagent": "^7.1.2"
|
"superagent": "^7.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-yaml": "^4.0.5"
|
"@types/js-yaml": "^4.0.5"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/elastic-search",
|
"name": "@hydrooj/elastic-search",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro",
|
"repository": "https://github.com/hydro-dev/Hydro",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elastic/elasticsearch": "^8.1.0"
|
"@elastic/elasticsearch": "^8.2.0-patch.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,9 @@ bus.on('problem/del', async (domainId, docId) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
global.Hydro.lib.problemSearch = async (domainId, q, opts) => {
|
global.Hydro.lib.problemSearch = async (domainId, q, opts) => {
|
||||||
|
const allowedSize = system.get('elasic-search.indexSize') || 10000;
|
||||||
const size = opts?.limit || system.get('pagination.problem');
|
const size = opts?.limit || system.get('pagination.problem');
|
||||||
const from = opts?.skip || 0;
|
const from = Math.min(allowedSize - size, opts?.skip || 0);
|
||||||
const union = await DomainModel.getUnion(domainId);
|
const union = await DomainModel.getUnion(domainId);
|
||||||
const domainIds = [domainId, ...(union?.union || [])];
|
const domainIds = [domainId, ...(union?.union || [])];
|
||||||
const res = await client.search({
|
const res = await client.search({
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
url:
|
url:
|
||||||
value: http://127.0.0.1:9200
|
value: http://127.0.0.1:9200
|
||||||
|
indexSize:
|
||||||
|
value: 10000
|
||||||
@@ -104,15 +104,15 @@ class FpsProblemImportHandler extends Handler {
|
|||||||
if (!this.request.files.file) throw new ValidationError('file');
|
if (!this.request.files.file) throw new ValidationError('file');
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
try {
|
try {
|
||||||
const file = await fs.stat(this.request.files.file.path);
|
const file = await fs.stat(this.request.files.file.filepath);
|
||||||
if (file.size > 64 * 1024 * 1024) throw new FileTooLargeError('64m');
|
if (file.size > 64 * 1024 * 1024) throw new FileTooLargeError('64m');
|
||||||
const content = fs.readFileSync(this.request.files.file.path, 'utf-8');
|
const content = fs.readFileSync(this.request.files.file.filepath, 'utf-8');
|
||||||
const result = await xml2js.parseStringPromise(content);
|
const result = await xml2js.parseStringPromise(content);
|
||||||
tasks.push(result);
|
tasks.push(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof FileTooLargeError) throw e;
|
if (e instanceof FileTooLargeError) throw e;
|
||||||
console.log(e);
|
console.log(e);
|
||||||
const zip = new AdmZip(this.request.files.file.path);
|
const zip = new AdmZip(this.request.files.file.filepath);
|
||||||
for (const entry of zip.getEntries()) {
|
for (const entry of zip.getEntries()) {
|
||||||
try {
|
try {
|
||||||
const buf = entry.getData();
|
const buf = entry.getData();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/fps-importer",
|
"name": "@hydrooj/fps-importer",
|
||||||
"version": "1.3.3",
|
"version": "1.3.4",
|
||||||
"description": "Import FPS problems",
|
"description": "Import FPS problems",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
@@ -10,12 +10,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "0.5.5",
|
"adm-zip": "0.5.5",
|
||||||
"decode-html": "^2.0.0",
|
"decode-html": "^2.0.0",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.1.0",
|
||||||
"mongodb": "^3.7.3",
|
"mongodb": "^3.7.3",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/xml2js": "^0.4.9"
|
"@types/xml2js": "^0.4.11"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"hydrooj": "*"
|
"hydrooj": "*"
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/hydrojudge",
|
"name": "@hydrooj/hydrojudge",
|
||||||
"bin": "bin/hydrojudge.js",
|
"bin": "bin/hydrojudge.js",
|
||||||
"version": "2.18.1",
|
"version": "2.20.4",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hydrooj/utils": "workspace:*",
|
"@hydrooj/utils": "workspace:*",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.27.2",
|
||||||
"cac": "^6.7.12",
|
"cac": "^6.7.12",
|
||||||
"esbuild": "0.14.3",
|
"esbuild": "0.14.3",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.1.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mongodb": "^3.7.3",
|
"mongodb": "^3.7.3",
|
||||||
"p-queue": "^7.2.0",
|
"p-queue": "^7.2.0",
|
||||||
"shell-quote": "^1.7.3",
|
"shell-quote": "^1.7.3",
|
||||||
"ws": "^8.5.0"
|
"ws": "^8.6.0"
|
||||||
},
|
},
|
||||||
"preferUnplugged": true,
|
"preferUnplugged": true,
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/lodash": "^4.14.181",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/shell-quote": "^1.7.1",
|
"@types/shell-quote": "^1.7.1",
|
||||||
"@types/ws": "^8.5.3"
|
"@types/ws": "^8.5.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,11 +37,6 @@ memoryMax:
|
|||||||
type: text
|
type: text
|
||||||
name: memoryMax
|
name: memoryMax
|
||||||
default: 512m
|
default: 512m
|
||||||
retry_delay_sec:
|
|
||||||
type: number
|
|
||||||
name: retry_delay_sec
|
|
||||||
desc: Retry delay sec
|
|
||||||
default: 15
|
|
||||||
cache_dir:
|
cache_dir:
|
||||||
type: text
|
type: text
|
||||||
name: cache_dir
|
name: cache_dir
|
||||||
@@ -52,6 +47,10 @@ parallelism:
|
|||||||
name: parallelism
|
name: parallelism
|
||||||
desc: Runner parallelism
|
desc: Runner parallelism
|
||||||
default: 2
|
default: 2
|
||||||
|
singleTaskParallelism:
|
||||||
|
type: number
|
||||||
|
name: singleTaskParallelism
|
||||||
|
default: 2
|
||||||
rate:
|
rate:
|
||||||
type: float
|
type: float
|
||||||
name: rate
|
name: rate
|
||||||
|
|||||||
@@ -1,233 +1,104 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { max, sum } from 'lodash';
|
import { max, sum } from 'lodash';
|
||||||
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
|
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
|
||||||
|
import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
|
||||||
import { changeErrorType } from '@hydrooj/utils/lib/utils';
|
import { changeErrorType } from '@hydrooj/utils/lib/utils';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import { FormatError, SystemError } from './error';
|
import { FormatError, SystemError } from './error';
|
||||||
import { ensureFile, parseMemoryMB, parseTimeMS } from './utils';
|
import { ensureFile, parseMemoryMB } from './utils';
|
||||||
|
|
||||||
interface Re0 {
|
|
||||||
reg: RegExp,
|
|
||||||
output: ((a: RegExpExecArray) => string)[],
|
|
||||||
id: (a: RegExpExecArray) => number,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Re1 extends Re0 {
|
|
||||||
subtask: (a: RegExpExecArray) => number,
|
|
||||||
}
|
|
||||||
|
|
||||||
const RE0: Re0[] = [
|
|
||||||
{
|
|
||||||
reg: /^([^\d]*)(\d+).(in|txt)$/,
|
|
||||||
output: [
|
|
||||||
(a) => `${a[1] + a[2]}.out`,
|
|
||||||
(a) => `${a[1] + a[2]}.ans`,
|
|
||||||
(a) => `${a[1] + a[2]}.out`.replace(/input/g, 'output'),
|
|
||||||
(a) => (a[1].includes('input') ? `${a[1] + a[2]}.txt`.replace(/input/g, 'output') : null),
|
|
||||||
],
|
|
||||||
id: (a) => +a[2],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: /^([^\d]*)\.in(\d+)$/,
|
|
||||||
output: [
|
|
||||||
(a) => `${a[1]}.ou${a[2]}`,
|
|
||||||
(a) => `${a[1]}.ou${a[2]}`.replace(/input/g, 'output'),
|
|
||||||
],
|
|
||||||
id: (a) => +a[2],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const RE1: Re1[] = [
|
|
||||||
{
|
|
||||||
reg: /^([^\d]*)([0-9]+)([-_])([0-9]+).in$/,
|
|
||||||
output: [(a) => `${a[1] + a[2]}${a[3]}${a[4]}.out`],
|
|
||||||
subtask: (a) => +a[2],
|
|
||||||
id: (a) => +a[4],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function read0(folder: string, files: string[], checkFile, cfg) {
|
|
||||||
const cases = [];
|
|
||||||
for (const file of files) {
|
|
||||||
for (const REG of RE0) {
|
|
||||||
if (REG.reg.test(file)) {
|
|
||||||
const data = REG.reg.exec(file);
|
|
||||||
const c = { input: file, output: '', id: REG.id(data) };
|
|
||||||
for (const func of REG.output) {
|
|
||||||
if (cfg.noOutputFile) c.output = '/dev/null';
|
|
||||||
else c.output = func(data);
|
|
||||||
if (c.output && (c.output === '/dev/null' || fs.existsSync(path.resolve(folder, c.output)))) {
|
|
||||||
cases.push(c);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cases.sort((a, b) => (a.id - b.id));
|
|
||||||
const extra = cases.length - (100 % cases.length);
|
|
||||||
const config = {
|
|
||||||
count: 0,
|
|
||||||
subtasks: [{
|
|
||||||
time: parseTimeMS(cfg.time || '1s'),
|
|
||||||
memory: parseMemoryMB(cfg.memory || '256m'),
|
|
||||||
type: 'sum',
|
|
||||||
cases: [],
|
|
||||||
score: Math.floor(100 / cases.length),
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
for (let i = 0; i < extra; i++) {
|
|
||||||
config.count++;
|
|
||||||
config.subtasks[0].cases.push({
|
|
||||||
id: config.count,
|
|
||||||
input: checkFile(cases[i].input),
|
|
||||||
output: checkFile(cases[i].output),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (extra < cases.length) {
|
|
||||||
config.subtasks.push({
|
|
||||||
time: parseTimeMS(cfg.time || '1s'),
|
|
||||||
memory: parseMemoryMB(cfg.memory || '256m'),
|
|
||||||
type: 'sum',
|
|
||||||
cases: [],
|
|
||||||
score: Math.floor(100 / cases.length) + 1,
|
|
||||||
});
|
|
||||||
for (let i = extra; i < cases.length; i++) {
|
|
||||||
config.count++;
|
|
||||||
config.subtasks[1].cases.push({
|
|
||||||
id: config.count,
|
|
||||||
input: checkFile(cases[i].input),
|
|
||||||
output: checkFile(cases[i].output),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function read1(folder: string, files: string[], checkFile, cfg, rst) {
|
|
||||||
const subtask = {};
|
|
||||||
for (const s of rst.subtasks) if (s.id) subtask[s.id] = s;
|
|
||||||
const subtasks = [];
|
|
||||||
for (const file of files) {
|
|
||||||
for (const REG of RE1) {
|
|
||||||
if (REG.reg.test(file)) {
|
|
||||||
const data = REG.reg.exec(file);
|
|
||||||
const sid = REG.subtask(data);
|
|
||||||
const c = { input: file, output: '', id: REG.id(data) };
|
|
||||||
for (const func of REG.output) {
|
|
||||||
if (cfg.noOutputFile) c.output = '/dev/null';
|
|
||||||
else c.output = func(data);
|
|
||||||
if (c.output === '/dev/null' || fs.existsSync(path.resolve(folder, c.output))) {
|
|
||||||
if (!subtask[sid]) {
|
|
||||||
subtask[sid] = {
|
|
||||||
time: parseTimeMS(cfg.time || '1s'),
|
|
||||||
memory: parseMemoryMB(cfg.memory || '256m'),
|
|
||||||
type: 'min',
|
|
||||||
cases: [c],
|
|
||||||
};
|
|
||||||
} else if (!subtask[sid].cases) subtask[sid].cases = [c];
|
|
||||||
else subtask[sid].cases.push(c);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const i in subtask) {
|
|
||||||
subtask[i].cases.sort((a, b) => (a.id - b.id));
|
|
||||||
subtasks.push(subtask[i]);
|
|
||||||
}
|
|
||||||
const base = Math.floor(100 / subtasks.length);
|
|
||||||
const extra = subtasks.length - (100 % subtasks.length);
|
|
||||||
const config = { count: 0, subtasks };
|
|
||||||
const keys = Object.keys(subtask);
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
if (i >= extra) subtask[keys[i]].score = base + 1;
|
|
||||||
else subtask[keys[i]].score = base;
|
|
||||||
for (const j of subtask[keys[i]].cases) {
|
|
||||||
config.count++;
|
|
||||||
j.input = checkFile(j.input);
|
|
||||||
j.output = checkFile(j.output);
|
|
||||||
j.id = config.count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readAutoCases(folder: string, { next }, cfg, rst) {
|
|
||||||
const config = {
|
|
||||||
checker_type: 'default',
|
|
||||||
count: 0,
|
|
||||||
subtasks: [],
|
|
||||||
judge_extra_files: [],
|
|
||||||
user_extra_files: [],
|
|
||||||
};
|
|
||||||
const checkFile = ensureFile(folder);
|
|
||||||
try {
|
|
||||||
const files = await fs.readdir(folder);
|
|
||||||
if (await fs.pathExists(path.resolve(folder, 'input'))) {
|
|
||||||
const inputs = await fs.readdir(path.resolve(folder, 'input'));
|
|
||||||
files.push(...inputs.map((i) => `input/${i}`));
|
|
||||||
}
|
|
||||||
if (await fs.pathExists(path.resolve(folder, 'output'))) {
|
|
||||||
const outputs = await fs.readdir(path.resolve(folder, 'output'));
|
|
||||||
files.push(...outputs.map((i) => `output/${i}`));
|
|
||||||
}
|
|
||||||
let result = await read0(folder, files, checkFile, cfg);
|
|
||||||
if (!result.count) result = await read1(folder, files, checkFile, cfg, rst);
|
|
||||||
Object.assign(config, result);
|
|
||||||
if (cfg.isSelfSubmission) next({ message: { message: 'Found {0} testcases.', params: [config.count] } });
|
|
||||||
} catch (e) {
|
|
||||||
throw new SystemError('Cannot parse testdata.', [e.message, ...e.params]);
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidConfig(config) {
|
function isValidConfig(config) {
|
||||||
if (config.count > (getConfig('testcases_max') || 100)) {
|
if (config.count > (getConfig('testcases_max') || 100)) {
|
||||||
throw new FormatError('Too many testcases. Cancelled.');
|
throw new FormatError('Too many testcases. Cancelled.');
|
||||||
}
|
}
|
||||||
const time = sum(config.subtasks.map((subtask) => subtask.time * subtask.cases.length));
|
const time = sum(config.subtasks.map((subtask) => sum(subtask.cases.map((c) => c.time))));
|
||||||
if (time > (getConfig('total_time_limit') || 60) * 1000) {
|
if (time > (getConfig('total_time_limit') || 60) * 1000) {
|
||||||
throw new FormatError('Total time limit longer than {0}s. Cancelled.', [+getConfig('total_time_limit') || 60]);
|
throw new FormatError('Total time limit longer than {0}s. Cancelled.', [+getConfig('total_time_limit') || 60]);
|
||||||
}
|
}
|
||||||
const memMax = max(config.subtasks.map((subtask) => subtask.memory));
|
const memMax = max(config.subtasks.map((subtask) => max(subtask.cases.map((c) => c.memory))));
|
||||||
if (memMax > parseMemoryMB(getConfig('memoryMax'))) throw new FormatError('Memory limit larger than memory_max');
|
if (memMax > parseMemoryMB(getConfig('memoryMax'))) throw new FormatError('Memory limit larger than memory_max');
|
||||||
if (!['default', 'strict'].includes(config.checker_type || 'default') && !config.checker) {
|
if (!['default', 'strict'].includes(config.checker_type || 'default') && !config.checker) {
|
||||||
throw new FormatError('You did not specify a checker.');
|
throw new FormatError('You did not specify a checker.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function collectFiles(folder: string) {
|
||||||
|
const files = await fs.readdir(folder);
|
||||||
|
await Promise.all(['input', 'output'].map(async (t) => {
|
||||||
|
if (await fs.pathExists(path.resolve(folder, t))) {
|
||||||
|
const f = await fs.readdir(path.resolve(folder, t));
|
||||||
|
files.push(...f.map((i) => `${t}/${i}`));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processTestdata(folder: string) {
|
||||||
|
let files = await fs.readdir(folder);
|
||||||
|
if (files.length <= 2) {
|
||||||
|
if (files.length === 2) files.splice(files.indexOf('version'), 1);
|
||||||
|
if (fs.statSync(path.resolve(folder, files[0])).isDirectory()) {
|
||||||
|
folder = path.resolve(folder, files[0]);
|
||||||
|
files = await fs.readdir(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ini = files.filter((i) => i.toLowerCase() === 'config.ini')[0];
|
||||||
|
if (!ini) return;
|
||||||
|
const t = fs.readFileSync(path.resolve(folder, ini), 'utf8');
|
||||||
|
await fs.writeFile(path.resolve(folder, 'config.ini'), t.toLowerCase());
|
||||||
|
for (const i of files) {
|
||||||
|
if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`);
|
||||||
|
if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`);
|
||||||
|
}
|
||||||
|
await Promise.all(['input', 'output'].flatMap(async (f) => {
|
||||||
|
const dir = path.resolve(folder, f);
|
||||||
|
const sf = await fs.readdir(dir);
|
||||||
|
return sf
|
||||||
|
.filter((i) => i !== i.toLowerCase())
|
||||||
|
.map((i) => fs.rename(`${dir}/${i}`, `${dir}/${i.toLowerCase()}`));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export default async function readCases(folder: string, cfg: Record<string, any> = {}, args) {
|
export default async function readCases(folder: string, cfg: Record<string, any> = {}, args) {
|
||||||
const iniConfig = path.resolve(folder, 'config.ini');
|
const iniConfig = path.resolve(folder, 'config.ini');
|
||||||
const yamlConfig = path.resolve(folder, 'config.yaml');
|
const yamlConfig = path.resolve(folder, 'config.yaml');
|
||||||
const ymlConfig = path.resolve(folder, 'config.yml');
|
const ymlConfig = path.resolve(folder, 'config.yml');
|
||||||
let config;
|
const config: Record<string, any> = {
|
||||||
if (fs.existsSync(yamlConfig)) {
|
checker_type: 'default',
|
||||||
config = { ...cfg, ...yaml.load(fs.readFileSync(yamlConfig).toString()) as object };
|
count: 0,
|
||||||
} else if (fs.existsSync(ymlConfig)) {
|
subtasks: [],
|
||||||
config = { ...cfg, ...yaml.load(fs.readFileSync(ymlConfig).toString()) as object };
|
judge_extra_files: [],
|
||||||
} else if (fs.existsSync(iniConfig)) {
|
user_extra_files: [],
|
||||||
try {
|
...cfg,
|
||||||
config = { ...cfg, ...convertIniConfig(fs.readFileSync(iniConfig).toString()) };
|
};
|
||||||
} catch (e) {
|
|
||||||
throw changeErrorType(e, FormatError);
|
|
||||||
}
|
|
||||||
} else config = cfg;
|
|
||||||
let result;
|
|
||||||
try {
|
try {
|
||||||
result = await readYamlCases(config, ensureFile(folder));
|
if (fs.existsSync(yamlConfig)) {
|
||||||
|
Object.assign(config, yaml.load(fs.readFileSync(yamlConfig).toString()));
|
||||||
|
} else if (fs.existsSync(ymlConfig)) {
|
||||||
|
Object.assign(config, yaml.load(fs.readFileSync(ymlConfig).toString()));
|
||||||
|
} else if (fs.existsSync(iniConfig)) {
|
||||||
|
Object.assign(config, convertIniConfig(fs.readFileSync(iniConfig).toString()));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw changeErrorType(e, FormatError);
|
throw changeErrorType(e, FormatError);
|
||||||
}
|
}
|
||||||
let cases = result.outputs?.length || 0;
|
const checkFile = ensureFile(folder);
|
||||||
cases += Math.sum((result.subtasks || []).map((subtask) => subtask.cases.length));
|
const result = await readYamlCases(config, checkFile)
|
||||||
if (!cases) {
|
.catch((e) => { throw changeErrorType(e, FormatError); });
|
||||||
const c = await readAutoCases(folder, args, config, result);
|
result.count = result.outputs?.length || Math.sum((result.subtasks || []).map((s) => s.cases.length));
|
||||||
result.subtasks = c.subtasks;
|
if (!result.count) {
|
||||||
result.count = c.count;
|
try {
|
||||||
|
result.subtasks = readSubtasksFromFiles(await collectFiles(folder), cfg);
|
||||||
|
result.count = Math.sum(result.subtasks.map((i) => i.cases.length));
|
||||||
|
if (cfg.isSelfSubmission) args.next?.({ message: { message: 'Found {0} testcases.', params: [result.count] } });
|
||||||
|
} catch (e) {
|
||||||
|
throw new SystemError('Cannot parse testdata.', [e.message, ...(e.params || [])]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
result.subtasks = normalizeSubtasks(result.subtasks || [], checkFile, config.time, config.memory);
|
||||||
if (result.key && args.key !== result.key) throw new FormatError('Incorrect secret key');
|
if (result.key && args.key !== result.key) throw new FormatError('Incorrect secret key');
|
||||||
if (!result.key) isValidConfig(result);
|
if (!result.key) isValidConfig(result);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export async function check(config: CheckConfig): Promise<[number, number, strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any): Promise<Execute> {
|
export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any): Promise<Execute> {
|
||||||
|
if (['default', 'strict'].includes(checkerType)) {
|
||||||
|
return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
|
||||||
|
}
|
||||||
if (!checkers[checkerType]) throw new SystemError('Unknown checker type {0}.', [checkerType]);
|
if (!checkers[checkerType]) throw new SystemError('Unknown checker type {0}.', [checkerType]);
|
||||||
if (checkerType === 'testlib') copyIn['testlib.h'] = { src: testlibSrc };
|
if (checkerType === 'testlib') copyIn['testlib.h'] = { src: testlibSrc };
|
||||||
const s = checker.replace('@', '.').split('.');
|
const s = checker.replace('@', '.').split('.');
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export = async function compile(
|
|||||||
);
|
);
|
||||||
if (status !== STATUS.STATUS_ACCEPTED) throw new CompileError({ status, stdout, stderr });
|
if (status !== STATUS.STATUS_ACCEPTED) throw new CompileError({ status, stdout, stderr });
|
||||||
if (!fileIds[target]) throw new CompileError({ stderr: 'Executable file not found.' });
|
if (!fileIds[target]) throw new CompileError({ stderr: 'Executable file not found.' });
|
||||||
if (next) next({ compiler_text: compilerText(stdout, stderr) });
|
if (next) next({ compilerText: compilerText(stdout, stderr) });
|
||||||
return {
|
return {
|
||||||
execute,
|
execute,
|
||||||
copyIn: { ...copyIn, [target]: { fileId: fileIds[target] } },
|
copyIn: { ...copyIn, [target]: { fileId: fileIds[target] } },
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable prefer-const */
|
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import cac from 'cac';
|
import cac from 'cac';
|
||||||
@@ -9,19 +8,19 @@ const argv = cac().parse();
|
|||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
let CONFIG_FILE = path.resolve(os.homedir(), '.config', 'hydro', 'judge.yaml');
|
let CONFIG_FILE = path.resolve(os.homedir(), '.config', 'hydro', 'judge.yaml');
|
||||||
|
|
||||||
let config = {
|
const config = {
|
||||||
cache_dir: path.resolve(os.homedir(), '.cache', 'hydro', 'judge'),
|
cache_dir: path.resolve(os.homedir(), '.cache', 'hydro', 'judge'),
|
||||||
tmp_dir: path.resolve(os.tmpdir(), 'hydro', 'judge'),
|
tmp_dir: path.resolve(os.tmpdir(), 'hydro', 'judge'),
|
||||||
tmpfs_size: '256m',
|
tmpfs_size: '256m',
|
||||||
stdio_size: '32m',
|
stdio_size: '32m',
|
||||||
memoryMax: '512m',
|
memoryMax: '512m',
|
||||||
strict_memory: false,
|
strict_memory: false,
|
||||||
retry_delay_sec: 15,
|
|
||||||
sandbox_host: 'http://localhost:5050',
|
sandbox_host: 'http://localhost:5050',
|
||||||
testcases_max: 100,
|
testcases_max: 100,
|
||||||
total_time_limit: 60,
|
total_time_limit: 60,
|
||||||
processLimit: 32,
|
processLimit: 32,
|
||||||
parallelism: 2,
|
parallelism: 2,
|
||||||
|
singleTaskParallelism: 2,
|
||||||
rerun: 0,
|
rerun: 0,
|
||||||
rate: 1,
|
rate: 1,
|
||||||
config: null,
|
config: null,
|
||||||
@@ -29,11 +28,12 @@ let config = {
|
|||||||
env: `\
|
env: `\
|
||||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
HOME=/w`,
|
HOME=/w`,
|
||||||
|
hosts: {},
|
||||||
secret: String.random(32),
|
secret: String.random(32),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getConfig(key: string) {
|
export function getConfig(key: keyof typeof config) {
|
||||||
return global.Hydro ? global.Hydro.model.system.get(`hydrojudge.${key}`) : config[key];
|
return global.Hydro ? global.Hydro.model.system.get(`hydrojudge.${key}`) ?? config[key] : config[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(path.resolve(process.cwd(), '.env'))) {
|
if (fs.existsSync(path.resolve(process.cwd(), '.env'))) {
|
||||||
@@ -61,5 +61,5 @@ if (!global.Hydro) {
|
|||||||
config.sandbox_host = path.resolve(process.env.EXECUTION_HOST || argv.options.sandbox);
|
config.sandbox_host = path.resolve(process.env.EXECUTION_HOST || argv.options.sandbox);
|
||||||
}
|
}
|
||||||
const configFile = fs.readFileSync(CONFIG_FILE).toString();
|
const configFile = fs.readFileSync(CONFIG_FILE).toString();
|
||||||
config = { ...config, ...yaml.load(configFile) as any };
|
Object.assign(config, yaml.load(configFile) as any);
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ import './utils';
|
|||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import * as Session from './hosts/index';
|
import * as Session from './hosts/index';
|
||||||
import log from './log';
|
import log from './log';
|
||||||
import { Queue, sleep } from './utils';
|
import { Queue } from './utils';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace NodeJS {
|
namespace NodeJS {
|
||||||
@@ -28,8 +28,8 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!global.onDestroy) global.onDestroy = [];
|
global.onDestroy ||= [];
|
||||||
if (!global.hosts) global.hosts = [];
|
global.hosts ||= [];
|
||||||
let exit = false;
|
let exit = false;
|
||||||
|
|
||||||
const terminate = async () => {
|
const terminate = async () => {
|
||||||
@@ -60,28 +60,16 @@ async function worker(queue: Queue<any>) {
|
|||||||
|
|
||||||
async function daemon() {
|
async function daemon() {
|
||||||
const _hosts = getConfig('hosts');
|
const _hosts = getConfig('hosts');
|
||||||
const delay = getConfig('retry_delay_sec');
|
|
||||||
const hosts = {};
|
const hosts = {};
|
||||||
const queue = new Queue<any>();
|
const queue = new Queue<any>();
|
||||||
|
worker(queue).catch((e) => log.error(e));
|
||||||
for (const i in _hosts) {
|
for (const i in _hosts) {
|
||||||
_hosts[i].host = _hosts[i].host || i;
|
_hosts[i].host = _hosts[i].host || i;
|
||||||
hosts[i] = new Session[_hosts[i].type || 'vj4'](_hosts[i]);
|
hosts[i] = new Session[_hosts[i].type || 'hydro'](_hosts[i]);
|
||||||
await hosts[i].init();
|
await hosts[i].init();
|
||||||
}
|
}
|
||||||
|
for (const i in hosts) hosts[i].consume(queue);
|
||||||
global.hosts = hosts;
|
global.hosts = hosts;
|
||||||
worker(queue);
|
|
||||||
for (const i in hosts) {
|
|
||||||
while ('Orz twd2') {
|
|
||||||
try {
|
|
||||||
await hosts[i].consume(queue);
|
|
||||||
break;
|
|
||||||
} catch (e) {
|
|
||||||
log.error(e, e.stack);
|
|
||||||
log.info(`在 ${delay} 秒后重试`);
|
|
||||||
await sleep(delay * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) daemon();
|
if (require.main === module) daemon();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export class CompileError extends Error {
|
|||||||
stdout: string;
|
stdout: string;
|
||||||
stderr: string;
|
stderr: string;
|
||||||
status: string;
|
status: string;
|
||||||
type: string;
|
type = 'CompileError';
|
||||||
|
|
||||||
constructor(obj: string | CompileErrorInfo) {
|
constructor(obj: string | CompileErrorInfo) {
|
||||||
super('Compile Error');
|
super('Compile Error');
|
||||||
@@ -17,48 +17,29 @@ export class CompileError extends Error {
|
|||||||
this.stderr = obj.stderr || '';
|
this.stderr = obj.stderr || '';
|
||||||
this.status = obj.status ? STATUS_TEXTS[obj.status] || '' : '';
|
this.status = obj.status ? STATUS_TEXTS[obj.status] || '' : '';
|
||||||
}
|
}
|
||||||
this.type = 'CompileError';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FormatError extends Error {
|
export class FormatError extends Error {
|
||||||
type: string;
|
type = 'FormatError';
|
||||||
params: any[];
|
|
||||||
|
|
||||||
constructor(message: string, params = []) {
|
constructor(message: string, public params = []) {
|
||||||
super(message);
|
super(message);
|
||||||
this.type = 'FormatError';
|
|
||||||
this.params = params;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RuntimeError extends Error {
|
export class RuntimeError extends Error {
|
||||||
type: string;
|
type = 'RuntimeError';
|
||||||
detail: any;
|
|
||||||
|
|
||||||
constructor(detail: string, message: string) {
|
constructor(public detail: string, message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.type = 'RuntimeError';
|
|
||||||
this.detail = detail;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SystemError extends Error {
|
export class SystemError extends Error {
|
||||||
type: string;
|
type = 'SystemError';
|
||||||
params: any[];
|
|
||||||
|
|
||||||
constructor(message: string, params = []) {
|
constructor(message: string, public params = []) {
|
||||||
super(message);
|
super(message);
|
||||||
this.type = 'SystemError';
|
|
||||||
this.params = params;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TooFrequentError extends Error {
|
|
||||||
type: string;
|
|
||||||
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.type = 'TooFrequentError';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,12 +9,13 @@ import PQueue from 'p-queue';
|
|||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||||
import readCases from '../cases';
|
import type { JudgeResultBody } from 'hydrooj';
|
||||||
|
import readCases, { processTestdata } from '../cases';
|
||||||
import { getConfig } from '../config';
|
import { getConfig } from '../config';
|
||||||
import { CompileError, FormatError, SystemError } from '../error';
|
import { CompileError, FormatError, SystemError } from '../error';
|
||||||
import judge from '../judge';
|
import judge from '../judge';
|
||||||
import log from '../log';
|
import log from '../log';
|
||||||
import { CopyInFile } from '../sandbox/interface';
|
import type { CopyInFile } from '../sandbox/interface';
|
||||||
import * as sysinfo from '../sysinfo';
|
import * as sysinfo from '../sysinfo';
|
||||||
import * as tmpfs from '../tmpfs';
|
import * as tmpfs from '../tmpfs';
|
||||||
import {
|
import {
|
||||||
@@ -82,21 +83,21 @@ class JudgeTask {
|
|||||||
await this.doSubmission(startPromise);
|
await this.doSubmission(startPromise);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof CompileError) {
|
if (e instanceof CompileError) {
|
||||||
this.next({ compiler_text: compilerText(e.stdout, e.stderr) });
|
this.next({ compilerText: compilerText(e.stdout, e.stderr) });
|
||||||
this.end({
|
this.end({
|
||||||
status: STATUS.STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0,
|
status: STATUS.STATUS_COMPILE_ERROR, score: 0, time: 0, memory: 0,
|
||||||
});
|
});
|
||||||
} else if (e instanceof FormatError) {
|
} else if (e instanceof FormatError) {
|
||||||
this.next({ judge_text: 'Testdata configuration incorrect.' });
|
this.next({ message: 'Testdata configuration incorrect.' });
|
||||||
this.next({ judge_text: { message: e.message, params: e.params } });
|
this.next({ message: { message: e.message, params: e.params } });
|
||||||
this.end({
|
this.end({
|
||||||
status: STATUS.STATUS_FORMAT_ERROR, score: 0, time_ms: 0, memory_kb: 0,
|
status: STATUS.STATUS_FORMAT_ERROR, score: 0, time: 0, memory: 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.error(e);
|
log.error(e);
|
||||||
this.next({ message: { message: e.message, params: e.params || [], ...argv.options.debug ? { stack: e.stack } : {} } });
|
this.next({ message: { message: e.message, params: e.params || [], ...argv.options.debug ? { stack: e.stack } : {} } });
|
||||||
this.end({
|
this.end({
|
||||||
status: STATUS.STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0,
|
status: STATUS.STATUS_SYSTEM_ERROR, score: 0, time: 0, memory: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -132,38 +133,21 @@ class JudgeTask {
|
|||||||
await judge[type].judge(this, startPromise);
|
await judge[type].judge(this, startPromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
next(data, id?: number) {
|
next(data: Partial<JudgeResultBody>, id?: number) {
|
||||||
log.debug('Next: %d %o', id, data);
|
log.debug('Next: %d %o', id, data);
|
||||||
data.key = 'next';
|
data.key = 'next';
|
||||||
data.rid = new ObjectID(this.rid);
|
data.rid = new ObjectID(this.rid);
|
||||||
if (data.time_ms) data.time = data.time_ms;
|
|
||||||
if (data.memory_kb) data.memory = data.memory_kb;
|
|
||||||
if (data.compiler_text) data.compilerText = data.compiler_text;
|
|
||||||
if (data.judge_text) data.message = data.judge_text;
|
|
||||||
delete data.time_ms;
|
|
||||||
delete data.memory_kb;
|
|
||||||
delete data.compiler_text;
|
|
||||||
delete data.judge_text;
|
|
||||||
if (data.case) {
|
if (data.case) {
|
||||||
data.case = {
|
data.case.id ||= id;
|
||||||
id,
|
data.case.message ||= '';
|
||||||
status: data.case.status,
|
|
||||||
time: data.case.time_ms || data.case.time,
|
|
||||||
memory: data.case.memory_kb || data.case.memory,
|
|
||||||
message: data.case.message || data.case.judgeText || data.case.judge_text || '',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
this.ws.send(JSON.stringify(data));
|
this.ws.send(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
end(data) {
|
end(data: Partial<JudgeResultBody>) {
|
||||||
log.info('End: %o', data);
|
log.info('End: %o', data);
|
||||||
data.key = 'end';
|
data.key = 'end';
|
||||||
data.rid = new ObjectID(this.request.rid);
|
data.rid = this.request.rid;
|
||||||
data.time = data.time_ms ?? data.time;
|
|
||||||
delete data.time_ms;
|
|
||||||
data.memory = data.memory_kb ?? data.memory;
|
|
||||||
delete data.memory_kb;
|
|
||||||
this.ws.send(JSON.stringify(data));
|
this.ws.send(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +205,7 @@ export default class Hydro {
|
|||||||
}
|
}
|
||||||
if (filenames.length) {
|
if (filenames.length) {
|
||||||
log.info(`Getting problem data: ${this.config.host}/${source}`);
|
log.info(`Getting problem data: ${this.config.host}/${source}`);
|
||||||
if (next) next({ judge_text: 'Syncing testdata, please wait...' });
|
if (next) next({ message: 'Syncing testdata, please wait...' });
|
||||||
await this.ensureLogin();
|
await this.ensureLogin();
|
||||||
const res = await this.axios.post(`/d/${domainId}/judge/files`, {
|
const res = await this.axios.post(`/d/${domainId}/judge/files`, {
|
||||||
pid: +pid,
|
pid: +pid,
|
||||||
@@ -248,7 +232,7 @@ export default class Hydro {
|
|||||||
queue.start();
|
queue.start();
|
||||||
await Promise.all(tasks);
|
await Promise.all(tasks);
|
||||||
fs.writeFileSync(path.join(filePath, 'etags'), JSON.stringify(version));
|
fs.writeFileSync(path.join(filePath, 'etags'), JSON.stringify(version));
|
||||||
await this.processData(filePath).catch(noop);
|
await processTestdata(filePath);
|
||||||
}
|
}
|
||||||
fs.writeFileSync(path.join(filePath, 'lastUsage'), new Date().getTime().toString());
|
fs.writeFileSync(path.join(filePath, 'lastUsage'), new Date().getTime().toString());
|
||||||
return filePath;
|
return filePath;
|
||||||
@@ -348,34 +332,6 @@ export default class Hydro {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async processData(folder: string) { // eslint-disable-line class-methods-use-this
|
|
||||||
let files = await fs.readdir(folder);
|
|
||||||
let ini = false;
|
|
||||||
if (files.length <= 2) {
|
|
||||||
if (files.length === 2) files.splice(files.indexOf('version'), 1);
|
|
||||||
const s = fs.statSync(path.resolve(folder, files[0]));
|
|
||||||
if (s.isDirectory()) folder = path.resolve(folder, files[0]);
|
|
||||||
}
|
|
||||||
files = await fs.readdir(folder);
|
|
||||||
for (const i of files) {
|
|
||||||
if (i.toLowerCase() === 'config.ini') {
|
|
||||||
ini = true;
|
|
||||||
await fs.rename(`${folder}/${i}`, `${folder}/config.ini`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ini) {
|
|
||||||
for (const i of files) {
|
|
||||||
if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`);
|
|
||||||
else if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`);
|
|
||||||
}
|
|
||||||
files = await fs.readdir(`${folder}/input`);
|
|
||||||
for (const i of files) await fs.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`);
|
|
||||||
files = await fs.readdir(`${folder}/output`);
|
|
||||||
for (const i of files) await fs.rename(`${folder}/output/${i}`, `${folder}/output/${i.toLowerCase()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async retry(queue: Queue<any>) {
|
async retry(queue: Queue<any>) {
|
||||||
this.consume(queue).catch(() => {
|
this.consume(queue).catch(() => {
|
||||||
setTimeout(() => this.retry(queue), 30000);
|
setTimeout(() => this.retry(queue), 30000);
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import { Logger } from '../log';
|
|||||||
import { del, run } from '../sandbox';
|
import { del, run } from '../sandbox';
|
||||||
import { CmdFile } from '../sandbox/interface';
|
import { CmdFile } from '../sandbox/interface';
|
||||||
import signals from '../signals';
|
import signals from '../signals';
|
||||||
import { parseFilename } from '../utils';
|
import { NormalizedCase, NormalizedSubtask, parseFilename } from '../utils';
|
||||||
import {
|
import { Context, ContextSubTask } from './interface';
|
||||||
Case, Context, ContextSubTask, SubTask,
|
|
||||||
} from './interface';
|
|
||||||
|
|
||||||
const Score = {
|
const Score = {
|
||||||
sum: (a: number, b: number) => (a + b),
|
sum: (a: number, b: number) => (a + b),
|
||||||
@@ -21,18 +19,18 @@ const Score = {
|
|||||||
|
|
||||||
const logger = new Logger('judge/default');
|
const logger = new Logger('judge/default');
|
||||||
|
|
||||||
function judgeCase(c: Case, sid: string) {
|
function judgeCase(c: NormalizedCase, sid: string) {
|
||||||
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
|
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
|
||||||
if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min')
|
if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min')
|
||||||
|| (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score)
|
|| (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score)
|
||||||
|| ((ctxSubtask.subtask.if || []).filter((i: string) => ctx.failed[i]).length)
|
|| ((ctxSubtask.subtask.if || []).filter((i) => ctx.failed[i]).length)
|
||||||
) {
|
) {
|
||||||
ctx.next({
|
ctx.next({
|
||||||
case: {
|
case: {
|
||||||
status: STATUS.STATUS_CANCELED,
|
status: STATUS.STATUS_CANCELED,
|
||||||
score: 0,
|
score: 0,
|
||||||
time_ms: 0,
|
time: 0,
|
||||||
memory_kb: 0,
|
memory: 0,
|
||||||
message: '',
|
message: '',
|
||||||
},
|
},
|
||||||
addProgress: 100 / ctx.config.count,
|
addProgress: 100 / ctx.config.count,
|
||||||
@@ -76,7 +74,7 @@ function judgeCase(c: Case, sid: string) {
|
|||||||
user_stdout: stdout,
|
user_stdout: stdout,
|
||||||
user_stderr: stderr,
|
user_stderr: stderr,
|
||||||
checker_type: ctx.config.checker_type,
|
checker_type: ctx.config.checker_type,
|
||||||
score: ctxSubtask.subtask.score,
|
score: c.score,
|
||||||
detail: ctx.config.detail ?? true,
|
detail: ctx.config.detail ?? true,
|
||||||
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
|
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
|
||||||
});
|
});
|
||||||
@@ -100,10 +98,11 @@ function judgeCase(c: Case, sid: string) {
|
|||||||
ctx.total_memory_usage_kb = Math.max(ctx.total_memory_usage_kb, memory_usage_kb);
|
ctx.total_memory_usage_kb = Math.max(ctx.total_memory_usage_kb, memory_usage_kb);
|
||||||
ctx.next({
|
ctx.next({
|
||||||
case: {
|
case: {
|
||||||
|
subtaskId: ctxSubtask.subtask.id,
|
||||||
status,
|
status,
|
||||||
score: 0,
|
score,
|
||||||
time_ms: time_usage_ms,
|
time: time_usage_ms,
|
||||||
memory_kb: memory_usage_kb,
|
memory: memory_usage_kb,
|
||||||
message,
|
message,
|
||||||
},
|
},
|
||||||
addProgress: 100 / ctx.config.count,
|
addProgress: 100 / ctx.config.count,
|
||||||
@@ -126,7 +125,7 @@ function judgeCase(c: Case, sid: string) {
|
|||||||
env: ctx.env,
|
env: ctx.env,
|
||||||
});
|
});
|
||||||
const out = r.stdout.toString();
|
const out = r.stdout.toString();
|
||||||
if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) });
|
if (out.length) ctx.next({ compilerText: out.substring(0, 1024) });
|
||||||
if (process.env.DEV) console.log(r);
|
if (process.env.DEV) console.log(r);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.info('Failed to run analysis');
|
logger.info('Failed to run analysis');
|
||||||
@@ -137,7 +136,7 @@ function judgeCase(c: Case, sid: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function judgeSubtask(subtask: SubTask, sid: string) {
|
function judgeSubtask(subtask: NormalizedSubtask, sid: string) {
|
||||||
return async (ctx: Context) => {
|
return async (ctx: Context) => {
|
||||||
subtask.type = subtask.type || 'min';
|
subtask.type = subtask.type || 'min';
|
||||||
const ctxSubtask = {
|
const ctxSubtask = {
|
||||||
@@ -178,21 +177,17 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
|
|||||||
),
|
),
|
||||||
ctx.next,
|
ctx.next,
|
||||||
),
|
),
|
||||||
(() => {
|
compileChecker(
|
||||||
if (['default', 'strict'].includes(ctx.config.checker_type || 'default')) {
|
ctx.getLang,
|
||||||
return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
|
ctx.config.checker_type,
|
||||||
}
|
ctx.config.checker,
|
||||||
const copyIn = { user_code: ctx.code };
|
{
|
||||||
for (const file of ctx.config.judge_extra_files) {
|
user_code: ctx.code,
|
||||||
copyIn[parseFilename(file)] = { src: file };
|
...Object.fromEntries(
|
||||||
}
|
(ctx.config.judge_extra_files || []).map((i) => [parseFilename(i), { src: i }]),
|
||||||
return compileChecker(
|
),
|
||||||
ctx.getLang,
|
},
|
||||||
ctx.config.checker_type,
|
),
|
||||||
ctx.config.checker,
|
|
||||||
copyIn,
|
|
||||||
);
|
|
||||||
})(),
|
|
||||||
]);
|
]);
|
||||||
ctx.clean.push(ctx.execute.clean, ctx.checker.clean);
|
ctx.clean.push(ctx.execute.clean, ctx.checker.clean);
|
||||||
await startPromise;
|
await startPromise;
|
||||||
@@ -203,7 +198,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
|
|||||||
ctx.total_memory_usage_kb = 0;
|
ctx.total_memory_usage_kb = 0;
|
||||||
ctx.total_time_usage_ms = 0;
|
ctx.total_time_usage_ms = 0;
|
||||||
ctx.rerun = getConfig('rerun') || 0;
|
ctx.rerun = getConfig('rerun') || 0;
|
||||||
ctx.queue = new Queue({ concurrency: getConfig('parallelism') });
|
ctx.queue = new Queue({ concurrency: getConfig('singleTaskParallelism') });
|
||||||
ctx.failed = {};
|
ctx.failed = {};
|
||||||
for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid], sid)(ctx));
|
for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid], sid)(ctx));
|
||||||
const scores = await Promise.all(tasks);
|
const scores = await Promise.all(tasks);
|
||||||
@@ -219,7 +214,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
|
|||||||
ctx.end({
|
ctx.end({
|
||||||
status: ctx.total_status,
|
status: ctx.total_status,
|
||||||
score: ctx.total_score,
|
score: ctx.total_score,
|
||||||
time_ms: Math.floor(ctx.total_time_usage_ms * 1000000) / 1000000,
|
time: Math.floor(ctx.total_time_usage_ms * 1000000) / 1000000,
|
||||||
memory_kb: ctx.total_memory_usage_kb,
|
memory: ctx.total_memory_usage_kb,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -5,10 +5,10 @@ import { getConfig } from '../config';
|
|||||||
import { runPiped } from '../sandbox';
|
import { runPiped } from '../sandbox';
|
||||||
import signals from '../signals';
|
import signals from '../signals';
|
||||||
import { parse } from '../testlib';
|
import { parse } from '../testlib';
|
||||||
import { findFileSync, parseFilename } from '../utils';
|
|
||||||
import {
|
import {
|
||||||
Case, Context, ContextSubTask, SubTask,
|
findFileSync, NormalizedCase, NormalizedSubtask, parseFilename,
|
||||||
} from './interface';
|
} from '../utils';
|
||||||
|
import { Context, ContextSubTask } from './interface';
|
||||||
|
|
||||||
const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h');
|
const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h');
|
||||||
const Score = {
|
const Score = {
|
||||||
@@ -17,7 +17,7 @@ const Score = {
|
|||||||
min: Math.min,
|
min: Math.min,
|
||||||
};
|
};
|
||||||
|
|
||||||
function judgeCase(c: Case) {
|
function judgeCase(c: NormalizedCase) {
|
||||||
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
|
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
|
||||||
ctx.executeInteractor.copyIn.in = c.input ? { src: c.input } : { content: '' };
|
ctx.executeInteractor.copyIn.in = c.input ? { src: c.input } : { content: '' };
|
||||||
ctx.executeInteractor.copyIn.out = c.output ? { src: c.output } : { content: '' };
|
ctx.executeInteractor.copyIn.out = c.output ? { src: c.output } : { content: '' };
|
||||||
@@ -50,7 +50,7 @@ function judgeCase(c: Case) {
|
|||||||
if (code < 32) message = signals[code];
|
if (code < 32) message = signals[code];
|
||||||
else message = { message: 'Your program returned {0}.', params: [code] };
|
else message = { message: 'Your program returned {0}.', params: [code] };
|
||||||
} else {
|
} else {
|
||||||
const result = parse(resInteractor.stderr, ctxSubtask.subtask.score);
|
const result = parse(resInteractor.stderr, c.score);
|
||||||
status = result.status;
|
status = result.status;
|
||||||
score = result.score;
|
score = result.score;
|
||||||
message = result.message;
|
message = result.message;
|
||||||
@@ -63,10 +63,11 @@ function judgeCase(c: Case) {
|
|||||||
ctx.next({
|
ctx.next({
|
||||||
status: STATUS.STATUS_JUDGING,
|
status: STATUS.STATUS_JUDGING,
|
||||||
case: {
|
case: {
|
||||||
|
subtaskId: ctxSubtask.subtask.id,
|
||||||
status,
|
status,
|
||||||
score,
|
score,
|
||||||
time_ms: time_usage_ms,
|
time: time_usage_ms,
|
||||||
memory_kb: memory_usage_kb,
|
memory: memory_usage_kb,
|
||||||
message,
|
message,
|
||||||
},
|
},
|
||||||
progress: Math.floor((c.id * 100) / ctx.config.count),
|
progress: Math.floor((c.id * 100) / ctx.config.count),
|
||||||
@@ -74,7 +75,7 @@ function judgeCase(c: Case) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function judgeSubtask(subtask: SubTask) {
|
function judgeSubtask(subtask: NormalizedSubtask) {
|
||||||
return async (ctx: Context) => {
|
return async (ctx: Context) => {
|
||||||
subtask.type = subtask.type || 'min';
|
subtask.type = subtask.type || 'min';
|
||||||
const ctxSubtask = {
|
const ctxSubtask = {
|
||||||
@@ -134,7 +135,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
|
|||||||
ctx.end({
|
ctx.end({
|
||||||
status: ctx.total_status,
|
status: ctx.total_status,
|
||||||
score: ctx.total_score,
|
score: ctx.total_score,
|
||||||
time_ms: ctx.total_time_usage_ms,
|
time: ctx.total_time_usage_ms,
|
||||||
memory_kb: ctx.total_memory_usage_kb,
|
memory: ctx.total_memory_usage_kb,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1,31 +1,15 @@
|
|||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
import type { NormalizedSubtask } from '@hydrooj/utils/lib/common';
|
||||||
|
import type { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||||
|
import type { JudgeResultBody } from 'hydrooj';
|
||||||
import { Execute } from '../interface';
|
import { Execute } from '../interface';
|
||||||
import { CopyInFile } from '../sandbox/interface';
|
import { CopyInFile } from '../sandbox/interface';
|
||||||
|
|
||||||
export type Context = JudgeTaskInterface & RuntimeContext;
|
export type Context = JudgeTaskInterface & RuntimeContext;
|
||||||
|
|
||||||
export interface JudgeTaskInterface {
|
export interface JudgeTaskInterface {
|
||||||
next(arg0: {
|
next(data: Partial<JudgeResultBody>, id?: number): void;
|
||||||
compiler_text?: string;
|
end(data: Partial<JudgeResultBody>): void;
|
||||||
status?: number;
|
|
||||||
message?: string;
|
|
||||||
progress?: number;
|
|
||||||
addProgress?: number;
|
|
||||||
case?: {
|
|
||||||
status: number;
|
|
||||||
score?: number;
|
|
||||||
time_ms: number;
|
|
||||||
memory_kb: number;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
}, arg1?: number): void;
|
|
||||||
end(arg0: {
|
|
||||||
status: number;
|
|
||||||
score: number;
|
|
||||||
time_ms: number;
|
|
||||||
memory_kb: number;
|
|
||||||
}): void;
|
|
||||||
getLang: (name: string) => LangConfig;
|
getLang: (name: string) => LangConfig;
|
||||||
|
|
||||||
stat: Record<string, Date>;
|
stat: Record<string, Date>;
|
||||||
@@ -56,22 +40,20 @@ export interface RuntimeContext {
|
|||||||
executeUser?: Execute;
|
executeUser?: Execute;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtraFile = string[];
|
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
type: string;
|
type: string;
|
||||||
subType?: string;
|
subType?: string;
|
||||||
time: string;
|
time: string;
|
||||||
memory: string;
|
memory: string;
|
||||||
|
|
||||||
subtasks?: SubTask[];
|
subtasks?: NormalizedSubtask[];
|
||||||
count?: number;
|
count?: number;
|
||||||
checker_type?: string;
|
checker_type?: string;
|
||||||
detail?: boolean;
|
detail?: boolean;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
|
|
||||||
judge_extra_files?: ExtraFile;
|
judge_extra_files: string[];
|
||||||
user_extra_files?: ExtraFile;
|
user_extra_files: string[];
|
||||||
template?: Record<string, [string, string]>;
|
template?: Record<string, [string, string]>;
|
||||||
|
|
||||||
checker?: string;
|
checker?: string;
|
||||||
@@ -86,23 +68,8 @@ export interface Config {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubTask {
|
|
||||||
time: number;
|
|
||||||
memory: number;
|
|
||||||
type: 'sum' | 'max' | 'min';
|
|
||||||
score: number;
|
|
||||||
cases: Record<string, Case>;
|
|
||||||
if: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContextSubTask {
|
export interface ContextSubTask {
|
||||||
subtask: SubTask;
|
subtask: NormalizedSubtask;
|
||||||
score: number;
|
score: number;
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Case {
|
|
||||||
id: number;
|
|
||||||
input: string;
|
|
||||||
output: string;
|
|
||||||
}
|
|
||||||
@@ -29,11 +29,11 @@ export async function judge({
|
|||||||
status: totalStatus,
|
status: totalStatus,
|
||||||
progress: (100 * (+i + 1)) / config.outputs.length,
|
progress: (100 * (+i + 1)) / config.outputs.length,
|
||||||
case: {
|
case: {
|
||||||
status, score, time_ms: 0, memory_kb: 0, message: '',
|
status, score, time: 0, memory: 0, message: '',
|
||||||
},
|
},
|
||||||
}, +i + 1);
|
}, +i + 1);
|
||||||
}
|
}
|
||||||
return end({
|
return end({
|
||||||
status: totalStatus, score: totalScore, time_ms: 0, memory_kb: 0,
|
status: totalStatus, score: totalScore, time: 0, memory: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,8 @@ import { Context } from './interface';
|
|||||||
const failure = (status: number, message?: string) => ({
|
const failure = (status: number, message?: string) => ({
|
||||||
status,
|
status,
|
||||||
score: 0,
|
score: 0,
|
||||||
time_ms: 0,
|
time: 0,
|
||||||
memory_kb: 0,
|
memory: 0,
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,9 +88,11 @@ export const judge = async (ctx: Context) => {
|
|||||||
ctx.next({
|
ctx.next({
|
||||||
status,
|
status,
|
||||||
case: {
|
case: {
|
||||||
|
subtaskId: 0,
|
||||||
status,
|
status,
|
||||||
time_ms: time_usage_ms,
|
score: 100,
|
||||||
memory_kb: memory_usage_kb,
|
time: time_usage_ms,
|
||||||
|
memory: memory_usage_kb,
|
||||||
message: message.join('\n').substring(0, 102400),
|
message: message.join('\n').substring(0, 102400),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -115,7 +117,7 @@ export const judge = async (ctx: Context) => {
|
|||||||
memory: 256,
|
memory: 256,
|
||||||
});
|
});
|
||||||
const out = r.stdout.toString();
|
const out = r.stdout.toString();
|
||||||
if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) });
|
if (out.length) ctx.next({ compilerText: out.substring(0, 1024) });
|
||||||
if (process.env.DEV) console.log(r);
|
if (process.env.DEV) console.log(r);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.info('Failed to run analysis');
|
logger.info('Failed to run analysis');
|
||||||
@@ -128,7 +130,7 @@ export const judge = async (ctx: Context) => {
|
|||||||
ctx.end({
|
ctx.end({
|
||||||
status,
|
status,
|
||||||
score: status === STATUS.STATUS_ACCEPTED ? 100 : 0,
|
score: status === STATUS.STATUS_ACCEPTED ? 100 : 0,
|
||||||
time_ms: Math.floor(time_usage_ms * 1000000) / 1000000,
|
time: Math.floor(time_usage_ms * 1000000) / 1000000,
|
||||||
memory_kb: memory_usage_kb,
|
memory: memory_usage_kb,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -5,10 +5,8 @@ import { check, compileChecker } from '../check';
|
|||||||
import { getConfig } from '../config';
|
import { getConfig } from '../config';
|
||||||
import { FormatError } from '../error';
|
import { FormatError } from '../error';
|
||||||
import { del, run } from '../sandbox';
|
import { del, run } from '../sandbox';
|
||||||
import { parseFilename } from '../utils';
|
import { NormalizedCase, NormalizedSubtask, parseFilename } from '../utils';
|
||||||
import {
|
import { Context, ContextSubTask } from './interface';
|
||||||
Case, Context, ContextSubTask, SubTask,
|
|
||||||
} from './interface';
|
|
||||||
|
|
||||||
const Score = {
|
const Score = {
|
||||||
sum: (a: number, b: number) => (a + b),
|
sum: (a: number, b: number) => (a + b),
|
||||||
@@ -16,18 +14,18 @@ const Score = {
|
|||||||
min: Math.min,
|
min: Math.min,
|
||||||
};
|
};
|
||||||
|
|
||||||
function judgeCase(c: Case, sid: string) {
|
function judgeCase(c: NormalizedCase, sid: string) {
|
||||||
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
|
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
|
||||||
if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min')
|
if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min')
|
||||||
|| (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score)
|
|| (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score)
|
||||||
|| ((ctxSubtask.subtask.if || []).filter((i: string) => ctx.failed[i]).length)
|
|| ((ctxSubtask.subtask.if || []).filter((i) => ctx.failed[i]).length)
|
||||||
) {
|
) {
|
||||||
ctx.next({
|
ctx.next({
|
||||||
case: {
|
case: {
|
||||||
status: STATUS.STATUS_CANCELED,
|
status: STATUS.STATUS_CANCELED,
|
||||||
score: 0,
|
score: 0,
|
||||||
time_ms: 0,
|
time: 0,
|
||||||
memory_kb: 0,
|
memory: 0,
|
||||||
message: '',
|
message: '',
|
||||||
},
|
},
|
||||||
addProgress: 100 / ctx.config.count,
|
addProgress: 100 / ctx.config.count,
|
||||||
@@ -74,7 +72,7 @@ function judgeCase(c: Case, sid: string) {
|
|||||||
user_stdout: file,
|
user_stdout: file,
|
||||||
user_stderr: { content: '' },
|
user_stderr: { content: '' },
|
||||||
checker_type: ctx.config.checker_type,
|
checker_type: ctx.config.checker_type,
|
||||||
score: ctxSubtask.subtask.score,
|
score: c.score,
|
||||||
detail: ctx.config.detail ?? true,
|
detail: ctx.config.detail ?? true,
|
||||||
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
|
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
|
||||||
});
|
});
|
||||||
@@ -87,8 +85,8 @@ function judgeCase(c: Case, sid: string) {
|
|||||||
case: {
|
case: {
|
||||||
status,
|
status,
|
||||||
score: 0,
|
score: 0,
|
||||||
time_ms: 0,
|
time: 0,
|
||||||
memory_kb: 0,
|
memory: 0,
|
||||||
message,
|
message,
|
||||||
},
|
},
|
||||||
addProgress: 100 / ctx.config.count,
|
addProgress: 100 / ctx.config.count,
|
||||||
@@ -96,7 +94,7 @@ function judgeCase(c: Case, sid: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function judgeSubtask(subtask: SubTask, sid: string) {
|
function judgeSubtask(subtask: NormalizedSubtask, sid: string) {
|
||||||
return async (ctx: Context) => {
|
return async (ctx: Context) => {
|
||||||
subtask.type = subtask.type || 'min';
|
subtask.type = subtask.type || 'min';
|
||||||
const ctxSubtask = {
|
const ctxSubtask = {
|
||||||
@@ -124,9 +122,6 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
|
|||||||
if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.');
|
if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.');
|
||||||
startPromise.then(() => ctx.next({ status: STATUS.STATUS_COMPILING }));
|
startPromise.then(() => ctx.next({ status: STATUS.STATUS_COMPILING }));
|
||||||
ctx.checker = await (() => {
|
ctx.checker = await (() => {
|
||||||
if (['default', 'strict'].includes(ctx.config.checker_type || 'default')) {
|
|
||||||
return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
|
|
||||||
}
|
|
||||||
const copyIn = { user_code: ctx.code };
|
const copyIn = { user_code: ctx.code };
|
||||||
for (const file of ctx.config.judge_extra_files) {
|
for (const file of ctx.config.judge_extra_files) {
|
||||||
copyIn[parseFilename(file)] = { src: file };
|
copyIn[parseFilename(file)] = { src: file };
|
||||||
@@ -160,7 +155,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
|
|||||||
ctx.end({
|
ctx.end({
|
||||||
status: ctx.total_status,
|
status: ctx.total_status,
|
||||||
score: ctx.total_score,
|
score: ctx.total_score,
|
||||||
time_ms: 0,
|
time: 0,
|
||||||
memory_kb: 0,
|
memory: 0,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1,29 +1,9 @@
|
|||||||
import { format, inspect, InspectOptions } from 'util';
|
import { format, inspect, InspectOptions } from 'util';
|
||||||
import cac from 'cac';
|
import cac from 'cac';
|
||||||
|
import { Time } from '@hydrooj/utils/lib/utils';
|
||||||
|
|
||||||
const argv = cac().parse();
|
const argv = cac().parse();
|
||||||
|
|
||||||
namespace Time {
|
|
||||||
export const second = 1000;
|
|
||||||
export const minute = second * 60;
|
|
||||||
export const hour = minute * 60;
|
|
||||||
export const day = hour * 24;
|
|
||||||
export const week = day * 7;
|
|
||||||
export function formatTimeShort(ms: number) {
|
|
||||||
const abs = Math.abs(ms);
|
|
||||||
if (abs >= day - hour / 2) {
|
|
||||||
return `${Math.round(ms / day)}d`;
|
|
||||||
} if (abs >= hour - minute / 2) {
|
|
||||||
return `${Math.round(ms / hour)}h`;
|
|
||||||
} if (abs >= minute - second / 2) {
|
|
||||||
return `${Math.round(ms / minute)}m`;
|
|
||||||
} if (abs >= second) {
|
|
||||||
return `${Math.round(ms / second)}s`;
|
|
||||||
}
|
|
||||||
return `${ms}ms`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62,
|
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62,
|
||||||
63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113,
|
63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import cac from 'cac';
|
import cac from 'cac';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
import PQueue from 'p-queue';
|
||||||
import { ParseEntry } from 'shell-quote';
|
import { ParseEntry } from 'shell-quote';
|
||||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
@@ -191,3 +192,9 @@ export async function run(execute: string, params?: Parameter): Promise<SandboxA
|
|||||||
}
|
}
|
||||||
return await adaptResult(result, params);
|
return await adaptResult(result, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queue = new PQueue({ concurrency: getConfig('parallelism') });
|
||||||
|
|
||||||
|
export function runQueued(execute: string, params?: Parameter, priority = 0) {
|
||||||
|
return queue.add(() => run(execute, params), { priority });
|
||||||
|
}
|
||||||
@@ -7,9 +7,10 @@ import fs from 'fs-extra';
|
|||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
import { ObjectID } from 'mongodb';
|
import { ObjectID } from 'mongodb';
|
||||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||||
|
import type { JudgeResultBody } from 'hydrooj';
|
||||||
import { Logger } from 'hydrooj/src/logger';
|
import { Logger } from 'hydrooj/src/logger';
|
||||||
import * as monitor from 'hydrooj/src/service/monitor';
|
import * as monitor from 'hydrooj/src/service/monitor';
|
||||||
import readCases from './cases';
|
import readCases, { processTestdata } from './cases';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import { CompileError, FormatError, SystemError } from './error';
|
import { CompileError, FormatError, SystemError } from './error';
|
||||||
import { CopyInFile } from './sandbox/interface';
|
import { CopyInFile } from './sandbox/interface';
|
||||||
@@ -22,7 +23,6 @@ declare module 'hydrooj/src/interface' {
|
|||||||
'hydrojudge.cache_dir': string;
|
'hydrojudge.cache_dir': string;
|
||||||
'hydrojudge.tmp_dir': string;
|
'hydrojudge.tmp_dir': string;
|
||||||
'hydrojudge.tmpfs_size': string;
|
'hydrojudge.tmpfs_size': string;
|
||||||
'hydrojudge.retry_delay_sec': number;
|
|
||||||
'hydrojudge.sandbox_host': string;
|
'hydrojudge.sandbox_host': string;
|
||||||
'hydrojudge.memoryMax': string;
|
'hydrojudge.memoryMax': string;
|
||||||
'hydrojudge.testcases_max': number;
|
'hydrojudge.testcases_max': number;
|
||||||
@@ -53,28 +53,6 @@ async function postInit() {
|
|||||||
monitor.updateJudge({ mid, ...inf });
|
monitor.updateJudge({ mid, ...inf });
|
||||||
}, 1200000);
|
}, 1200000);
|
||||||
|
|
||||||
async function processData(folder: string) {
|
|
||||||
let files = await fs.readdir(folder);
|
|
||||||
let ini = false;
|
|
||||||
for (const i of files) {
|
|
||||||
if (i.toLowerCase() === 'config.ini') {
|
|
||||||
ini = true;
|
|
||||||
await fs.rename(`${folder}/${i}`, `${folder}/config.ini`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ini) {
|
|
||||||
for (const i of files) {
|
|
||||||
if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`);
|
|
||||||
else if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`);
|
|
||||||
}
|
|
||||||
files = await fs.readdir(`${folder}/input`);
|
|
||||||
for (const i of files) await fs.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`);
|
|
||||||
files = await fs.readdir(`${folder}/output`);
|
|
||||||
for (const i of files) await fs.rename(`${folder}/output/${i}`, `${folder}/output/${i.toLowerCase()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchCodeFile(name: string) {
|
async function fetchCodeFile(name: string) {
|
||||||
const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_'));
|
const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_'));
|
||||||
await storage.get(`submission/${name}`, target);
|
await storage.get(`submission/${name}`, target);
|
||||||
@@ -103,39 +81,23 @@ async function postInit() {
|
|||||||
}
|
}
|
||||||
fs.writeFileSync(path.join(filePath, 'etags'), JSON.stringify(version));
|
fs.writeFileSync(path.join(filePath, 'etags'), JSON.stringify(version));
|
||||||
fs.writeFileSync(path.join(filePath, 'lastUsage'), new Date().getTime().toString());
|
fs.writeFileSync(path.join(filePath, 'lastUsage'), new Date().getTime().toString());
|
||||||
await processData(filePath).catch(noop);
|
await processTestdata(filePath);
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNext(that) {
|
function getNext(that) {
|
||||||
return (data, id = 0) => {
|
return (data: Partial<JudgeResultBody>, id = 0) => {
|
||||||
logger.debug('Next: %d %o', id, data);
|
logger.debug('Next: %d %o', id, data);
|
||||||
data.rid = new ObjectID(that.rid);
|
data.rid = new ObjectID(that.rid);
|
||||||
if (data.time_ms) data.time = data.time_ms;
|
if (data.case) data.case.id ||= id;
|
||||||
if (data.memory_kb) data.memory = data.memory_kb;
|
|
||||||
if (data.compiler_text) data.compilerText = data.compiler_text;
|
|
||||||
delete data.time_ms;
|
|
||||||
delete data.memory_kb;
|
|
||||||
delete data.compiler_text;
|
|
||||||
if (data.case) {
|
|
||||||
data.case = {
|
|
||||||
id,
|
|
||||||
status: data.case.status,
|
|
||||||
time: data.case.time_ms || data.case.time,
|
|
||||||
memory: data.case.memory_kb || data.case.memory,
|
|
||||||
message: data.case.message || data.case.judgeText || '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
_judge.next(data);
|
_judge.next(data);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEnd(rid: string) {
|
function getEnd(rid: string) {
|
||||||
return (data) => {
|
return (data: Partial<JudgeResultBody>) => {
|
||||||
data.key = 'end';
|
data.key = 'end';
|
||||||
data.rid = new ObjectID(rid);
|
data.rid = new ObjectID(rid);
|
||||||
data.time = data.time_ms ?? data.time;
|
|
||||||
data.memory = data.memory_kb ?? data.memory;
|
|
||||||
logger.info('End: status=%d score=%d time=%dms memory=%dkb', data.status, data.score, data.time, data.memory);
|
logger.info('End: status=%d score=%d time=%dms memory=%dkb', data.status, data.score, data.time, data.memory);
|
||||||
_judge.end(data);
|
_judge.end(data);
|
||||||
};
|
};
|
||||||
@@ -160,8 +122,8 @@ async function postInit() {
|
|||||||
config: any;
|
config: any;
|
||||||
env: any;
|
env: any;
|
||||||
input?: string;
|
input?: string;
|
||||||
next: (data: any, id?: number) => void;
|
next: ReturnType<typeof getNext>;
|
||||||
end: (data: any) => void;
|
end: ReturnType<typeof getEnd>;
|
||||||
tmpdir: string;
|
tmpdir: string;
|
||||||
clean: (() => Promise<any>)[];
|
clean: (() => Promise<any>)[];
|
||||||
folder: string;
|
folder: string;
|
||||||
@@ -204,20 +166,20 @@ async function postInit() {
|
|||||||
await this.submission();
|
await this.submission();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof CompileError) {
|
if (e instanceof CompileError) {
|
||||||
this.next({ compiler_text: compilerText(e.stdout, e.stderr) });
|
this.next({ compilerText: compilerText(e.stdout, e.stderr) });
|
||||||
this.end({
|
this.end({
|
||||||
status: STATUS.STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0,
|
status: STATUS.STATUS_COMPILE_ERROR, score: 0, time: 0, memory: 0,
|
||||||
});
|
});
|
||||||
} else if (e instanceof FormatError) {
|
} else if (e instanceof FormatError) {
|
||||||
this.next({ message: { message: e.message, params: e.params } });
|
this.next({ message: { message: e.message, params: e.params } });
|
||||||
this.end({
|
this.end({
|
||||||
status: STATUS.STATUS_FORMAT_ERROR, score: 0, time_ms: 0, memory_kb: 0,
|
status: STATUS.STATUS_FORMAT_ERROR, score: 0, time: 0, memory: 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
this.next({ message: { message: e.message, params: e.params, ...process.env.DEV ? { stack: e.stack } : {} } });
|
this.next({ message: { message: e.message, params: e.params, ...process.env.DEV ? { stack: e.stack } : {} } });
|
||||||
this.end({
|
this.end({
|
||||||
status: STATUS.STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0,
|
status: STATUS.STATUS_SYSTEM_ERROR, score: 0, time: 0, memory: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// TODO this should be moved into web
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
@@ -30,6 +31,8 @@ int main(){
|
|||||||
type: 'default',
|
type: 'default',
|
||||||
time: '3s',
|
time: '3s',
|
||||||
memory: '256m',
|
memory: '256m',
|
||||||
|
user_extra_files: [],
|
||||||
|
judge_extra_files: [],
|
||||||
},
|
},
|
||||||
stat: {},
|
stat: {},
|
||||||
clean: [],
|
clean: [],
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function parse(output: string, fullscore: number) {
|
|||||||
if (p === 1) {
|
if (p === 1) {
|
||||||
status = STATUS.STATUS_ACCEPTED;
|
status = STATUS.STATUS_ACCEPTED;
|
||||||
score = fullscore;
|
score = fullscore;
|
||||||
const base = output.split('points ')[1] || '';
|
const base = output.replace('points ', '') || '';
|
||||||
message = base.substring(base.indexOf(' '), 1024);
|
message = base.substring(base.indexOf(' '), 1024);
|
||||||
} else score = Math.floor(fullscore * p);
|
} else score = Math.floor(fullscore * p);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "3.12.1",
|
"version": "3.13.8",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "src/loader",
|
"main": "src/loader",
|
||||||
"module": "src/loader",
|
"module": "src/loader",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"preferUnplugged": true,
|
"preferUnplugged": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphql-tools/schema": "^8.3.6",
|
"@graphql-tools/schema": "^8.3.11",
|
||||||
"@hydrooj/utils": "workspace:*",
|
"@hydrooj/utils": "workspace:*",
|
||||||
"adm-zip": "0.5.5",
|
"adm-zip": "0.5.5",
|
||||||
"ajv": "^8.11.0",
|
"ajv": "^8.11.0",
|
||||||
@@ -23,32 +23,32 @@
|
|||||||
"emoji-regex": "^10.1.0",
|
"emoji-regex": "^10.1.0",
|
||||||
"emojis-list": "2.1.0",
|
"emojis-list": "2.1.0",
|
||||||
"esbuild": "0.14.3",
|
"esbuild": "0.14.3",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.1.0",
|
||||||
"graphql": "^16.3.0",
|
"graphql": "^16.5.0",
|
||||||
"graphql-scalars": "1.17.0",
|
"graphql-scalars": "1.17.0",
|
||||||
"isbinaryfile": "^5.0.0",
|
"isbinaryfile": "^5.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"koa": "^2.13.4",
|
"koa": "^2.13.4",
|
||||||
"koa-body": "^4.2.0",
|
"koa-body": "^5.0.0",
|
||||||
"koa-compress": "^5.1.0",
|
"koa-compress": "^5.1.0",
|
||||||
"koa-proxies": "^0.12.2",
|
"koa-proxies": "^0.12.2",
|
||||||
"koa-router": "^10.1.1",
|
"koa-router": "^10.1.1",
|
||||||
"koa-static-cache": "^5.1.4",
|
"koa-static-cache": "^5.1.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lru-cache": "7.7.3",
|
"lru-cache": "7.9.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"minio": "7.0.25",
|
"minio": "7.0.25",
|
||||||
"moment-timezone": "^0.5.34",
|
"moment-timezone": "^0.5.34",
|
||||||
"mongodb": "^3.7.3",
|
"mongodb": "^3.7.3",
|
||||||
"nanoid": "^3.3.2",
|
"nanoid": "^3.3.4",
|
||||||
"nodemailer": "^6.7.3",
|
"nodemailer": "^6.7.5",
|
||||||
"notp": "^2.0.3",
|
"notp": "^2.0.3",
|
||||||
"p-queue": "^7.2.0",
|
"p-queue": "^7.2.0",
|
||||||
"require-resolve-hook": "^1.1.0",
|
"require-resolve-hook": "^1.1.0",
|
||||||
"semver": "^7.3.6",
|
"semver": "^7.3.7",
|
||||||
"serialize-javascript": "^6.0.0",
|
"serialize-javascript": "^6.0.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"superagent": "^7.1.2",
|
"superagent": "^7.1.3",
|
||||||
"thirty-two": "^1.0.2",
|
"thirty-two": "^1.0.2",
|
||||||
"tx2": "^1.0.5"
|
"tx2": "^1.0.5"
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"@types/koa-compress": "^4.0.3",
|
"@types/koa-compress": "^4.0.3",
|
||||||
"@types/koa-router": "^7.4.4",
|
"@types/koa-router": "^7.4.4",
|
||||||
"@types/koa-static-cache": "^5.1.1",
|
"@types/koa-static-cache": "^5.1.1",
|
||||||
"@types/lodash": "^4.14.181",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/lru-cache": "^7.6.1",
|
"@types/lru-cache": "^7.6.1",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/minio": "^7.0.12",
|
"@types/minio": "^7.0.12",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"@types/serialize-javascript": "^5.0.2",
|
"@types/serialize-javascript": "^5.0.2",
|
||||||
"@types/source-map-support": "^0.5.4",
|
"@types/source-map-support": "^0.5.4",
|
||||||
"@types/superagent": "^4.1.15",
|
"@types/superagent": "^4.1.15",
|
||||||
"moment": "^2.29.2",
|
"moment": "^2.29.3",
|
||||||
"pm2": "^5.2.0"
|
"pm2": "^5.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ export async function service(pending: string[], fail: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const key in global.Hydro.service) {
|
for (const key in global.Hydro.service) {
|
||||||
|
if (key === 'server') continue;
|
||||||
const srv = global.Hydro.service[key];
|
const srv = global.Hydro.service[key];
|
||||||
if (!srv.started && srv.start) await srv.start();
|
if (!srv.started && srv.start) await srv.start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Handler, Route } from '../service/server';
|
||||||
|
|
||||||
|
class ProblemCategoryCompatHandler extends Handler {
|
||||||
|
async get({ category }) {
|
||||||
|
this.response.redirect = this.url('problem_main', { query: { q: `category:${category}` } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.Hydro.handler.compat = () => {
|
||||||
|
Route('problem_category_compat', '/p/category/:category', ProblemCategoryCompatHandler);
|
||||||
|
};
|
||||||
@@ -240,7 +240,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
|
|||||||
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
|
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
|
||||||
const msg = JSON.stringify({
|
const msg = JSON.stringify({
|
||||||
message: 'User {0} mentioned you in {1:link}',
|
message: 'User {0} mentioned you in {1:link}',
|
||||||
params: [this.user.uname, this.request.path],
|
params: [this.user.uname, `/d/${domainId}${this.request.path}`],
|
||||||
});
|
});
|
||||||
for (const uid of uids) {
|
for (const uid of uids) {
|
||||||
message.send(1, uid, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
|
message.send(1, uid, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
|
||||||
@@ -259,7 +259,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
|
|||||||
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
|
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
|
||||||
const msg = JSON.stringify({
|
const msg = JSON.stringify({
|
||||||
message: 'User {0} mentioned you in {1:link}',
|
message: 'User {0} mentioned you in {1:link}',
|
||||||
params: [this.user.uname, this.request.path],
|
params: [this.user.uname, `/d/${domainId}${this.request.path}`],
|
||||||
});
|
});
|
||||||
for (const uid of uids) {
|
for (const uid of uids) {
|
||||||
message.send(1, uid, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
|
message.send(1, uid, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ProblemImportHydroHandler extends Handler {
|
|||||||
if (keepUser) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
|
if (keepUser) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
|
||||||
if (!this.request.files.file) throw new ValidationError('file');
|
if (!this.request.files.file) throw new ValidationError('file');
|
||||||
const tmpdir = path.join(os.tmpdir(), 'hydro', `${Math.random()}.import`);
|
const tmpdir = path.join(os.tmpdir(), 'hydro', `${Math.random()}.import`);
|
||||||
const zip = new AdmZip(this.request.files.file.path);
|
const zip = new AdmZip(this.request.files.file.filepath);
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
zip.extractAllToAsync(tmpdir, true, (err) => {
|
zip.extractAllToAsync(tmpdir, true, (err) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ import './domain';
|
|||||||
import './status';
|
import './status';
|
||||||
import './api';
|
import './api';
|
||||||
import './blog';
|
import './blog';
|
||||||
|
import './compat';
|
||||||
@@ -41,19 +41,19 @@ export async function postJudge(rdoc: RecordDoc) {
|
|||||||
await bus.serial('record/judge', rdoc, updated);
|
await bus.serial('record/judge', rdoc, updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function next(body: JudgeResultBody) {
|
export async function next(body: Partial<JudgeResultBody>) {
|
||||||
body.rid = new ObjectID(body.rid);
|
body.rid = new ObjectID(body.rid);
|
||||||
let rdoc = await record.get(body.rid);
|
let rdoc = await record.get(body.rid);
|
||||||
if (!rdoc) return;
|
if (!rdoc) return;
|
||||||
const $set: Partial<RecordDoc> = {};
|
const $set: Partial<RecordDoc> = {};
|
||||||
const $push: any = {};
|
const $push: any = {};
|
||||||
if (body.case) {
|
if (body.case) {
|
||||||
const c: TestCase = {
|
const c: Required<TestCase> = {
|
||||||
|
...body.case,
|
||||||
id: body.case.id || 0,
|
id: body.case.id || 0,
|
||||||
memory: body.case.memory,
|
subtaskId: body.case.subtaskId || 0,
|
||||||
time: body.case.time,
|
score: body.case.score || 0,
|
||||||
message: body.case.message || '',
|
message: body.case.message || '',
|
||||||
status: body.case.status,
|
|
||||||
};
|
};
|
||||||
rdoc.testCases.push(c);
|
rdoc.testCases.push(c);
|
||||||
$push.testCases = c;
|
$push.testCases = c;
|
||||||
@@ -75,7 +75,7 @@ export async function next(body: JudgeResultBody) {
|
|||||||
bus.broadcast('record/change', rdoc!, $set, $push);
|
bus.broadcast('record/change', rdoc!, $set, $push);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function end(body: JudgeResultBody) {
|
export async function end(body: Partial<JudgeResultBody>) {
|
||||||
if (body.rid) body.rid = new ObjectID(body.rid);
|
if (body.rid) body.rid = new ObjectID(body.rid);
|
||||||
let rdoc = await record.get(body.rid);
|
let rdoc = await record.get(body.rid);
|
||||||
if (!rdoc) return;
|
if (!rdoc) return;
|
||||||
|
|||||||
@@ -49,16 +49,17 @@ export class FilesHandler extends Handler {
|
|||||||
if ((this.user._files?.length || 0) >= system.get('limit.user_files')) {
|
if ((this.user._files?.length || 0) >= system.get('limit.user_files')) {
|
||||||
throw new ForbiddenError('File limit exceeded.');
|
throw new ForbiddenError('File limit exceeded.');
|
||||||
}
|
}
|
||||||
if (!this.request.files.file) throw new ValidationError('file');
|
const file = this.request.files?.file;
|
||||||
const f = statSync(this.request.files.file.path);
|
if (!file) throw new ValidationError('file');
|
||||||
|
const f = statSync(file.filepath);
|
||||||
const size = Math.sum((this.user._files || []).map((i) => i.size)) + f.size;
|
const size = Math.sum((this.user._files || []).map((i) => i.size)) + f.size;
|
||||||
if (size >= system.get('limit.user_files_size')) {
|
if (size >= system.get('limit.user_files_size')) {
|
||||||
throw new ForbiddenError('File size limit exceeded.');
|
throw new ForbiddenError('File size limit exceeded.');
|
||||||
}
|
}
|
||||||
if (!filename) filename = this.request.files.file.name || String.random(16);
|
if (!filename) filename = file.originalFilename || String.random(16);
|
||||||
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
|
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
|
||||||
if (this.user._files.filter((i) => i.name === filename).length) throw new BadRequestError('file exists');
|
if (this.user._files.filter((i) => i.name === filename).length) throw new BadRequestError('file exists');
|
||||||
await storage.put(`user/${this.user._id}/${filename}`, this.request.files.file.path, this.user._id);
|
await storage.put(`user/${this.user._id}/${filename}`, file.filepath, this.user._id);
|
||||||
const meta = await storage.getMeta(`user/${this.user._id}/${filename}`);
|
const meta = await storage.getMeta(`user/${this.user._id}/${filename}`);
|
||||||
const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
|
||||||
if (!meta) throw new Error('Upload failed');
|
if (!meta) throw new Error('Upload failed');
|
||||||
@@ -90,7 +91,7 @@ export class FSDownloadHandler extends Handler {
|
|||||||
@param('noDisposition', Types.Boolean)
|
@param('noDisposition', Types.Boolean)
|
||||||
async get(domainId: string, uid: number, filename: string, noDisposition = false) {
|
async get(domainId: string, uid: number, filename: string, noDisposition = false) {
|
||||||
const targetUser = await user.getById('system', uid);
|
const targetUser = await user.getById('system', uid);
|
||||||
if (!targetUser.hasPriv(PRIV.PRIV_CREATE_FILE)) throw new ForbiddenError('Access denied');
|
if (this.user._id !== uid && !targetUser.hasPriv(PRIV.PRIV_CREATE_FILE)) throw new ForbiddenError('Access denied');
|
||||||
this.response.addHeader('Cache-Control', 'public');
|
this.response.addHeader('Cache-Control', 'public');
|
||||||
const target = `user/${uid}/${filename}`;
|
const target = `user/${uid}/${filename}`;
|
||||||
const file = await storage.getMeta(target);
|
const file = await storage.getMeta(target);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import AdmZip from 'adm-zip';
|
import AdmZip from 'adm-zip';
|
||||||
import { readFile, statSync } from 'fs-extra';
|
import { readFile, statSync } from 'fs-extra';
|
||||||
import { isBinaryFile } from 'isbinaryfile';
|
import { isBinaryFile } from 'isbinaryfile';
|
||||||
import { intersection, isSafeInteger } from 'lodash';
|
import { flattenDeep, intersection, isSafeInteger } from 'lodash';
|
||||||
import { FilterQuery, ObjectID } from 'mongodb';
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { sortFiles } from '@hydrooj/utils/lib/utils';
|
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
|
||||||
import {
|
import {
|
||||||
BadRequestError, ContestNotAttendedError, ContestNotEndedError,
|
BadRequestError, ContestNotAttendedError, ContestNotEndedError,
|
||||||
ContestNotFoundError, ContestNotLiveError,
|
ContestNotFoundError, ContestNotLiveError,
|
||||||
@@ -125,7 +125,7 @@ export class ProblemHandler extends Handler {
|
|||||||
page, ppcount, pcount, pdocs, psdict, qs,
|
page, ppcount, pcount, pdocs, psdict, qs,
|
||||||
}),
|
}),
|
||||||
this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation }),
|
this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation }),
|
||||||
this.renderHTML('partials/problem_lucky.html', { category }),
|
this.renderHTML('partials/problem_lucky.html', { qs }),
|
||||||
])).map((i) => ({ html: i })),
|
])).map((i) => ({ html: i })),
|
||||||
raw: {
|
raw: {
|
||||||
page, pcount, ppcount, pdocs, psdict, category,
|
page, pcount, ppcount, pdocs, psdict, category,
|
||||||
@@ -138,8 +138,7 @@ export class ProblemHandler extends Handler {
|
|||||||
export class ProblemMainHandler extends ProblemHandler {
|
export class ProblemMainHandler extends ProblemHandler {
|
||||||
@param('page', Types.PositiveInt, true)
|
@param('page', Types.PositiveInt, true)
|
||||||
@param('q', Types.Content, true)
|
@param('q', Types.Content, true)
|
||||||
@param('category', Types.Name, true, null, parseCategory)
|
async get(domainId: string, page = 1, q = '') {
|
||||||
async get(domainId: string, page = 1, q = '', category = []) {
|
|
||||||
this.response.template = 'problem_main.html';
|
this.response.template = 'problem_main.html';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const query = buildQuery(this.user);
|
const query = buildQuery(this.user);
|
||||||
@@ -148,11 +147,15 @@ export class ProblemMainHandler extends ProblemHandler {
|
|||||||
let sort: string[];
|
let sort: string[];
|
||||||
let fail = false;
|
let fail = false;
|
||||||
let pcountRelation = 'eq';
|
let pcountRelation = 'eq';
|
||||||
|
const category = flattenDeep(decodeURIComponent(q).split(' ')
|
||||||
|
.filter((i) => i.startsWith('category:'))
|
||||||
|
.map((i) => i.split('category:')[1]?.split(',')));
|
||||||
|
const text = q.split(' ').filter((i) => !i.startsWith('category:')).join(' ');
|
||||||
if (category.length) query.$and = category.map((tag) => ({ tag }));
|
if (category.length) query.$and = category.map((tag) => ({ tag }));
|
||||||
if (q) category.push(q);
|
if (text) category.push(text);
|
||||||
if (category.length) this.UiContext.extraTitleContent = category.join(',');
|
if (category.length) this.UiContext.extraTitleContent = category.join(',');
|
||||||
let total = 0;
|
let total = 0;
|
||||||
if (q) {
|
if (text) {
|
||||||
if (search) {
|
if (search) {
|
||||||
const result = await search(domainId, q, { skip: (page - 1) * system.get('pagination.problem') });
|
const result = await search(domainId, q, { skip: (page - 1) * system.get('pagination.problem') });
|
||||||
total = result.total;
|
total = result.total;
|
||||||
@@ -166,7 +169,7 @@ export class ProblemMainHandler extends ProblemHandler {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
sort = result.hits;
|
sort = result.hits;
|
||||||
} else query.$text = { $search: q };
|
} else query.$text = { $search: text };
|
||||||
}
|
}
|
||||||
await bus.serial('problem/list', query, this);
|
await bus.serial('problem/list', query, this);
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
@@ -209,7 +212,7 @@ export class ProblemMainHandler extends ProblemHandler {
|
|||||||
pdocs,
|
pdocs,
|
||||||
psdict,
|
psdict,
|
||||||
category: category.join(','),
|
category: category.join(','),
|
||||||
qs: q ? `q=${encodeURIComponent(q)}` : '',
|
qs: q,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,8 +284,11 @@ export class ProblemMainHandler extends ProblemHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProblemRandomHandler extends ProblemHandler {
|
export class ProblemRandomHandler extends ProblemHandler {
|
||||||
@param('category', Types.Name, true, null, parseCategory)
|
@param('q', Types.Content, true)
|
||||||
async get(domainId: string, category: string[] = []) {
|
async get(domainId: string, qs = '') {
|
||||||
|
const category = flattenDeep(decodeURIComponent(qs).split(' ')
|
||||||
|
.filter((i) => i.startsWith('category:'))
|
||||||
|
.map((i) => i.split('category:')[1]?.split(',')));
|
||||||
const q = buildQuery(this.user);
|
const q = buildQuery(this.user);
|
||||||
if (category.length) q.$and = category.map((tag) => ({ tag }));
|
if (category.length) q.$and = category.map((tag) => ({ tag }));
|
||||||
await bus.serial('problem/list', q, this);
|
await bus.serial('problem/list', q, this);
|
||||||
@@ -486,13 +492,13 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
|
|||||||
if (typeof config !== 'object' || config === null) throw new BadRequestError('Incorrect problem config');
|
if (typeof config !== 'object' || config === null) throw new BadRequestError('Incorrect problem config');
|
||||||
const sizeLimit = config.type === 'submit_answer' ? 128 * 1024 * 1024 : 65535;
|
const sizeLimit = config.type === 'submit_answer' ? 128 * 1024 * 1024 : 65535;
|
||||||
if (file.size > sizeLimit) throw new ValidationError('file');
|
if (file.size > sizeLimit) throw new ValidationError('file');
|
||||||
if (file.size > 65535 || await isBinaryFile(file.path, file.size)) {
|
if (file.size > 65535 || await isBinaryFile(file.filepath, file.size)) {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
await storage.put(`submission/${this.user._id}/${id}`, file.path, this.user._id);
|
await storage.put(`submission/${this.user._id}/${id}`, file.filepath, this.user._id);
|
||||||
code = `@@hydro_submission_file@@${this.user._id}/${id}#${file.name}`;
|
code = `@@hydro_submission_file@@${this.user._id}/${id}#${file.originalFilename}`;
|
||||||
} else {
|
} else {
|
||||||
// TODO auto detect & convert encoding
|
// TODO auto detect & convert encoding
|
||||||
code = await readFile(file.path, 'utf-8');
|
code = await readFile(file.filepath, 'utf-8');
|
||||||
}
|
}
|
||||||
} else if (code.startsWith('@@hydro_submission_file@@')) throw new ValidationError('code');
|
} else if (code.startsWith('@@hydro_submission_file@@')) throw new ValidationError('code');
|
||||||
await this.limitRate('add_record', 60, system.get('limit.submission'));
|
await this.limitRate('add_record', 60, system.get('limit.submission'));
|
||||||
@@ -525,6 +531,7 @@ export class ProblemManageHandler extends ProblemDetailHandler {
|
|||||||
|
|
||||||
export class ProblemEditHandler extends ProblemManageHandler {
|
export class ProblemEditHandler extends ProblemManageHandler {
|
||||||
async get() {
|
async get() {
|
||||||
|
this.response.body.additional_file = sortFiles(this.pdoc.additional_file || []);
|
||||||
this.response.template = 'problem_edit.html';
|
this.response.template = 'problem_edit.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +557,23 @@ export class ProblemEditHandler extends ProblemManageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ProblemConfigHandler extends ProblemManageHandler {
|
||||||
|
async get() {
|
||||||
|
if (this.pdoc.reference) throw new ForbiddenError('Cannot edit config of a referenced problem.');
|
||||||
|
this.response.body.testdata = sortFiles(this.pdoc.data || []);
|
||||||
|
const configFile = (this.pdoc.data || []).filter((i) => i.name.toLowerCase() === 'config.yaml');
|
||||||
|
this.response.body.config = '';
|
||||||
|
if (configFile.length > 0) {
|
||||||
|
try {
|
||||||
|
this.response.body.config = (await streamToBuffer(
|
||||||
|
await storage.get(`problem/${this.pdoc.domainId}/${this.pdoc.docId}/testdata/${configFile[0].name}`),
|
||||||
|
)).toString();
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
this.response.template = 'problem_config.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ProblemFilesHandler extends ProblemDetailHandler {
|
export class ProblemFilesHandler extends ProblemDetailHandler {
|
||||||
notUsage = true;
|
notUsage = true;
|
||||||
|
|
||||||
@@ -608,12 +632,12 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
|||||||
async postUploadFile(domainId: string, filename: string, type = 'testdata') {
|
async postUploadFile(domainId: string, filename: string, type = 'testdata') {
|
||||||
if (this.pdoc.reference) throw new ForbiddenError('Cannot edit files of a referenced problem.');
|
if (this.pdoc.reference) throw new ForbiddenError('Cannot edit files of a referenced problem.');
|
||||||
if (!this.request.files.file) throw new ValidationError('file');
|
if (!this.request.files.file) throw new ValidationError('file');
|
||||||
if (!filename) filename = this.request.files.file.name || String.random(16);
|
if (!filename) filename = this.request.files.file.originalFilename || String.random(16);
|
||||||
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
|
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
|
||||||
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||||
const files = [];
|
const files = [];
|
||||||
if (filename.endsWith('.zip')) {
|
if (filename.endsWith('.zip')) {
|
||||||
const zip = new AdmZip(this.request.files.file.path);
|
const zip = new AdmZip(this.request.files.file.filepath);
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.name) continue;
|
if (!entry.name) continue;
|
||||||
@@ -628,8 +652,8 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
|||||||
files.push({
|
files.push({
|
||||||
type,
|
type,
|
||||||
name: filename,
|
name: filename,
|
||||||
size: statSync(this.request.files.file.path).size,
|
size: statSync(this.request.files.file.filepath).size,
|
||||||
data: () => this.request.files.file.path,
|
data: () => this.request.files.file.filepath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!this.user.hasPriv(PRIV.PRIV_EDIT_SYSTEM)) {
|
if (!this.user.hasPriv(PRIV.PRIV_EDIT_SYSTEM)) {
|
||||||
@@ -831,6 +855,7 @@ export class ProblemCreateHandler extends Handler {
|
|||||||
this.response.template = 'problem_edit.html';
|
this.response.template = 'problem_edit.html';
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
page_name: 'problem_create',
|
page_name: 'problem_create',
|
||||||
|
additional_file: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,11 +912,11 @@ export class ProblemPrefixListHandler extends Handler {
|
|||||||
|
|
||||||
export async function apply() {
|
export async function apply() {
|
||||||
Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_category', '/p/category/:category', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
|
|
||||||
Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
||||||
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
|
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
|
||||||
|
Route('problem_config', '/p/:pid/config', ProblemConfigHandler);
|
||||||
Route('problem_files', '/p/:pid/files', ProblemFilesHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_files', '/p/:pid/files', ProblemFilesHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_file_download', '/p/:pid/file/:filename', ProblemFileDownloadHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_file_download', '/p/:pid/file/:filename', ProblemFileDownloadHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
Route('problem_solution', '/p/:pid/solution', ProblemSolutionHandler, PERM.PERM_VIEW_PROBLEM);
|
Route('problem_solution', '/p/:pid/solution', ProblemSolutionHandler, PERM.PERM_VIEW_PROBLEM);
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class RecordDetailHandler extends Handler {
|
|||||||
time: 0,
|
time: 0,
|
||||||
memory: 0,
|
memory: 0,
|
||||||
testCases: [{
|
testCases: [{
|
||||||
status: 9, score: 0, time: 0, memory: 0, message: 'score canceled',
|
id: 0, subtaskId: 0, status: 9, score: 0, time: 0, memory: 0, message: 'score canceled',
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
const [latest] = await Promise.all([
|
const [latest] = await Promise.all([
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type fs from 'fs';
|
|||||||
import type { Dictionary, NumericDictionary } from 'lodash';
|
import type { Dictionary, NumericDictionary } from 'lodash';
|
||||||
import type { ItemBucketMetadata } from 'minio';
|
import type { ItemBucketMetadata } from 'minio';
|
||||||
import type { Cursor, ObjectID } from 'mongodb';
|
import type { Cursor, ObjectID } from 'mongodb';
|
||||||
import type { Readable, Writable } from 'stream';
|
|
||||||
import type { ProblemDoc } from './model/problem';
|
import type { ProblemDoc } from './model/problem';
|
||||||
|
|
||||||
type document = typeof import('./model/document');
|
type document = typeof import('./model/document');
|
||||||
@@ -127,10 +126,11 @@ export interface FileInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TestCaseConfig {
|
export interface TestCaseConfig {
|
||||||
input: string,
|
input: string;
|
||||||
output: string,
|
output: string;
|
||||||
time?: number,
|
time?: string;
|
||||||
memory?: number,
|
memory?: string;
|
||||||
|
score?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProblemType {
|
export enum ProblemType {
|
||||||
@@ -147,8 +147,8 @@ export enum SubtaskType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SubtaskConfig {
|
export interface SubtaskConfig {
|
||||||
time?: number;
|
time?: string;
|
||||||
memory?: number;
|
memory?: string;
|
||||||
score?: number;
|
score?: number;
|
||||||
if?: number[];
|
if?: number[];
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -207,6 +207,7 @@ export interface SampleContentNode {
|
|||||||
sectionTitle: string,
|
sectionTitle: string,
|
||||||
payload: [string, string],
|
payload: [string, string],
|
||||||
}
|
}
|
||||||
|
// TODO drop contentNode support
|
||||||
export type ContentNode = PlainContentNode | TextContentNode | SampleContentNode;
|
export type ContentNode = PlainContentNode | TextContentNode | SampleContentNode;
|
||||||
export type Content = string | ContentNode[] | Record<string, ContentNode[]>;
|
export type Content = string | ContentNode[] | Record<string, ContentNode[]>;
|
||||||
|
|
||||||
@@ -271,6 +272,8 @@ export interface ProblemStatusDoc extends StatusDoc {
|
|||||||
|
|
||||||
export interface TestCase {
|
export interface TestCase {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
subtaskId?: number;
|
||||||
|
score?: number;
|
||||||
time: number;
|
time: number;
|
||||||
memory: number;
|
memory: number;
|
||||||
status: number;
|
status: number;
|
||||||
@@ -287,9 +290,9 @@ export interface RecordDoc {
|
|||||||
score: number;
|
score: number;
|
||||||
memory: number;
|
memory: number;
|
||||||
time: number;
|
time: number;
|
||||||
judgeTexts: string[];
|
judgeTexts: (string | JudgeMessage)[];
|
||||||
compilerTexts: string[];
|
compilerTexts: string[];
|
||||||
testCases: TestCase[];
|
testCases: Required<TestCase>[];
|
||||||
rejudged: boolean;
|
rejudged: boolean;
|
||||||
source?: string;
|
source?: string;
|
||||||
/** judge uid */
|
/** judge uid */
|
||||||
@@ -496,24 +499,25 @@ export interface Script {
|
|||||||
validate: any,
|
validate: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JudgeMessage {
|
||||||
|
message: string;
|
||||||
|
params?: string[];
|
||||||
|
stack?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface JudgeResultBody {
|
export interface JudgeResultBody {
|
||||||
|
key: string;
|
||||||
domainId: string;
|
domainId: string;
|
||||||
rid: ObjectID;
|
rid: ObjectID;
|
||||||
judger?: number;
|
judger?: number;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
addProgress?: number;
|
addProgress?: number;
|
||||||
case?: {
|
case?: TestCase,
|
||||||
id?: number;
|
|
||||||
status: number;
|
|
||||||
time: number;
|
|
||||||
memory: number;
|
|
||||||
message?: string;
|
|
||||||
},
|
|
||||||
status?: number;
|
status?: number;
|
||||||
score?: number;
|
score?: number;
|
||||||
time?: number;
|
time?: number;
|
||||||
memory?: number;
|
memory?: number;
|
||||||
message?: string;
|
message?: string | JudgeMessage;
|
||||||
compilerText?: string,
|
compilerText?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,19 +530,6 @@ export interface Task {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadStream extends Writable {
|
|
||||||
id: ObjectID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HydroFileSystem {
|
|
||||||
openUploadStream: (filename: string) => UploadStream
|
|
||||||
openDownloadStream: (_id: ObjectID) => Readable
|
|
||||||
// TODO
|
|
||||||
find: (...args: any[]) => any
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PathComponent = [string, string, Dictionary<any>?, boolean?];
|
|
||||||
|
|
||||||
export interface BaseService {
|
export interface BaseService {
|
||||||
started: boolean;
|
started: boolean;
|
||||||
error?: Error | string;
|
error?: Error | string;
|
||||||
@@ -636,7 +627,6 @@ export interface Model {
|
|||||||
export interface Service {
|
export interface Service {
|
||||||
bus: typeof import('./service/bus'),
|
bus: typeof import('./service/bus'),
|
||||||
db: typeof import('./service/db'),
|
db: typeof import('./service/db'),
|
||||||
fs: HydroFileSystem,
|
|
||||||
monitor: typeof import('./service/monitor'),
|
monitor: typeof import('./service/monitor'),
|
||||||
server: typeof import('./service/server'),
|
server: typeof import('./service/server'),
|
||||||
storage: typeof import('./service/storage'),
|
storage: typeof import('./service/storage'),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const _CACHE_INFO = {
|
|||||||
|
|
||||||
function _LOGP(x: number) {
|
function _LOGP(x: number) {
|
||||||
const sqrtPi = 2.506628274631; // Sqrt[Pi]
|
const sqrtPi = 2.506628274631; // Sqrt[Pi]
|
||||||
return (2 * Math.exp(-1.0 * (Math.log(x) ** 2) * 2)) / x / sqrtPi;
|
return (2 * Math.exp(-2.0 * (Math.log(x) ** 2))) / x / sqrtPi;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _intergrateEnsureCache(y: number) {
|
function _intergrateEnsureCache(y: number) {
|
||||||
@@ -27,7 +27,7 @@ function _intergrateEnsureCache(y: number) {
|
|||||||
return _CACHE_INFO;
|
return _CACHE_INFO;
|
||||||
}
|
}
|
||||||
|
|
||||||
_intergrateEnsureCache(1000000);
|
_intergrateEnsureCache(10000);
|
||||||
|
|
||||||
function _integrate(y: number) {
|
function _integrate(y: number) {
|
||||||
_intergrateEnsureCache(y);
|
_intergrateEnsureCache(y);
|
||||||
@@ -38,7 +38,7 @@ function difficultyAlgorithm(nSubmit: number, nAccept: number) {
|
|||||||
if (!nSubmit) return null;
|
if (!nSubmit) return null;
|
||||||
const s = _integrate(nSubmit);
|
const s = _integrate(nSubmit);
|
||||||
const acRate = nAccept / nSubmit;
|
const acRate = nAccept / nSubmit;
|
||||||
const ans = Math.round(10.0 - 1.30 * s * 10.0 * acRate);
|
const ans = Math.round(10 - 13 * s * acRate);
|
||||||
return Math.max(ans, 1);
|
return Math.max(ans, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { load } from 'js-yaml';
|
import { load } from 'js-yaml';
|
||||||
import readYamlCases from '@hydrooj/utils/lib/cases';
|
import readYamlCases from '@hydrooj/utils/lib/cases';
|
||||||
import type { ProblemConfig, ProblemConfigFile } from '../interface';
|
import type { ProblemConfig, ProblemConfigFile } from '../interface';
|
||||||
|
import { normalizeSubtasks, parseMemoryMB, parseTimeMS } from '../utils';
|
||||||
|
|
||||||
export async function parseConfig(config: string | ProblemConfigFile = {}) {
|
export async function parseConfig(config: string | ProblemConfigFile = {}) {
|
||||||
let cfg: ProblemConfigFile = {};
|
const cfg: ProblemConfigFile = typeof config === 'string'
|
||||||
if (typeof config === 'string') {
|
? await readYamlCases(load(config) as Record<string, any>)
|
||||||
// TODO should validate here?
|
: await readYamlCases(config);
|
||||||
cfg = await readYamlCases(load(config) as Record<string, any>);
|
|
||||||
} else if (typeof config === 'object') cfg = await readYamlCases(config);
|
|
||||||
const result: ProblemConfig = {
|
const result: ProblemConfig = {
|
||||||
count: 0,
|
count: 0,
|
||||||
memoryMin: Number.MAX_SAFE_INTEGER,
|
memoryMin: Number.MAX_SAFE_INTEGER,
|
||||||
@@ -18,23 +17,19 @@ export async function parseConfig(config: string | ProblemConfigFile = {}) {
|
|||||||
};
|
};
|
||||||
if (cfg.subType) result.subType = cfg.subType;
|
if (cfg.subType) result.subType = cfg.subType;
|
||||||
if (cfg.target) result.target = cfg.target;
|
if (cfg.target) result.target = cfg.target;
|
||||||
if (cfg.subtasks.length) {
|
if (cfg.subtasks?.length) {
|
||||||
for (const subtask of cfg.subtasks) {
|
for (const subtask of normalizeSubtasks(cfg.subtasks as any || [], (i) => i, cfg.time, cfg.memory)) {
|
||||||
result.memoryMax = Math.max(result.memoryMax, subtask.memory);
|
result.memoryMax = Math.max(result.memoryMax, ...subtask.cases.map((i) => parseMemoryMB(i.memory)));
|
||||||
result.memoryMin = Math.min(result.memoryMin, subtask.memory);
|
result.memoryMin = Math.min(result.memoryMin, ...subtask.cases.map((i) => parseMemoryMB(i.memory)));
|
||||||
result.timeMax = Math.max(result.timeMax, subtask.time);
|
result.timeMax = Math.max(result.timeMax, ...subtask.cases.map((i) => parseTimeMS(i.time)));
|
||||||
result.timeMin = Math.min(result.timeMin, subtask.time);
|
result.timeMin = Math.min(result.timeMin, ...subtask.cases.map((i) => parseTimeMS(i.time)));
|
||||||
}
|
}
|
||||||
} else if (cfg.time || cfg.memory) {
|
} else {
|
||||||
if (cfg.time) result.timeMax = result.timeMin = cfg.time as unknown as number;
|
if (cfg.time) result.timeMax = result.timeMin = cfg.time as unknown as number;
|
||||||
if (cfg.memory) result.memoryMax = result.memoryMin = cfg.memory as unknown as number;
|
if (cfg.memory) result.memoryMax = result.memoryMin = cfg.memory as unknown as number;
|
||||||
}
|
}
|
||||||
if (result.memoryMax < result.memoryMin) {
|
if (result.memoryMax < result.memoryMin) result.memoryMax = result.memoryMin = 256;
|
||||||
result.memoryMax = result.memoryMin = 256;
|
if (result.timeMax < result.timeMin) result.timeMax = result.timeMin = 1000;
|
||||||
}
|
|
||||||
if (result.timeMax < result.timeMin) {
|
|
||||||
result.timeMax = result.timeMin = 1000;
|
|
||||||
}
|
|
||||||
if (cfg.langs) result.langs = cfg.langs;
|
if (cfg.langs) result.langs = cfg.langs;
|
||||||
if (cfg.redirect) result.redirect = cfg.redirect.split('/', 2) as any;
|
if (cfg.redirect) result.redirect = cfg.redirect.split('/', 2) as any;
|
||||||
if (cfg.filename && result.type === 'default') {
|
if (cfg.filename && result.type === 'default') {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const acm = buildContestRule({
|
|||||||
check: () => { },
|
check: () => { },
|
||||||
statusSort: { accept: -1, time: 1 },
|
statusSort: { accept: -1, time: 1 },
|
||||||
submitAfterAccept: false,
|
submitAfterAccept: false,
|
||||||
showScoreboard: () => true,
|
showScoreboard: (tdoc, now) => now > tdoc.beginAt,
|
||||||
showSelfRecord: () => true,
|
showSelfRecord: () => true,
|
||||||
showRecord: (tdoc, now) => now > tdoc.endAt,
|
showRecord: (tdoc, now) => now > tdoc.endAt,
|
||||||
stat(tdoc, journal: AcmJournal[], ignoreLock = false) {
|
stat(tdoc, journal: AcmJournal[], ignoreLock = false) {
|
||||||
@@ -297,7 +297,7 @@ const ioi = buildContestRule({
|
|||||||
submitAfterAccept: false,
|
submitAfterAccept: false,
|
||||||
showRecord: (tdoc, now) => now > tdoc.endAt,
|
showRecord: (tdoc, now) => now > tdoc.endAt,
|
||||||
showSelfRecord: () => true,
|
showSelfRecord: () => true,
|
||||||
showScoreboard: () => true,
|
showScoreboard: (tdoc, now) => now > tdoc.beginAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const homework = buildContestRule({
|
const homework = buildContestRule({
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default (router, logger) => async (ctx: KoaContext, next) => {
|
|||||||
}
|
}
|
||||||
for (const key in kwargs.query || {}) {
|
for (const key in kwargs.query || {}) {
|
||||||
if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString();
|
if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString();
|
||||||
else query[key] = encodeURIComponent(kwargs.query[key].toString());
|
else query[key] = kwargs.query[key].toString();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { anchor } = args;
|
const { anchor } = args;
|
||||||
@@ -52,7 +52,7 @@ export default (router, logger) => async (ctx: KoaContext, next) => {
|
|||||||
|| (host instanceof Array
|
|| (host instanceof Array
|
||||||
? (!host.includes(request.host))
|
? (!host.includes(request.host))
|
||||||
: request.host !== host)
|
: request.host !== host)
|
||||||
)) withDomainId = domainId;
|
)) withDomainId ||= domainId;
|
||||||
res = router.url(name, args, { query }).toString();
|
res = router.url(name, args, { query }).toString();
|
||||||
if (anchor) res = `${res}#${anchor}`;
|
if (anchor) res = `${res}#${anchor}`;
|
||||||
if (withDomainId) res = `/d/${withDomainId}${res}`;
|
if (withDomainId) res = `/d/${withDomainId}${res}`;
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ app.on('error', (error) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
wsServer.on('error', (error) => {
|
wsServer.on('error', (error) => {
|
||||||
console.log(123, error);
|
console.log('Websocket server error:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
const ignoredLimit = `,${argv.options.ignoredLimit},`;
|
const ignoredLimit = `,${argv.options.ignoredLimit},`;
|
||||||
@@ -139,7 +139,7 @@ ${ctx.response.status} ${endTime - startTime}ms ${ctx.response.length}`);
|
|||||||
}));
|
}));
|
||||||
const layers = [baseLayer, rendererLayer(router, logger), responseLayer(router), userLayer];
|
const layers = [baseLayer, rendererLayer(router, logger), responseLayer(router), userLayer];
|
||||||
app.use(async (ctx, next) => await next().catch(console.error)).use(domainLayer);
|
app.use(async (ctx, next) => await next().catch(console.error)).use(domainLayer);
|
||||||
layers.forEach((layer) => router.use(layer));
|
layers.forEach((layer) => router.use(layer as any));
|
||||||
wsServer.on('connection', async (socket, request) => {
|
wsServer.on('connection', async (socket, request) => {
|
||||||
const ctx: any = app.createContext(request, {} as any);
|
const ctx: any = app.createContext(request, {} as any);
|
||||||
await domainLayer(ctx, () => baseLayer(ctx, () => layers[1](ctx, () => userLayer(ctx, () => { }))));
|
await domainLayer(ctx, () => baseLayer(ctx, () => layers[1](ctx, () => userLayer(ctx, () => { }))));
|
||||||
|
|||||||
@@ -119,9 +119,9 @@ class ImportQduojHandler extends Handler {
|
|||||||
|
|
||||||
async post({ domainId }) {
|
async post({ domainId }) {
|
||||||
if (!this.request.files.file) throw new ValidationError('file');
|
if (!this.request.files.file) throw new ValidationError('file');
|
||||||
const stat = await fs.stat(this.request.files.file.path);
|
const stat = await fs.stat(this.request.files.file.filepath);
|
||||||
if (stat.size > 128 * 1024 * 1024) throw new ValidationError('file', 'File too large');
|
if (stat.size > 128 * 1024 * 1024) throw new ValidationError('file', 'File too large');
|
||||||
await this.fromFile(domainId, this.request.files.file.path);
|
await this.fromFile(domainId, this.request.files.file.filepath);
|
||||||
this.response.redirect = this.url('problem_main');
|
this.response.redirect = this.url('problem_main');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/import-qduoj",
|
"name": "@hydrooj/import-qduoj",
|
||||||
"version": "1.3.3",
|
"version": "1.3.4",
|
||||||
"description": "Import QDUOJ problem export",
|
"description": "Import QDUOJ problem export",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
@@ -10,6 +10,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "0.5.5",
|
"adm-zip": "0.5.5",
|
||||||
"decode-html": "^2.0.0",
|
"decode-html": "^2.0.0",
|
||||||
"fs-extra": "^10.0.1"
|
"fs-extra": "^10.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,6 @@
|
|||||||
"hydrooj": "*"
|
"hydrooj": "*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"superagent": "^7.1.2"
|
"superagent": "^7.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,6 @@
|
|||||||
"hydrooj": "*"
|
"hydrooj": "*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"superagent": "^7.1.2"
|
"superagent": "^7.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# migrate-hustoj
|
|
||||||
|
|
||||||
这个模块用于从HustOJ的数据库导入数据。
|
|
||||||
|
|
||||||
## 迁移须知
|
|
||||||
|
|
||||||
迁移前,您需要指定数据来源。
|
|
||||||
迁移过程中,Hydro 的内容会被**清空**,以下内容将被迁移:
|
|
||||||
|
|
||||||
题目与测试数据,用户列表。
|
|
||||||
|
|
||||||
迁移过程中请保持网络畅通。
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
migrate from hustoj: 从 HustOJ 导入
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { } from 'hydrooj';
|
|
||||||
|
|
||||||
const { md5, sha1 } = global.Hydro.lib;
|
|
||||||
|
|
||||||
interface Udoc {
|
|
||||||
uname: string,
|
|
||||||
unameLower: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
function hash(password: string, salt: string, udoc: Udoc) {
|
|
||||||
const { uname, unameLower } = udoc;
|
|
||||||
const pmd5 = md5(password);
|
|
||||||
const mixedSha1 = sha1(md5(unameLower + pmd5) + salt + sha1(pmd5 + salt));
|
|
||||||
return `${Buffer.from(uname).toString('base64')}|${mixedSha1}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
global.Hydro.lib['hash.vj2'] = hash;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
migrate from vijos: 从 Vijos 导入
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@hydrooj/migrate-vijos",
|
|
||||||
"version": "1.5.1",
|
|
||||||
"main": "package.json",
|
|
||||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
|
||||||
"author": "undefined <i@undefined.moe>",
|
|
||||||
"preferUnplugged": true,
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/mongodb": "^3.6.20"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"mongodb": "^3.7.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
# migrate-vijos
|
# migrate
|
||||||
|
|
||||||
这个模块用于从Vijos4.0的数据库导入数据。
|
|
||||||
|
|
||||||
## 迁移须知
|
这个模块用于从其他系统批量导入数据。
|
||||||
|
迁移过程中请保持网络畅通。
|
||||||
|
|
||||||
|
## migrate-vijos
|
||||||
|
|
||||||
|
从Vijos4.0的数据库导入数据。
|
||||||
迁移前,您需要指定数据来源。
|
迁移前,您需要指定数据来源。
|
||||||
**请不要**将 Hydro 正在使用的数据库设置为数据来源的数据库。
|
**请不要**将 Hydro 正在使用的数据库设置为数据来源的数据库。
|
||||||
迁移过程中,Hydro 的以下内容会被**清空**:
|
迁移过程中,Hydro 的以下内容会被**清空**:
|
||||||
|
|
||||||
题目列表,提交记录,用户列表,比赛列表,比赛成绩表,训练列表,训练进度,站内消息,题解,讨论
|
题目列表,提交记录,用户列表,比赛列表,比赛成绩表,训练列表,训练进度,站内消息,题解,讨论
|
||||||
|
|
||||||
以下内容将被迁移:
|
以下内容将被迁移:
|
||||||
|
|
||||||
题目与测试数据、题解,讨论与回复,比赛、训练、作业相关数据,提交记录,用户列表,站内消息。
|
题目与测试数据、题解,讨论与回复,比赛、训练、作业相关数据,提交记录,用户列表,站内消息。
|
||||||
|
|
||||||
迁移过程中请保持网络畅通。
|
## migrate-hustoj
|
||||||
|
|
||||||
|
从HustOJ的数据库导入数据。
|
||||||
|
迁移过程中,Hydro 的内容会被**清空**,以下内容将被迁移:
|
||||||
|
题目与测试数据,用户列表。
|
||||||
@@ -2,7 +2,7 @@ import { md5, sha1 } from 'hydrooj/src/lib/crypto';
|
|||||||
|
|
||||||
const RE_MD5 = /^[\da-f]{32}$/;
|
const RE_MD5 = /^[\da-f]{32}$/;
|
||||||
|
|
||||||
function hash($password: string, $saved: string) {
|
global.Hydro.lib['hash.hust'] = ($password: string, $saved: string) => {
|
||||||
$password = md5($password);
|
$password = md5($password);
|
||||||
if (RE_MD5.test($saved)) return $password === $saved;
|
if (RE_MD5.test($saved)) return $password === $saved;
|
||||||
const $svd = Buffer.from($saved, 'base64').toString('hex');
|
const $svd = Buffer.from($saved, 'base64').toString('hex');
|
||||||
@@ -13,6 +13,11 @@ function hash($password: string, $saved: string) {
|
|||||||
]).toString('base64');
|
]).toString('base64');
|
||||||
if ($hash.trim() === $saved.trim()) return true;
|
if ($hash.trim() === $saved.trim()) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
global.Hydro.lib['hash.hust'] = hash;
|
global.Hydro.lib['hash.vj2'] = (password: string, salt: string, udoc: { uname: string; unameLower: string }) => {
|
||||||
|
const { uname, unameLower } = udoc;
|
||||||
|
const pmd5 = md5(password);
|
||||||
|
const mixedSha1 = sha1(md5(unameLower + pmd5) + salt + sha1(pmd5 + salt));
|
||||||
|
return `${Buffer.from(uname).toString('base64')}|${mixedSha1}`;
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
migrate from hustoj: 从 HustOJ 导入
|
||||||
|
migrate from vijos: 从 Vijos 导入
|
||||||
@@ -1,20 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/migrate-hustoj",
|
"name": "@hydrooj/migrate",
|
||||||
"version": "0.1.4",
|
"version": "0.1.0",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
"repository": "git@github.com:hydro-dev/Hydro.git",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"preferUnplugged": true,
|
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint lib.ts script.ts --fix",
|
|
||||||
"build": "tsc"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/mongodb": "^3.6.20",
|
||||||
"@types/mysql": "^2.15.21"
|
"@types/mysql": "^2.15.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hydrooj/utils": "workspace:*",
|
"@hydrooj/utils": "workspace:*",
|
||||||
|
"mongodb": "^3.7.3",
|
||||||
"mysql": "^2.18.1"
|
"mysql": "^2.18.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import './scripts/hustoj';
|
||||||
|
import './scripts/vijos';
|
||||||
File renamed without changes.
@@ -200,9 +200,13 @@ const tasks = {
|
|||||||
},
|
},
|
||||||
record: async (doc) => {
|
record: async (doc) => {
|
||||||
if (doc.hidden) return null;
|
if (doc.hidden) return null;
|
||||||
const testCases: TestCase[] = [];
|
const testCases: Required<TestCase>[] = [];
|
||||||
|
let i = 1;
|
||||||
for (const c of doc.cases || []) {
|
for (const c of doc.cases || []) {
|
||||||
testCases.push({
|
testCases.push({
|
||||||
|
id: i++,
|
||||||
|
subtaskId: 0,
|
||||||
|
score: 0,
|
||||||
status: c.status,
|
status: c.status,
|
||||||
time: c.time_ms || c.time,
|
time: c.time_ms || c.time,
|
||||||
memory: c.memory_kb || c.memory,
|
memory: c.memory_kb || c.memory,
|
||||||
@@ -8,6 +8,6 @@
|
|||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"preferUnplugged": true,
|
"preferUnplugged": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"superagent": "^7.1.2"
|
"superagent": "^7.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ module.exports = {
|
|||||||
indent: [
|
indent: [
|
||||||
'error',
|
'error',
|
||||||
2,
|
2,
|
||||||
{ SwitchCase: 0 },
|
{ SwitchCase: 1 },
|
||||||
],
|
],
|
||||||
'max-len': ['error', 150],
|
'max-len': ['error', 150],
|
||||||
quotes: 'warn',
|
quotes: 'warn',
|
||||||
@@ -115,6 +115,7 @@ module.exports = {
|
|||||||
'no-underscore-dangle': 'off',
|
'no-underscore-dangle': 'off',
|
||||||
'no-await-in-loop': 'off',
|
'no-await-in-loop': 'off',
|
||||||
'no-lonely-if': 'off',
|
'no-lonely-if': 'off',
|
||||||
|
'no-nested-ternary': 'off',
|
||||||
'no-param-reassign': 'off',
|
'no-param-reassign': 'off',
|
||||||
'no-script-url': 'off',
|
'no-script-url': 'off',
|
||||||
'no-bitwise': 'off',
|
'no-bitwise': 'off',
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export default function (env = {}) {
|
|||||||
mode: (env.production || env.measure) ? 'production' : 'development',
|
mode: (env.production || env.measure) ? 'production' : 'development',
|
||||||
profile: true,
|
profile: true,
|
||||||
context: root(),
|
context: root(),
|
||||||
|
devtool: env.production ? 'source-map' : false,
|
||||||
entry: {
|
entry: {
|
||||||
hydro: './entry.js',
|
hydro: './entry.js',
|
||||||
polyfill: './polyfill.ts',
|
polyfill: './polyfill.ts',
|
||||||
@@ -100,8 +101,6 @@ export default function (env = {}) {
|
|||||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
alias: {
|
alias: {
|
||||||
vj: root(),
|
vj: root(),
|
||||||
'js-yaml': root('utils/yamlCompact'),
|
|
||||||
'real-js-yaml': require.resolve('js-yaml'),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
@@ -205,12 +204,12 @@ export default function (env = {}) {
|
|||||||
patterns: [
|
patterns: [
|
||||||
{ from: root('static') },
|
{ from: root('static') },
|
||||||
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
|
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
|
||||||
{ from: root('.build/sharedworker.js'), to: 'sharedworker.js' },
|
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': {
|
'process.env': {
|
||||||
NODE_ENV: env.production ? '"production"' : '"debug"',
|
NODE_ENV: env.production ? '"production"' : '"debug"',
|
||||||
|
VERSION: JSON.stringify(require('@hydrooj/ui-default/package.json').version),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new webpack.LoaderOptionsPlugin({
|
new webpack.LoaderOptionsPlugin({
|
||||||
@@ -224,10 +223,10 @@ export default function (env = {}) {
|
|||||||
filename: '[name].[hash:10].worker.js',
|
filename: '[name].[hash:10].worker.js',
|
||||||
customLanguages: [{
|
customLanguages: [{
|
||||||
label: 'yaml',
|
label: 'yaml',
|
||||||
entry: require.resolve('@undefined-moe/monaco-yaml/lib/esm/monaco.contribution'),
|
entry: require.resolve('monaco-yaml/index.js'),
|
||||||
worker: {
|
worker: {
|
||||||
id: 'vs/language/yaml/yamlWorker',
|
id: 'vs/language/yaml/yamlWorker',
|
||||||
entry: require.resolve('@undefined-moe/monaco-yaml/lib/esm/yaml.worker.js'),
|
entry: require.resolve('monaco-yaml/yaml.worker.js'),
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import cac from 'cac';
|
import cac from 'cac';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import webpack from 'webpack';
|
import webpack from 'webpack';
|
||||||
import esbuild from 'esbuild';
|
|
||||||
import WebpackDevServer from 'webpack-dev-server';
|
import WebpackDevServer from 'webpack-dev-server';
|
||||||
import gulp from 'gulp';
|
import gulp from 'gulp';
|
||||||
import log from 'fancy-log';
|
import log from 'fancy-log';
|
||||||
@@ -18,15 +17,6 @@ const argv = cac().parse();
|
|||||||
async function runWebpack({
|
async function runWebpack({
|
||||||
watch, production, measure, dev,
|
watch, production, measure, dev,
|
||||||
}) {
|
}) {
|
||||||
const result = esbuild.build({
|
|
||||||
bundle: true,
|
|
||||||
format: 'iife',
|
|
||||||
entryPoints: [root('sharedworker.ts')],
|
|
||||||
outfile: root('.build/sharedworker.js'),
|
|
||||||
watch: dev || watch,
|
|
||||||
minify: production,
|
|
||||||
});
|
|
||||||
if (!dev && !watch) await result;
|
|
||||||
const compiler = webpack(webpackConfig({ watch, production, measure }));
|
const compiler = webpack(webpackConfig({ watch, production, measure }));
|
||||||
if (dev) {
|
if (dev) {
|
||||||
const server = new WebpackDevServer(compiler, {
|
const server = new WebpackDevServer(compiler, {
|
||||||
@@ -36,7 +26,7 @@ async function runWebpack({
|
|||||||
stats: 'none',
|
stats: 'none',
|
||||||
index: root('public'),
|
index: root('public'),
|
||||||
proxy: {
|
proxy: {
|
||||||
context: (path) => !path.includes('sharedworker.js'),
|
context: (path) => !path.includes('sockjs-node'),
|
||||||
target: 'http://localhost:2333',
|
target: 'http://localhost:2333',
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
@@ -98,6 +88,10 @@ async function main() {
|
|||||||
if (fs.existsSync('public/default.theme.css')) {
|
if (fs.existsSync('public/default.theme.css')) {
|
||||||
fs.copyFileSync('public/default.theme.css', `public/default-${pkg.version}.theme.css`);
|
fs.copyFileSync('public/default.theme.css', `public/default-${pkg.version}.theme.css`);
|
||||||
}
|
}
|
||||||
|
if (argv.options.production) {
|
||||||
|
const files = fs.readdirSync('public');
|
||||||
|
files.filter((i) => /(^[in]\..+|worker)\.js\.map$/.test(i)).forEach((i) => fs.removeSync(`public/${i}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
process.chdir(dir);
|
process.chdir(dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { assign } from 'lodash';
|
import { assign } from 'lodash';
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
import AutoComplete from '.';
|
import AutoComplete from '.';
|
||||||
@@ -33,15 +33,12 @@ export default class DomainSelectAutoComplete extends AutoComplete {
|
|||||||
|
|
||||||
attach() {
|
attach() {
|
||||||
const value = this.$dom.val();
|
const value = this.$dom.val();
|
||||||
ReactDOM.render(
|
ReactDOM.createRoot(this.container).render(<Component
|
||||||
<Component
|
ref={(ref) => { this.ref = ref; }}
|
||||||
ref={(ref) => { this.ref = ref; }}
|
value={value}
|
||||||
value={value}
|
onChange={this.onChange}
|
||||||
onChange={this.onChange}
|
multi={this.options.multi}
|
||||||
multi={this.options.multi}
|
/>);
|
||||||
/>,
|
|
||||||
this.container,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { assign } from 'lodash';
|
import { assign } from 'lodash';
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
import AutoComplete from '.';
|
import AutoComplete from '.';
|
||||||
@@ -33,14 +33,13 @@ export default class ProblemSelectAutoComplete extends AutoComplete {
|
|||||||
|
|
||||||
attach() {
|
attach() {
|
||||||
const value = this.$dom.val();
|
const value = this.$dom.val();
|
||||||
ReactDOM.render(
|
ReactDOM.createRoot(this.container).render(
|
||||||
<Component
|
<Component
|
||||||
ref={(ref) => { this.ref = ref; }}
|
ref={(ref) => { this.ref = ref; }}
|
||||||
value={value}
|
value={value}
|
||||||
multi={this.options.multi}
|
multi={this.options.multi}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
/>,
|
/>,
|
||||||
this.container,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { assign } from 'lodash';
|
import { assign } from 'lodash';
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
import AutoComplete, { AutoCompleteOptions } from '.';
|
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||||
@@ -38,14 +38,13 @@ export default class UserSelectAutoComplete<Multi extends boolean> extends AutoC
|
|||||||
|
|
||||||
attach() {
|
attach() {
|
||||||
const value = this.$dom.val();
|
const value = this.$dom.val();
|
||||||
ReactDOM.render(
|
ReactDOM.createRoot(this.container).render(
|
||||||
<Component
|
<Component
|
||||||
ref={(ref) => { this.ref = ref; }}
|
ref={(ref) => { this.ref = ref; }}
|
||||||
value={value}
|
value={value}
|
||||||
multi={this.options.multi}
|
multi={this.options.multi}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
/>,
|
/>,
|
||||||
this.container,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
text-overflow: ellipsis
|
text-overflow: ellipsis
|
||||||
|
|
||||||
&>span.bp3-icon>svg
|
&>span.bp4-icon>svg
|
||||||
font-size: 12px
|
font-size: 12px
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
padding: 4px
|
padding: 4px
|
||||||
@@ -78,19 +78,19 @@
|
|||||||
&>div
|
&>div
|
||||||
flex-grow: 1
|
flex-grow: 1
|
||||||
|
|
||||||
&>span.bp3-icon>svg
|
&>span.bp4-icon>svg
|
||||||
color: transparent
|
color: transparent
|
||||||
|
|
||||||
&[aria-selected='true']
|
&[aria-selected='true']
|
||||||
background-color: #fafafa
|
background-color: #fafafa
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
|
|
||||||
&>span.bp3-icon>svg
|
&>span.bp4-icon>svg
|
||||||
color: #1890ff
|
color: #1890ff
|
||||||
|
|
||||||
&[data-focus='true']
|
&[data-focus='true']
|
||||||
background-color: #e6f7ff
|
background-color: #e6f7ff
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
&>span.bp3-icon>svg
|
&>span.bp4-icon>svg
|
||||||
color: currentColor
|
color: currentColor
|
||||||
@@ -17,6 +17,7 @@ export interface AutoCompleteProps<Item> {
|
|||||||
*/
|
*/
|
||||||
height?: string;
|
height?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
disabledHint?: string;
|
disabledHint?: string;
|
||||||
listStyle?: React.CSSProperties;
|
listStyle?: React.CSSProperties;
|
||||||
cacheKey?: string;
|
cacheKey?: string;
|
||||||
@@ -128,8 +129,10 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
|
|||||||
else onChange(selectedKeys.join(','));
|
else onChange(selectedKeys.join(','));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let first = !multi;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatchChange();
|
if (first) first = false;
|
||||||
|
else dispatchChange();
|
||||||
const ids = [];
|
const ids = [];
|
||||||
for (const key of selectedKeys) if (!valueCache[key]) ids.push(key);
|
for (const key of selectedKeys) if (!valueCache[key]) ids.push(key);
|
||||||
if (!ids.length) return;
|
if (!ids.length) return;
|
||||||
@@ -293,6 +296,7 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
|
|||||||
if (allowEmptyQuery) handleInputChange();
|
if (allowEmptyQuery) handleInputChange();
|
||||||
setFocused(true);
|
setFocused(true);
|
||||||
}}
|
}}
|
||||||
|
placeholder={props.placeholder}
|
||||||
onBlur={() => setFocused(false)}
|
onBlur={() => setFocused(false)}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
defaultValue={multi ? '' : selectedKeys.join(',')}
|
defaultValue={multi ? '' : selectedKeys.join(',')}
|
||||||
@@ -342,6 +346,7 @@ AutoComplete.propTypes = {
|
|||||||
allowEmptyQuery: PropTypes.bool,
|
allowEmptyQuery: PropTypes.bool,
|
||||||
freeSolo: PropTypes.bool,
|
freeSolo: PropTypes.bool,
|
||||||
freeSoloConverter: PropTypes.func,
|
freeSoloConverter: PropTypes.func,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
AutoComplete.defaultProps = {
|
AutoComplete.defaultProps = {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ CustomSelectAutoComplete.propTypes = {
|
|||||||
allowEmptyQuery: PropTypes.bool,
|
allowEmptyQuery: PropTypes.bool,
|
||||||
freeSolo: PropTypes.bool,
|
freeSolo: PropTypes.bool,
|
||||||
freeSoloConverter: PropTypes.func,
|
freeSoloConverter: PropTypes.func,
|
||||||
data: PropTypes.any,
|
data: PropTypes.arrayOf(PropTypes.any),
|
||||||
};
|
};
|
||||||
|
|
||||||
CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete';
|
CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete';
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import AutoComplete from './AutoComplete';
|
||||||
|
|
||||||
|
type DefaultProps = {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
} & string;
|
||||||
|
|
||||||
|
const FileSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
|
||||||
|
<AutoComplete<DefaultProps>
|
||||||
|
ref={ref as any}
|
||||||
|
fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
|
||||||
|
queryItems={(query) => props.data.filter((i) => (i.name
|
||||||
|
? i.name.toLowerCase().match(query.toLowerCase())
|
||||||
|
: i.toString().toLowerCase().match(query.toLowerCase())
|
||||||
|
))}
|
||||||
|
itemText={(item) => `${item.name || item}`}
|
||||||
|
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
|
||||||
|
renderItem={(item) => item.name || item._id || item} // TODO icon
|
||||||
|
allowEmptyQuery
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
FileSelectAutoComplete.propTypes = {
|
||||||
|
width: PropTypes.string,
|
||||||
|
height: PropTypes.string,
|
||||||
|
listStyle: PropTypes.object,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
multi: PropTypes.bool,
|
||||||
|
selectedKeys: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
allowEmptyQuery: PropTypes.bool,
|
||||||
|
freeSolo: PropTypes.bool,
|
||||||
|
freeSoloConverter: PropTypes.func,
|
||||||
|
data: PropTypes.arrayOf(PropTypes.any),
|
||||||
|
};
|
||||||
|
|
||||||
|
FileSelectAutoComplete.displayName = 'FileSelectAutoComplete';
|
||||||
|
|
||||||
|
FileSelectAutoComplete.defaultProps = {
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
listStyle: {},
|
||||||
|
multi: false,
|
||||||
|
selectedKeys: [],
|
||||||
|
freeSolo: false,
|
||||||
|
freeSoloConverter: (input) => input,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileSelectAutoComplete;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { assign } from 'lodash';
|
import { assign } from 'lodash';
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
import AutoCompleteFC from './components/AutoComplete';
|
import AutoCompleteFC from './components/AutoComplete';
|
||||||
@@ -64,7 +64,7 @@ export default class AutoComplete extends DOMAttachedObject {
|
|||||||
|
|
||||||
attach() {
|
attach() {
|
||||||
const value = this.$dom.val();
|
const value = this.$dom.val();
|
||||||
ReactDOM.render(
|
ReactDOM.createRoot(this.container).render(
|
||||||
<AutoCompleteFC
|
<AutoCompleteFC
|
||||||
ref={(ref) => { this.ref = ref; }}
|
ref={(ref) => { this.ref = ref; }}
|
||||||
height="34px"
|
height="34px"
|
||||||
@@ -76,7 +76,6 @@ export default class AutoComplete extends DOMAttachedObject {
|
|||||||
multi={this.options.multi}
|
multi={this.options.multi}
|
||||||
freeSolo={this.options.multi}
|
freeSolo={this.options.multi}
|
||||||
/>,
|
/>,
|
||||||
this.container,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'jquery.easing';
|
import 'jquery.easing';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { Popover } from '@blueprintjs/core';
|
import { Popover } from '@blueprintjs/core';
|
||||||
import { AutoloadPage } from 'vj/misc/Page';
|
import { AutoloadPage } from 'vj/misc/Page';
|
||||||
import CommentBox from 'vj/components/discussion/CommentBox';
|
import CommentBox from 'vj/components/discussion/CommentBox';
|
||||||
@@ -256,7 +256,7 @@ const commentsPage = new AutoloadPage('commentsPage', () => {
|
|||||||
|
|
||||||
const canUseReaction = $('[data-op="react"]').length > 0;
|
const canUseReaction = $('[data-op="react"]').length > 0;
|
||||||
$('[data-op="react"]').each((i, e) => {
|
$('[data-op="react"]').each((i, e) => {
|
||||||
ReactDOM.render(<Reaction payload={$(e).data('form')} ele={$(e).closest('.media__body').find('.reactions')} />, e);
|
ReactDOM.createRoot(e).render(<Reaction payload={$(e).data('form')} ele={$(e).closest('.media__body').find('.reactions')} />);
|
||||||
});
|
});
|
||||||
$(document).on('click', '.reaction', async (e) => {
|
$(document).on('click', '.reaction', async (e) => {
|
||||||
if (!canUseReaction) {
|
if (!canUseReaction) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default class VjEditor extends DOMAttachedObject {
|
|||||||
? monaco.editor.getModel(monaco.Uri.parse(model))
|
? monaco.editor.getModel(monaco.Uri.parse(model))
|
||||||
|| monaco.editor.createModel(value, language === 'auto' ? undefined : language, monaco.Uri.parse(model))
|
|| monaco.editor.createModel(value, language === 'auto' ? undefined : language, monaco.Uri.parse(model))
|
||||||
: model;
|
: model;
|
||||||
this.model.setValue(value);
|
if (!this.options.model) this.model.setValue(value);
|
||||||
const cfg: import('../monaco').default.editor.IStandaloneEditorConstructionOptions = {
|
const cfg: import('../monaco').default.editor.IStandaloneEditorConstructionOptions = {
|
||||||
theme,
|
theme,
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
|
|||||||
@@ -38,15 +38,15 @@ function testElementHotkey(hotkey, $element, attr) {
|
|||||||
const [defStr, trigger] = singleDef.split(':');
|
const [defStr, trigger] = singleDef.split(':');
|
||||||
if (isHotkeyMatch(hotkey, defStr)) {
|
if (isHotkeyMatch(hotkey, defStr)) {
|
||||||
switch (trigger) {
|
switch (trigger) {
|
||||||
case 'submit':
|
case 'submit':
|
||||||
$element.closest('form').trigger('submit');
|
$element.closest('form').trigger('submit');
|
||||||
break;
|
break;
|
||||||
case undefined:
|
case undefined:
|
||||||
$element.trigger('click');
|
$element.trigger('click');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$element.trigger(trigger);
|
$element.trigger(trigger);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,9 @@
|
|||||||
opacity: 0.85
|
opacity: 0.85
|
||||||
|
|
||||||
a.menu__link, button.menu__link
|
a.menu__link, button.menu__link
|
||||||
|
&.half
|
||||||
|
padding-right: 0
|
||||||
|
width: 50%
|
||||||
&.disabled
|
&.disabled
|
||||||
cursor: default
|
cursor: default
|
||||||
opacity: 0.5
|
opacity: 0.5
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import { AutoloadPage } from 'vj/misc/Page';
|
|
||||||
import { InfoDialog } from 'vj/components/dialog';
|
|
||||||
import VjNotification from 'vj/components/notification/index';
|
|
||||||
import { FLAG_ALERT } from 'vj/constant/message';
|
|
||||||
import i18n from 'vj/utils/i18n';
|
|
||||||
import tpl from 'vj/utils/tpl';
|
|
||||||
|
|
||||||
const messagePage = new AutoloadPage('messagePage', (pagename) => {
|
|
||||||
if (pagename === 'home_messages' || !UserContext._id) return;
|
|
||||||
|
|
||||||
if (UserContext.unreadMsg) {
|
|
||||||
new VjNotification({
|
|
||||||
type: 'info',
|
|
||||||
message: i18n(`You have ${UserContext.unreadMsg > 1 ? 'new messages' : 'a new message'}.`),
|
|
||||||
duration: 5000,
|
|
||||||
action: () => window.open('/home/messages', '_blank'),
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
const onmessage = (message) => {
|
|
||||||
const msg = JSON.parse(message);
|
|
||||||
console.log('Received message', msg);
|
|
||||||
if (msg.mdoc.flag & FLAG_ALERT) {
|
|
||||||
// Is alert
|
|
||||||
return new InfoDialog({
|
|
||||||
cancelByClickingBack: false,
|
|
||||||
$body: tpl`
|
|
||||||
<div class="typo">
|
|
||||||
${{ templateRaw: true, html: i18n('Message from {0}:', `<div data-user>${msg.mdoc.from}</div>`) }}
|
|
||||||
<p>${i18n(msg.mdoc.content)}</p>
|
|
||||||
</div>`,
|
|
||||||
}).open();
|
|
||||||
}
|
|
||||||
if (msg.udoc._id === 1 && msg.mdoc.flag & 4) {
|
|
||||||
// System notification
|
|
||||||
return new VjNotification({
|
|
||||||
message: i18n('You received a system message, click here to view.'),
|
|
||||||
duration: 15000,
|
|
||||||
action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
// Is message
|
|
||||||
return new VjNotification({
|
|
||||||
title: msg.udoc.uname,
|
|
||||||
avatar: msg.udoc.avatarUrl,
|
|
||||||
message: msg.mdoc.content,
|
|
||||||
duration: 15000,
|
|
||||||
action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
|
|
||||||
}).show();
|
|
||||||
};
|
|
||||||
|
|
||||||
const path = new URL('/home/messages-conn', window.location.href).toString();
|
|
||||||
const worker = new SharedWorker('/sharedworker.js', { name: 'Hydro Messages Shared Worker' });
|
|
||||||
worker.port.start();
|
|
||||||
worker.port.postMessage({ type: 'init', path, cookie: document.cookie });
|
|
||||||
worker.port.onmessage = (e) => {
|
|
||||||
if (e.data.type !== 'message') return;
|
|
||||||
if (document.hidden) {
|
|
||||||
worker.port.postMessage({ type: 'fail' });
|
|
||||||
} else {
|
|
||||||
worker.port.postMessage({ type: 'success' });
|
|
||||||
onmessage(e.data.payload);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default messagePage;
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { AutoloadPage } from 'vj/misc/Page';
|
||||||
|
import { InfoDialog } from 'vj/components/dialog';
|
||||||
|
import VjNotification from 'vj/components/notification/index';
|
||||||
|
import ReconnectingWebsocket from 'reconnecting-websocket';
|
||||||
|
import { FLAG_ALERT } from 'vj/constant/message';
|
||||||
|
import i18n from 'vj/utils/i18n';
|
||||||
|
import tpl from 'vj/utils/tpl';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
const onmessage = (msg) => {
|
||||||
|
console.log('Received message', msg);
|
||||||
|
if (msg.mdoc.flag & FLAG_ALERT) {
|
||||||
|
// Is alert
|
||||||
|
return new InfoDialog({
|
||||||
|
cancelByClickingBack: false,
|
||||||
|
$body: tpl`
|
||||||
|
<div class="typo">
|
||||||
|
${{ templateRaw: true, html: i18n('Message from {0}:', `<div data-user>${msg.mdoc.from}</div>`) }}
|
||||||
|
<p>${i18n(msg.mdoc.content)}</p>
|
||||||
|
</div>`,
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
// Is message
|
||||||
|
return new VjNotification({
|
||||||
|
...(msg.udoc._id === 1 && msg.mdoc.flag & 4)
|
||||||
|
? { message: i18n('You received a system message, click here to view.') }
|
||||||
|
: {
|
||||||
|
title: msg.udoc.uname,
|
||||||
|
avatar: msg.udoc.avatarUrl,
|
||||||
|
message: msg.mdoc.content,
|
||||||
|
},
|
||||||
|
duration: 15000,
|
||||||
|
action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
|
||||||
|
}).show();
|
||||||
|
};
|
||||||
|
|
||||||
|
const messagePage = new AutoloadPage('messagePage', (pagename) => {
|
||||||
|
if (pagename === 'home_messages' || !UserContext._id) return;
|
||||||
|
if (UserContext.unreadMsg) {
|
||||||
|
new VjNotification({
|
||||||
|
type: 'info',
|
||||||
|
message: i18n(`You have ${UserContext.unreadMsg > 1 ? 'new messages' : 'a new message'}.`),
|
||||||
|
duration: 5000,
|
||||||
|
action: () => window.open('/home/messages', '_blank'),
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
if (!window.BroadcastChannel) {
|
||||||
|
console.error('BoardcastChannel not supported');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isMaster = false;
|
||||||
|
const selfId = nanoid();
|
||||||
|
const channel = new BroadcastChannel('hydro-messages');
|
||||||
|
const pages = JSON.parse(localStorage.getItem('pages') || '[]') as string[];
|
||||||
|
pages.push(selfId);
|
||||||
|
pages.sort();
|
||||||
|
localStorage.setItem('pages', JSON.stringify(pages));
|
||||||
|
|
||||||
|
window.onunload = () => {
|
||||||
|
const c = JSON.parse(localStorage.getItem('pages') || '[]') as string[];
|
||||||
|
localStorage.setItem('pages', JSON.stringify(c.filter((i) => i !== selfId)));
|
||||||
|
if (!isMaster) return;
|
||||||
|
localStorage.removeItem('page.master');
|
||||||
|
channel?.postMessage({ type: 'master' });
|
||||||
|
};
|
||||||
|
|
||||||
|
function asMaster() {
|
||||||
|
console.log('init as master');
|
||||||
|
isMaster = true;
|
||||||
|
localStorage.setItem('page.master', selfId);
|
||||||
|
const masterChannel = new BroadcastChannel('hydro-messages');
|
||||||
|
const url = new URL('/home/messages-conn', window.location.href.replace('http', 'ws'));
|
||||||
|
// TODO handle a better way for cookie
|
||||||
|
url.searchParams.append('sid', document.cookie);
|
||||||
|
const sock = new ReconnectingWebsocket(url.toString());
|
||||||
|
const pending = {};
|
||||||
|
sock.onopen = () => console.log('Connected');
|
||||||
|
sock.onerror = console.error;
|
||||||
|
sock.onclose = (...args) => console.log('Closed', ...args);
|
||||||
|
sock.onmessage = async (message) => {
|
||||||
|
if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message);
|
||||||
|
const payload = JSON.parse(message.data);
|
||||||
|
const id = nanoid();
|
||||||
|
masterChannel.postMessage({ type: 'message', id, payload });
|
||||||
|
const success = await new Promise<boolean>((resolve) => {
|
||||||
|
pending[id] = resolve;
|
||||||
|
setTimeout(() => {
|
||||||
|
delete pending[id];
|
||||||
|
resolve(false);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
if (!success && window.Notification?.permission === 'granted') {
|
||||||
|
const notification = new window.Notification(
|
||||||
|
payload.udoc.uname || 'Hydro Notification',
|
||||||
|
{
|
||||||
|
icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png',
|
||||||
|
body: payload.mdoc.content,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
notification.onclick = () => window.open('/home/messages');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
masterChannel.onmessage = (msg) => {
|
||||||
|
if (msg.data.type === 'message-push') pending[msg.data.id]?.(true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.onmessage = (msg) => {
|
||||||
|
if (msg.data.type === 'message' && !document.hidden) {
|
||||||
|
channel.postMessage({ type: 'message-push', id: msg.data.id });
|
||||||
|
onmessage(msg.data.payload);
|
||||||
|
}
|
||||||
|
if (msg.data.type === 'master') {
|
||||||
|
const c = JSON.parse(localStorage.getItem('pages') || '[]') as string[];
|
||||||
|
if (c[0] === selfId) asMaster();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!localStorage.getItem('page.master')) asMaster();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messagePage;
|
||||||
@@ -21,14 +21,14 @@ function scrollToViewport() {
|
|||||||
|
|
||||||
export default function reducer(state = null, action) {
|
export default function reducer(state = null, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'DIALOGUES_SWITCH_TO': {
|
case 'DIALOGUES_SWITCH_TO': {
|
||||||
scrollToViewport();
|
scrollToViewport();
|
||||||
return action.payload;
|
return action.payload;
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_POST_SEND_FULFILLED': {
|
case 'DIALOGUES_POST_SEND_FULFILLED': {
|
||||||
return action.meta.dialogueId;
|
return action.meta.dialogueId;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,52 +2,52 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
export default function reducer(state = {}, action) {
|
export default function reducer(state = {}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
|
case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
|
||||||
const { messages } = action.payload;
|
const { messages } = action.payload;
|
||||||
return { ...state, ..._.keyBy(messages, 'udoc._id') };
|
return { ...state, ..._.keyBy(messages, 'udoc._id') };
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_CREATE': {
|
case 'DIALOGUES_CREATE': {
|
||||||
const { user } = action.payload;
|
const { user } = action.payload;
|
||||||
if (state[user._id]) return state;
|
if (state[user._id]) return state;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[user._id]: {
|
[user._id]: {
|
||||||
_id: user._id,
|
_id: user._id,
|
||||||
udoc: { ...user },
|
udoc: { ...user },
|
||||||
messages: [],
|
messages: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_POST_SEND_FULFILLED': {
|
case 'DIALOGUES_POST_SEND_FULFILLED': {
|
||||||
const id = action.meta.dialogueId;
|
const id = action.meta.dialogueId;
|
||||||
const { mdoc } = action.payload;
|
const { mdoc } = action.payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[id]: {
|
[id]: {
|
||||||
...state[id],
|
...state[id],
|
||||||
messages: [
|
messages: [
|
||||||
...state[id].messages,
|
...state[id].messages,
|
||||||
mdoc,
|
mdoc,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_MESSAGE_PUSH': {
|
case 'DIALOGUES_MESSAGE_PUSH': {
|
||||||
const { mdoc, udoc } = action.payload;
|
const { mdoc, udoc } = action.payload;
|
||||||
const to = mdoc.from === UserContext._id ? mdoc.to : mdoc.from;
|
const to = mdoc.from === UserContext._id ? mdoc.to : mdoc.from;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[to]: {
|
[to]: {
|
||||||
...state[to] || {},
|
...state[to] || {},
|
||||||
udoc,
|
udoc,
|
||||||
messages: [
|
messages: [
|
||||||
...state[to]?.messages || [],
|
...state[to]?.messages || [],
|
||||||
mdoc,
|
mdoc,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,42 +2,42 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
export default function reducer(state = {}, action) {
|
export default function reducer(state = {}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
|
case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
|
||||||
const dialogues = action.payload.messages;
|
const dialogues = action.payload.messages;
|
||||||
return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, ''])) };
|
return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, ''])) };
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_CREATE': {
|
case 'DIALOGUES_CREATE': {
|
||||||
const { user } = action.payload;
|
const { user } = action.payload;
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
[user._id]: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'DIALOGUES_INPUT_CHANGED': {
|
|
||||||
const id = action.meta.dialogueId;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
[id]: action.payload,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'DIALOGUES_POST_SEND_FULFILLED': {
|
|
||||||
const id = action.meta.dialogueId;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
[id]: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'DIALOGUES_MESSAGE_PUSH': {
|
|
||||||
const { udoc } = action.payload;
|
|
||||||
if (!state[udoc._id]) {
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[udoc._id]: '',
|
[user._id]: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return state;
|
case 'DIALOGUES_INPUT_CHANGED': {
|
||||||
}
|
const id = action.meta.dialogueId;
|
||||||
default:
|
return {
|
||||||
return state;
|
...state,
|
||||||
|
[id]: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'DIALOGUES_POST_SEND_FULFILLED': {
|
||||||
|
const id = action.meta.dialogueId;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[id]: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'DIALOGUES_MESSAGE_PUSH': {
|
||||||
|
const { udoc } = action.payload;
|
||||||
|
if (!state[udoc._id]) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[udoc._id]: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,47 +3,47 @@ import Notification from 'vj/components/notification';
|
|||||||
|
|
||||||
export default function reducer(state = {}, action) {
|
export default function reducer(state = {}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
|
case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
|
||||||
const dialogues = action.payload.messages;
|
const dialogues = action.payload.messages;
|
||||||
return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, false])) };
|
return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, false])) };
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_CREATE': {
|
case 'DIALOGUES_CREATE': {
|
||||||
const { user } = action.payload;
|
const { user } = action.payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[user._id]: false,
|
[user._id]: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_POST_SEND_PENDING': {
|
case 'DIALOGUES_POST_SEND_PENDING': {
|
||||||
const id = action.meta.dialogueId;
|
const id = action.meta.dialogueId;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[id]: true,
|
[id]: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_POST_SEND_REJECTED': {
|
case 'DIALOGUES_POST_SEND_REJECTED': {
|
||||||
Notification.error(action.payload.message);
|
Notification.error(action.payload.message);
|
||||||
const id = action.meta.dialogueId;
|
const id = action.meta.dialogueId;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[id]: false,
|
[id]: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_POST_SEND_FULFILLED': {
|
case 'DIALOGUES_POST_SEND_FULFILLED': {
|
||||||
const id = action.meta.dialogueId;
|
const id = action.meta.dialogueId;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[id]: false,
|
[id]: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'DIALOGUES_MESSAGE_PUSH': {
|
case 'DIALOGUES_MESSAGE_PUSH': {
|
||||||
const { udoc } = action.payload;
|
const { udoc } = action.payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[udoc._id]: false,
|
[udoc._id]: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
import { setDiagnosticsOptions } from 'monaco-yaml';
|
||||||
|
|
||||||
/** @type {Record<string, import('json-schema').JSONSchema7Definition>} */
|
/** @type {Record<string, import('json-schema').JSONSchema7Definition>} */
|
||||||
const problemConfigSchemaDef = {
|
const problemConfigSchemaDef = {
|
||||||
@@ -8,6 +8,9 @@ const problemConfigSchemaDef = {
|
|||||||
properties: {
|
properties: {
|
||||||
input: { type: 'string' },
|
input: { type: 'string' },
|
||||||
output: { type: 'string' },
|
output: { type: 'string' },
|
||||||
|
time: { $ref: '#/def/time' },
|
||||||
|
memory: { $ref: '#/def/memory' },
|
||||||
|
score: { $ref: '#/def/score', description: 'score' },
|
||||||
},
|
},
|
||||||
required: ['input'],
|
required: ['input'],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -28,7 +31,7 @@ const problemConfigSchemaDef = {
|
|||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
time: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([mu]?)s?$' },
|
time: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([mu]?)s?$' },
|
||||||
memory: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([kmg])b?$' },
|
memory: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([kKmMgG])[bB]?$' },
|
||||||
score: { type: 'integer', maximum: 100, minimum: 1 },
|
score: { type: 'integer', maximum: 100, minimum: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@ const problemConfigSchema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
def: problemConfigSchemaDef,
|
def: problemConfigSchemaDef,
|
||||||
properties: {
|
properties: {
|
||||||
redirect: { type: 'string', pattern: '[0-9a-z_-]+\\/[0-9]+' },
|
redirect: { type: 'string', pattern: '[0-9a-zA-Z_-]+\\/[0-9]+' },
|
||||||
key: { type: 'string', pattern: '[0-9a-f]{32}' },
|
key: { type: 'string', pattern: '[0-9a-f]{32}' },
|
||||||
type: { enum: ['default', 'interactive', 'submit_answer', 'objective', 'remote_judge'] },
|
type: { enum: ['default', 'interactive', 'submit_answer', 'objective', 'remote_judge'] },
|
||||||
subType: { type: 'string' },
|
subType: { type: 'string' },
|
||||||
@@ -75,8 +78,7 @@ const problemConfigSchema = {
|
|||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
setDiagnosticsOptions({
|
||||||
monaco.languages.yaml.yamlDefaults.setDiagnosticsOptions({
|
|
||||||
validate: true,
|
validate: true,
|
||||||
enableSchemaRequest: true,
|
enableSchemaRequest: true,
|
||||||
hover: true,
|
hover: true,
|
||||||
@@ -86,7 +88,7 @@ monaco.languages.yaml.yamlDefaults.setDiagnosticsOptions({
|
|||||||
{
|
{
|
||||||
uri: 'https://hydro.js.org/schema/problemConfig.json',
|
uri: 'https://hydro.js.org/schema/problemConfig.json',
|
||||||
fileMatch: ['hydro://problem/file/config.yaml'],
|
fileMatch: ['hydro://problem/file/config.yaml'],
|
||||||
schema: problemConfigSchema,
|
schema: problemConfigSchema as any,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uri: `${UiContext.cdn_prefix}manage/setting/schema.json`,
|
uri: `${UiContext.cdn_prefix}manage/setting/schema.json`,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import request from 'vj/utils/request';
|
import request from 'vj/utils/request';
|
||||||
|
import Notification from 'vj/components/notification';
|
||||||
|
|
||||||
export default class ObjectiveContainer extends React.PureComponent {
|
export default class ObjectiveContainer extends React.PureComponent {
|
||||||
constructor(args) {
|
constructor(args) {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import i18n from 'vj/utils/i18n';
|
||||||
|
import type { RootState } from './reducer/index';
|
||||||
|
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||||
|
|
||||||
|
export function FormItem({
|
||||||
|
columns, label, children, helpText = '', disableLabel = false, ...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div {...props} className={`${columns && `medium-${columns}`} columns form__item`}>
|
||||||
|
<label htmlFor={`${label}-form`}>
|
||||||
|
{!disableLabel && i18n(label)}
|
||||||
|
{children}
|
||||||
|
{helpText && (<p className="help-text">{i18n(helpText)}</p>)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyType<K, T = string | number> = {
|
||||||
|
[Q in keyof K]: K[Q] extends T ? Q : never;
|
||||||
|
}[keyof K];
|
||||||
|
|
||||||
|
export function ManagedInput({ placeholder, formKey }: { placeholder?: string, formKey: KeyType<RootState['config']> }) {
|
||||||
|
const value = useSelector((state: RootState) => state.config[formKey]);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
placeholder={i18n(placeholder)}
|
||||||
|
value={value || ''}
|
||||||
|
type={typeof value === 'number' ? 'number' : 'text'}
|
||||||
|
onChange={(ev) => {
|
||||||
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: ev.currentTarget.value });
|
||||||
|
}}
|
||||||
|
className="textbox"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ManagedSelect({ options, formKey }: { options: string[], formKey: KeyType<RootState['config']> }) {
|
||||||
|
const value = useSelector((state: RootState) => state.config[formKey]);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
value={value || ''}
|
||||||
|
onChange={(ev) => {
|
||||||
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: ev.currentTarget.value });
|
||||||
|
}}
|
||||||
|
className="select"
|
||||||
|
>
|
||||||
|
{options.map((o) => (<option id={o} key={o}>{o}</option>))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SingleFileSelect({ formKey }: { formKey: KeyType<RootState['config']> }) {
|
||||||
|
const value = useSelector((state: RootState) => state.config[formKey]);
|
||||||
|
const Files = useSelector((state: RootState) => state.testdata);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<FileSelectAutoComplete
|
||||||
|
width="100%"
|
||||||
|
data={Files}
|
||||||
|
selectedKeys={[value]}
|
||||||
|
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: val })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { editor } from 'monaco-editor';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { load } from 'vj/components/monaco/loader';
|
||||||
|
import Editor from 'vj/components/editor';
|
||||||
|
import { diffLines } from 'diff';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import type { ProblemConfigFile, TestCaseConfig } from 'hydrooj/src/interface';
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
config: state.config,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
handleUpdateCode: (code) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'CONFIG_CODE_UPDATE',
|
||||||
|
payload: code,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
config: object;
|
||||||
|
handleUpdateCode: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configKey = [
|
||||||
|
'type', 'subType', 'target', 'score', 'time',
|
||||||
|
'memory', 'filename', 'checker_type', 'checker', 'interactor',
|
||||||
|
'user_extra_files', 'judge_extra_files', 'detail', 'outputs', 'redirect',
|
||||||
|
'cases', 'subtasks', 'langs',
|
||||||
|
];
|
||||||
|
|
||||||
|
const subtasksKey = [
|
||||||
|
'time', 'memory', 'score', 'if', 'id',
|
||||||
|
'type', 'cases',
|
||||||
|
];
|
||||||
|
|
||||||
|
const casesKey = ['time', 'memory', 'input', 'output'];
|
||||||
|
|
||||||
|
export function configYamlFormat(config: ProblemConfigFile) {
|
||||||
|
const formatConfig: ProblemConfigFile = {};
|
||||||
|
configKey.forEach((key) => {
|
||||||
|
if (config[key] !== undefined) {
|
||||||
|
if (key === 'subType' && ['single', 'multi'].includes(config[key]) && config.type !== 'submit_answer') return;
|
||||||
|
if (key === 'checker_type' && config.type !== 'default') return;
|
||||||
|
if (key === 'checker'
|
||||||
|
&& (['default', 'strict'].includes(formatConfig.checker_type) || !formatConfig.checker_type)) return;
|
||||||
|
if (key === 'interactor' && config.type !== 'interactive') return;
|
||||||
|
if (key === 'subtasks') {
|
||||||
|
formatConfig[key] = [];
|
||||||
|
config[key].forEach((subtask) => {
|
||||||
|
const formatSubtask: object = {};
|
||||||
|
subtasksKey.forEach((subtaskKey) => {
|
||||||
|
if (subtask && subtask[subtaskKey] !== undefined) {
|
||||||
|
formatSubtask[subtaskKey] = subtask[subtaskKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
formatConfig[key].push(formatSubtask);
|
||||||
|
});
|
||||||
|
} else if (key === 'cases') {
|
||||||
|
formatConfig[key] = [];
|
||||||
|
config[key].forEach((caseItem) => {
|
||||||
|
const formatCase: TestCaseConfig = {
|
||||||
|
time: '1000ms', memory: '256MB', input: '', output: '',
|
||||||
|
};
|
||||||
|
casesKey.forEach((caseKey) => {
|
||||||
|
if (caseItem[caseKey] !== undefined) formatCase[caseKey] = caseItem[caseKey];
|
||||||
|
else delete formatCase[caseKey];
|
||||||
|
});
|
||||||
|
formatConfig[key].push(formatCase);
|
||||||
|
});
|
||||||
|
} else formatConfig[key] = config[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (formatConfig.type === 'objective') {
|
||||||
|
Object.keys(formatConfig).filter((i) => !['type', 'outputs'].includes(i)).forEach((i) => delete formatConfig[i]);
|
||||||
|
formatConfig.outputs = formatConfig.outputs || [];
|
||||||
|
}
|
||||||
|
Object.keys(formatConfig).filter((i) => i.startsWith('__')).forEach((i) => delete formatConfig[i]);
|
||||||
|
return formatConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent<Props> {
|
||||||
|
disposable = [];
|
||||||
|
containerElement: HTMLElement;
|
||||||
|
private __preventUpdate = false;
|
||||||
|
private __preventFormat = false;
|
||||||
|
|
||||||
|
editor: editor.IStandaloneCodeEditor;
|
||||||
|
model: editor.ITextModel;
|
||||||
|
vjEditor: Editor;
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const { monaco } = await load(['yaml']);
|
||||||
|
const uri = monaco.Uri.parse('hydro://problem/file/config.yaml');
|
||||||
|
this.model = monaco.editor.createModel(yaml.dump(configYamlFormat(this.props.config)), 'yaml', uri);
|
||||||
|
this.vjEditor = Editor.getOrConstruct($(this.containerElement), {
|
||||||
|
language: 'yaml',
|
||||||
|
model: this.model,
|
||||||
|
onChange: (value: string) => {
|
||||||
|
this.__preventUpdate = true;
|
||||||
|
if (!this.__preventFormat) this.props.handleUpdateCode(value);
|
||||||
|
this.__preventUpdate = false;
|
||||||
|
},
|
||||||
|
}) as Editor;
|
||||||
|
this.editor = this.vjEditor.editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.__preventUpdate || !this.model) return;
|
||||||
|
if (yaml.dump(prevProps.config) !== yaml.dump(this.props.config)) {
|
||||||
|
this.__preventFormat = true;
|
||||||
|
const curValue = this.model.getValue();
|
||||||
|
const diff = diffLines(curValue, yaml.dump(configYamlFormat(this.props.config)));
|
||||||
|
const ops = [];
|
||||||
|
let cursor = 1;
|
||||||
|
for (const line of diff) {
|
||||||
|
if (line.added) {
|
||||||
|
let range = this.model.getFullModelRange();
|
||||||
|
range = range.setStartPosition(cursor, 0);
|
||||||
|
range = range.setEndPosition(cursor, 0);
|
||||||
|
ops.push({ range, text: line.value });
|
||||||
|
} else if (line.removed) {
|
||||||
|
let range = this.model.getFullModelRange();
|
||||||
|
range = range.setStartPosition(cursor, 0);
|
||||||
|
cursor += line.count;
|
||||||
|
range = range.setEndPosition(cursor, 0);
|
||||||
|
ops.push({ range, text: '' });
|
||||||
|
} else cursor += line.count;
|
||||||
|
}
|
||||||
|
this.model.pushEditOperations([], ops, undefined);
|
||||||
|
this.__preventFormat = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.vjEditor) this.vjEditor.destory();
|
||||||
|
if (this.model) this.model.dispose();
|
||||||
|
if (this.editor) this.editor.dispose();
|
||||||
|
this.disposable.map((i) => i.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
assignRef = (component) => {
|
||||||
|
this.containerElement = component;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={this.assignRef}
|
||||||
|
style={{
|
||||||
|
minHeight: '500px',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
className="ConfigMonacoEditor"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import i18n from 'vj/utils/i18n';
|
||||||
|
import {
|
||||||
|
Card, Tab, Tabs, InputGroup, Tag,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import type { RootState } from './reducer/index';
|
||||||
|
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
||||||
|
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||||
|
import { FormItem } from './BasicForm';
|
||||||
|
import ProblemType from './ProblemType';
|
||||||
|
import { TaskConfig } from './SubtaskTable';
|
||||||
|
|
||||||
|
function FileIOConfig() {
|
||||||
|
const filename = useSelector((state: RootState) => state.config.filename);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<FormItem columns={12} label="FileIOConfig" disableLabel>
|
||||||
|
<Card style={{ padding: 10 }}>
|
||||||
|
<div className="row">
|
||||||
|
<FormItem columns={6} label="FileIO">
|
||||||
|
<InputGroup
|
||||||
|
rightElement={<Tag minimal>.in/.out</Tag>}
|
||||||
|
value={filename || ''}
|
||||||
|
onChange={(ev) => {
|
||||||
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'filename', value: ev.currentTarget.value });
|
||||||
|
}}
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExtraFilesConfig() {
|
||||||
|
const Files = useSelector((state: RootState) => state.testdata);
|
||||||
|
const userExtraFiles = useSelector((state: RootState) => state.config.user_extra_files) || [];
|
||||||
|
const judgeExtraFiles = useSelector((state: RootState) => state.config.judge_extra_files) || [];
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const userRef = React.useRef<any>();
|
||||||
|
const judgeRef = React.useRef<any>();
|
||||||
|
React.useEffect(() => {
|
||||||
|
userRef.current.setSelectedKeys(userExtraFiles);
|
||||||
|
}, [JSON.stringify(userExtraFiles)]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
judgeRef.current.setSelectedKeys(judgeExtraFiles);
|
||||||
|
}, [JSON.stringify(judgeExtraFiles)]);
|
||||||
|
return (
|
||||||
|
<FormItem columns={12} label="ExtraFilesTabs" disableLabel>
|
||||||
|
<Tabs id="ExtraFilesTabs">
|
||||||
|
<Tab
|
||||||
|
id="user_extra_files"
|
||||||
|
title={i18n('user_extra_files')}
|
||||||
|
panel={(
|
||||||
|
<FileSelectAutoComplete
|
||||||
|
ref={userRef}
|
||||||
|
data={Files}
|
||||||
|
selectedKeys={userExtraFiles}
|
||||||
|
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'user_extra_files', value: val.split(',') })}
|
||||||
|
multi
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
id="judge_extra_files"
|
||||||
|
title={i18n('judge_extra_files')}
|
||||||
|
panel={(
|
||||||
|
<FileSelectAutoComplete
|
||||||
|
ref={judgeRef}
|
||||||
|
data={Files}
|
||||||
|
selectedKeys={judgeExtraFiles}
|
||||||
|
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'judge_extra_files', value: val.split(',') })}
|
||||||
|
multi
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LangConfig() {
|
||||||
|
const langs = useSelector((state: RootState) => state.config.langs) || [];
|
||||||
|
const prefixes = new Set(Object.keys(LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0]));
|
||||||
|
const data = Object.keys(LANGS).filter((i) => !prefixes.has(i))
|
||||||
|
.map((i) => ({ name: LANGS[i].display, _id: i }));
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const ref = React.useRef<any>();
|
||||||
|
const selectedKeys = langs.filter((i) => !prefixes.has(i));
|
||||||
|
React.useEffect(() => {
|
||||||
|
ref.current.setSelectedKeys(selectedKeys);
|
||||||
|
}, [JSON.stringify(selectedKeys)]);
|
||||||
|
return (
|
||||||
|
<FormItem columns={12} label="langs">
|
||||||
|
<CustomSelectAutoComplete
|
||||||
|
ref={ref}
|
||||||
|
data={data}
|
||||||
|
placeholder={i18n('Unlimited')}
|
||||||
|
selectedKeys={selectedKeys}
|
||||||
|
onChange={(val) => {
|
||||||
|
const value = val.split(',');
|
||||||
|
value.push(...Array.from(new Set(value.filter((i) => i.includes('.')).map((i) => i.split('.')[0]))));
|
||||||
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'langs', value });
|
||||||
|
}}
|
||||||
|
multi
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProblemConfigForm() {
|
||||||
|
const Type = useSelector((state: RootState) => state.config.type);
|
||||||
|
return (
|
||||||
|
<div className="row problem-config-form">
|
||||||
|
<ProblemType />
|
||||||
|
{Type === 'default' && <FileIOConfig />}
|
||||||
|
{!['submit_answer', 'objective'].includes(Type) && (
|
||||||
|
<>
|
||||||
|
<TaskConfig />
|
||||||
|
<ExtraFilesConfig />
|
||||||
|
<LangConfig />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import i18n from 'vj/utils/i18n';
|
||||||
|
import {
|
||||||
|
Card, Switch, Tab, Tabs,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import type { RootState } from './reducer/index';
|
||||||
|
import { FormItem, ManagedSelect, SingleFileSelect } from './BasicForm';
|
||||||
|
|
||||||
|
export default function ProblemType() {
|
||||||
|
const Type = useSelector((state: RootState) => state.config.type);
|
||||||
|
const checkerType = useSelector((state: RootState) => state.config.checker_type);
|
||||||
|
const filename = useSelector((state: RootState) => state.config.filename);
|
||||||
|
const subType = useSelector((state: RootState) => state.config.subType);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const dispatcher = (base) => (value) => dispatch({ ...base, value });
|
||||||
|
return (
|
||||||
|
<FormItem columns={12} label="" disableLabel>
|
||||||
|
<Card style={{ padding: 10 }}>
|
||||||
|
<Tabs
|
||||||
|
id="ProblemTypeTabs"
|
||||||
|
selectedTabId={Type}
|
||||||
|
onChange={dispatcher({ type: 'CONFIG_FORM_UPDATE', key: 'type' })}
|
||||||
|
defaultSelectedTabId="default"
|
||||||
|
renderActiveTabPanelOnly
|
||||||
|
>
|
||||||
|
<span className="bp4-tab">{i18n('Problem Type')}</span>
|
||||||
|
<Tabs.Expander />
|
||||||
|
<Tab
|
||||||
|
id="default"
|
||||||
|
title={i18n('problem_type.default')}
|
||||||
|
panel={(
|
||||||
|
<Tabs
|
||||||
|
id="CheckerTypeTabs"
|
||||||
|
selectedTabId={
|
||||||
|
['strict', 'default'].includes(checkerType) || !checkerType
|
||||||
|
? 'default' : (checkerType !== 'testlib' ? 'other' : 'testlib')
|
||||||
|
}
|
||||||
|
onChange={dispatcher({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type' })}
|
||||||
|
renderActiveTabPanelOnly
|
||||||
|
>
|
||||||
|
<span className="bp4-tab">{i18n('CheckerType')}</span>
|
||||||
|
<Tabs.Expander />
|
||||||
|
<Tab
|
||||||
|
id="default"
|
||||||
|
title={i18n('default')}
|
||||||
|
panel={(
|
||||||
|
<div className="row">
|
||||||
|
<FormItem columns={12} label="Config">
|
||||||
|
<Switch
|
||||||
|
checked={checkerType !== 'strict'}
|
||||||
|
label={i18n('Ignore trailing space and enter.')}
|
||||||
|
onChange={() => {
|
||||||
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{['testlib', 'other'].map((i) => (
|
||||||
|
<Tab
|
||||||
|
id={i}
|
||||||
|
title={i}
|
||||||
|
key={i}
|
||||||
|
panel={(
|
||||||
|
<div className="row">
|
||||||
|
{i === 'other' && (
|
||||||
|
<FormItem columns={6} label="Interface">
|
||||||
|
<ManagedSelect options={['syzoj', 'hustoj', 'qduoj', 'lemon']} formKey="checker_type" />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
<FormItem columns={6} label="Checker">
|
||||||
|
<SingleFileSelect formKey="checker" />
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
id="interactive"
|
||||||
|
title={i18n('problem_type.interactive')}
|
||||||
|
panel={(
|
||||||
|
<div className="row">
|
||||||
|
<FormItem columns={6} label="Interactor">
|
||||||
|
<SingleFileSelect formKey="interactor" />
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
id="submit_answer"
|
||||||
|
title={i18n('problem_type.submit_answer')}
|
||||||
|
panel={(
|
||||||
|
<div className="row">
|
||||||
|
<FormItem columns={6} label="Config" disableLabel>
|
||||||
|
<Switch
|
||||||
|
checked={subType === 'multi'}
|
||||||
|
label={i18n('Multi-file')}
|
||||||
|
onChange={() => {
|
||||||
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'subType', value: subType === 'multi' ? 'single' : 'multi' });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem columns={6} label="Filename">
|
||||||
|
<input
|
||||||
|
defaultValue={filename || '#.txt'}
|
||||||
|
placeholder="#.txt"
|
||||||
|
disabled={subType !== 'multi'}
|
||||||
|
onChange={(ev) => dispatch(({ type: 'CONFIG_FORM_UPDATE', key: 'filename', value: ev.currentTarget.value }))}
|
||||||
|
className="textbox"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
id="objective"
|
||||||
|
title={i18n('problem_type.objective')}
|
||||||
|
panel={(<p>{i18n('Unsupported configure this type of problem. Please refer to the documentation.')}</p>)}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { Card, NumericInput, Tag } from '@blueprintjs/core';
|
||||||
|
import { pick, isEqual } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { SubtaskConfig } from 'hydrooj/src/interface';
|
||||||
|
import { parseTimeMS, parseMemoryMB } from '@hydrooj/utils/lib/common';
|
||||||
|
import i18n from 'vj/utils/i18n';
|
||||||
|
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
||||||
|
import { FormItem } from './BasicForm';
|
||||||
|
import { RootState } from './reducer';
|
||||||
|
import { CasesTable } from './TestCasesTable';
|
||||||
|
|
||||||
|
const eq = (a: SubtaskConfig, b: SubtaskConfig) => isEqual(a, b);
|
||||||
|
|
||||||
|
export function SubtasksIds({ index }) {
|
||||||
|
const ids = useSelector((state: RootState) => state.config.subtasks.map((i) => (i ? i.id : undefined)).filter((i) => i !== undefined).join(','));
|
||||||
|
const subtaskIf = useSelector((state: RootState) => state.config.subtasks[index].if);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td style={{ textAlign: 'center' }}>if</td>
|
||||||
|
<td colSpan={4}>
|
||||||
|
<CustomSelectAutoComplete
|
||||||
|
data={ids.split(',').filter((i) => i.length)}
|
||||||
|
selectedKeys={subtaskIf?.map(String) || []}
|
||||||
|
onChange={(val) => dispatch({
|
||||||
|
type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'if', value: val.split(','),
|
||||||
|
})}
|
||||||
|
multi
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubtasksTable({ index }) {
|
||||||
|
const keys = ['id', 'type', 'score', 'time', 'memory'];
|
||||||
|
const subtask = useSelector((state: RootState) => pick(state.config.subtasks[index], keys), eq);
|
||||||
|
const defaultTime = useSelector((state: RootState) => state.config.time);
|
||||||
|
const defaultMemory = useSelector((state: RootState) => state.config.memory);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const dispatcher = (key: string, suffix = '') => (ev: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | number) => {
|
||||||
|
let value = typeof ev !== 'object' ? ev : ev.currentTarget?.value;
|
||||||
|
if (value === 0) value = '';
|
||||||
|
if (value && suffix) value += suffix;
|
||||||
|
dispatch({
|
||||||
|
type: 'CONFIG_SUBTASK_UPDATE', id: index, key, value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (!subtask || Object.keys(subtask).length === 0) {
|
||||||
|
return (
|
||||||
|
<Card style={{ padding: 0 }}>
|
||||||
|
<span>Subtasks #{index + 1} </span>
|
||||||
|
<p>{i18n('Failed to parse subtask.')}</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card style={{ padding: 0 }}>
|
||||||
|
<span>Subtasks #{index + 1} </span>
|
||||||
|
<a onClick={() => dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'add' })}><span className="icon icon-add"></span></a>
|
||||||
|
<a
|
||||||
|
style={index === 0 ? { display: 'none' } : {}}
|
||||||
|
onClick={() => dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'delete' })}
|
||||||
|
>
|
||||||
|
<span className="icon icon-delete"></span>
|
||||||
|
</a>
|
||||||
|
<table className="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Memory</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
{['id', 'score'].map((key) => (
|
||||||
|
<td key={key}>
|
||||||
|
<input
|
||||||
|
value={subtask[key] ?? ''}
|
||||||
|
onChange={dispatcher(key)}
|
||||||
|
type="number"
|
||||||
|
className="textbox"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
value={subtask.type ?? 'min'}
|
||||||
|
onChange={dispatcher('type')}
|
||||||
|
className="select"
|
||||||
|
>
|
||||||
|
{['min', 'max', 'sum'].map((i) => (<option value={i} key={i}>{i}</option>))}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<NumericInput
|
||||||
|
rightElement={<Tag minimal>ms</Tag>}
|
||||||
|
value={subtask.time ? parseTimeMS(subtask.time, false).toString() : ''}
|
||||||
|
placeholder={parseTimeMS(defaultTime || '1000ms', false).toString()}
|
||||||
|
onValueChange={dispatcher('time', 'ms')}
|
||||||
|
buttonPosition="none"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<NumericInput
|
||||||
|
rightElement={<Tag minimal>MB</Tag>}
|
||||||
|
value={subtask.memory ? parseMemoryMB(subtask.memory, false).toString() : ''}
|
||||||
|
placeholder={parseMemoryMB(defaultMemory || '256m', false).toString()}
|
||||||
|
onValueChange={dispatcher('memory', 'MB')}
|
||||||
|
buttonPosition="none"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<SubtasksIds index={index} />
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<CasesTable index={index} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GlobalTaskConfig() {
|
||||||
|
const time = useSelector((state: RootState) => state.config.time);
|
||||||
|
const memory = useSelector((state: RootState) => state.config.memory);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const dispatcher = (key: string, suffix = '') => (ev: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | number) => {
|
||||||
|
let value = typeof ev !== 'object' ? ev : ev.currentTarget?.value;
|
||||||
|
if (value === 0) value = '';
|
||||||
|
if (value && suffix) value += suffix;
|
||||||
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key, value });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormItem columns={6} label="Time">
|
||||||
|
<NumericInput
|
||||||
|
rightElement={<Tag minimal>ms</Tag>}
|
||||||
|
value={time ? parseTimeMS(time, false).toString() : ''}
|
||||||
|
placeholder={parseTimeMS('1000ms').toString()}
|
||||||
|
onValueChange={dispatcher('time', 'ms')}
|
||||||
|
buttonPosition="none"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem columns={6} label="Memory">
|
||||||
|
<NumericInput
|
||||||
|
rightElement={<Tag minimal>MB</Tag>}
|
||||||
|
value={memory ? parseMemoryMB(memory, false).toString() : ''}
|
||||||
|
placeholder={parseMemoryMB('256m').toString()}
|
||||||
|
onValueChange={dispatcher('memory', 'MB')}
|
||||||
|
buttonPosition="none"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskConfig() {
|
||||||
|
const len = useSelector((state: RootState) => state.config.subtasks?.length);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<FormItem columns={12} label="Task Settings">
|
||||||
|
<div className="row">
|
||||||
|
<GlobalTaskConfig />
|
||||||
|
<FormItem columns={12} label="TestCases" disableLabel>
|
||||||
|
{len > 0
|
||||||
|
? [...Array(len).keys()].map((i) => <SubtasksTable index={i} key={i} />)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<span>Subtasks # </span>
|
||||||
|
<a onClick={() => dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: 0, key: 'add' })}>
|
||||||
|
<span className="icon icon-add"></span>
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import i18n from 'vj/utils/i18n';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { Tag, NumericInput } from '@blueprintjs/core';
|
||||||
|
import { parseTimeMS, parseMemoryMB } from '@hydrooj/utils/lib/common';
|
||||||
|
import type { SubtaskConfig } from 'hydrooj/src/interface';
|
||||||
|
import type { RootState } from './reducer/index';
|
||||||
|
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||||
|
|
||||||
|
const eq = (a: SubtaskConfig, b: SubtaskConfig) => isEqual(a, b);
|
||||||
|
const eqArr = (a: any[], b: any[]) => isEqual(a, b);
|
||||||
|
|
||||||
|
export function TestCaseEntry({ index, subindex }) {
|
||||||
|
const testcase = useSelector((state: RootState) => state.config.subtasks[index].cases[subindex], eq);
|
||||||
|
const Files = useSelector((state: RootState) => state.testdata, eqArr);
|
||||||
|
const defaultTime = useSelector((state: RootState) => state.config.subtasks[index].time || state.config.time);
|
||||||
|
const defaultMemory = useSelector((state: RootState) => state.config.subtasks[index].memory || state.config.memory);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const dispatcher = (casesKey: string, valueSuffix = '') => (ev: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | number) => {
|
||||||
|
let value = typeof ev !== 'object' ? ev : ev.currentTarget.value;
|
||||||
|
if (value === 0) value = '';
|
||||||
|
if (valueSuffix && value) value += valueSuffix;
|
||||||
|
dispatch({
|
||||||
|
type: 'CONFIG_SUBTASK_UPDATE',
|
||||||
|
id: index,
|
||||||
|
key: 'cases-edit',
|
||||||
|
casesId: subindex,
|
||||||
|
casesKey,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (!testcase || Object.keys(testcase).length === 0) {
|
||||||
|
return (
|
||||||
|
<tr><td colSpan={5}>{i18n('Failed to parse testcase.')}</td></tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<NumericInput
|
||||||
|
rightElement={<Tag minimal>ms</Tag>}
|
||||||
|
value={testcase.time ? parseTimeMS(testcase.time, false).toString() : ''}
|
||||||
|
placeholder={parseTimeMS(defaultTime || '1000ms', false).toString()}
|
||||||
|
onValueChange={dispatcher('time', 'ms')}
|
||||||
|
buttonPosition="none"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<NumericInput
|
||||||
|
rightElement={<Tag minimal>MB</Tag>}
|
||||||
|
value={testcase.memory ? parseMemoryMB(testcase.memory, false).toString() : ''}
|
||||||
|
placeholder={parseMemoryMB(defaultMemory || '256m', false).toString()}
|
||||||
|
onValueChange={dispatcher('memory', 'MB')}
|
||||||
|
buttonPosition="none"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{['input', 'output'].map((t) => (
|
||||||
|
<td key={t}>
|
||||||
|
<FileSelectAutoComplete
|
||||||
|
width="100%"
|
||||||
|
data={Files}
|
||||||
|
selectedKeys={[testcase[t]]}
|
||||||
|
onChange={dispatcher(t)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="col--operation">
|
||||||
|
<a
|
||||||
|
onClick={() => dispatch({
|
||||||
|
type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: subindex,
|
||||||
|
})}
|
||||||
|
><span className="icon icon-close"></span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CasesTable({ index }) {
|
||||||
|
const len = useSelector((state: RootState) => state.config.subtasks[index].cases?.length);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<table className="data-table">
|
||||||
|
<thead style={{ display: 'none' }}>
|
||||||
|
<tr>
|
||||||
|
<th>{i18n('Time')}</th>
|
||||||
|
<th>{i18n('Memory')}</th>
|
||||||
|
<th>{i18n('Input')}</th>
|
||||||
|
<th>{i18n('Output')}</th>
|
||||||
|
<th className="col--operation">
|
||||||
|
<a
|
||||||
|
onClick={() => dispatch({
|
||||||
|
type: 'CONFIG_SUBTASK_UPDATE',
|
||||||
|
id: index,
|
||||||
|
key: 'cases-add',
|
||||||
|
value: { input: '', output: '' },
|
||||||
|
})}
|
||||||
|
><span className="icon icon-add" />
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="thead">
|
||||||
|
<td>{i18n('Time')}</td>
|
||||||
|
<td>{i18n('Memory')}</td>
|
||||||
|
<td>{i18n('Input')}</td>
|
||||||
|
<td>{i18n('Output')}</td>
|
||||||
|
<td className="col--operation">
|
||||||
|
<a
|
||||||
|
onClick={() => dispatch({
|
||||||
|
type: 'CONFIG_SUBTASK_UPDATE',
|
||||||
|
id: index,
|
||||||
|
key: 'cases-add',
|
||||||
|
value: { input: '', output: '' },
|
||||||
|
})}
|
||||||
|
><span className="icon icon-add" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{len > 0 && [...Array(len).keys()].map((i) => <TestCaseEntry index={index} subindex={i} key={i} />)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import yaml from 'js-yaml';
|
||||||
|
import type { ProblemConfigFile } from 'hydrooj/src/interface';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
|
||||||
|
|
||||||
|
type State = ProblemConfigFile & { __loaded: boolean };
|
||||||
|
|
||||||
|
export default function reducer(state = { type: 'default', __loaded: false } as State, action): State {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'CONFIG_LOAD_FULFILLED': {
|
||||||
|
return { ...state, ...yaml.load(action.payload.config) as object, __loaded: true };
|
||||||
|
}
|
||||||
|
case 'CONFIG_FORM_UPDATE': {
|
||||||
|
const next = { ...state, [action.key]: action.value };
|
||||||
|
if (action.key === 'score' && action.value) next.score = +next.score;
|
||||||
|
if (action.key === 'checker_type' && action.value === 'other') next.checker_type = 'syzoj';
|
||||||
|
if (!action.value || (typeof action.value === 'object' && !action.value.join(''))) delete next[action.key];
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
case 'CONFIG_CODE_UPDATE': {
|
||||||
|
try {
|
||||||
|
return { ...state, ...yaml.load(action.payload) as object };
|
||||||
|
} catch {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'CONFIG_AUTOCASES_UPDATE': {
|
||||||
|
const next = { ...state };
|
||||||
|
const { subtasks } = action;
|
||||||
|
for (const subtask of subtasks) {
|
||||||
|
if (subtask.time === parseTimeMS(state.time || '1s')) delete subtask.time;
|
||||||
|
if (subtask.memory === parseMemoryMB(state.memory || '256m')) delete subtask.memory;
|
||||||
|
if (subtask.time) subtask.time += 'ms';
|
||||||
|
if (subtask.memory) subtask.memory += 'MB';
|
||||||
|
}
|
||||||
|
if (subtasks.length === 0) next.subtasks = [];
|
||||||
|
else {
|
||||||
|
next.subtasks = subtasks.map((subtask) => (
|
||||||
|
{ ...subtask, ...{ cases: subtask.cases.map((i) => ({ input: i.input, output: i.output })) } }));
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
case 'CONFIG_SUBTASK_UPDATE': {
|
||||||
|
const subtasks = cloneDeep(state.subtasks);
|
||||||
|
const subsubtasks = cloneDeep(state.subtasks[action.id]);
|
||||||
|
if (action.value !== '' && ['score', 'id'].includes(action.key)) action.value = +action.value;
|
||||||
|
if (action.key === 'if' && action.value.join('') !== '') action.value = action.value.map((i) => +i);
|
||||||
|
if (action.key.split('-')[0] === 'cases') {
|
||||||
|
if (action.key === 'cases-add') subsubtasks.cases.push(action.value);
|
||||||
|
else if (action.key === 'cases-edit') {
|
||||||
|
if (action.value === '') delete subsubtasks.cases[action.casesId][action.casesKey];
|
||||||
|
else subsubtasks.cases[action.casesId][action.casesKey] = action.value;
|
||||||
|
} else if (action.key === 'cases-delete') {
|
||||||
|
subsubtasks.cases = subsubtasks.cases.filter((k, v) => v !== action.value);
|
||||||
|
}
|
||||||
|
} else if (action.key === 'add') {
|
||||||
|
return { ...state, subtasks: [...subtasks, { time: state.time || '1s', memory: state.memory || '256m', cases: [] }] };
|
||||||
|
} else if (action.key === 'delete') return { ...state, subtasks: subtasks.filter((k, v) => v !== action.id) };
|
||||||
|
else {
|
||||||
|
if (action.value === '' || (action.key === 'if' && action.value.join('') === '')) delete subsubtasks[action.key];
|
||||||
|
else subsubtasks[action.key] = action.value;
|
||||||
|
}
|
||||||
|
subtasks[action.id] = subsubtasks;
|
||||||
|
return { ...state, subtasks };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import config from './config';
|
||||||
|
import testdata from './testdata';
|
||||||
|
|
||||||
|
const reducer = combineReducers({
|
||||||
|
config,
|
||||||
|
testdata,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default reducer;
|
||||||
|
export type RootState = ReturnType<typeof reducer>;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export default function reducer(state = [], action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'CONFIG_LOAD_FULFILLED': {
|
||||||
|
return state.concat(action.payload.testdata);
|
||||||
|
}
|
||||||
|
case 'CONFIG_DELETE_TESTDATA': {
|
||||||
|
const next = state.filter((i) => i.name !== action.value[0]);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
case 'CONFIG_ADD_TESTDATA': {
|
||||||
|
return state.concat([action.value]);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,36 +5,36 @@ export default function reducer(state = {
|
|||||||
items: {},
|
items: {},
|
||||||
}, action) {
|
}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
|
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
|
||||||
const { rdocs } = action.payload;
|
const { rdocs } = action.payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
rows: _.map(rdocs, '_id'),
|
rows: _.map(rdocs, '_id'),
|
||||||
items: _.keyBy(rdocs, '_id'),
|
items: _.keyBy(rdocs, '_id'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SCRATCHPAD_RECORDS_PUSH': {
|
case 'SCRATCHPAD_RECORDS_PUSH': {
|
||||||
const { rdoc } = action.payload;
|
const { rdoc } = action.payload;
|
||||||
const rows = [...state.rows];
|
const rows = [...state.rows];
|
||||||
if (!rows.includes(rdoc._id)) {
|
if (!rows.includes(rdoc._id)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
rows: [rdoc._id, ...state.rows],
|
||||||
|
items: {
|
||||||
|
...state.items,
|
||||||
|
[rdoc._id]: rdoc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
rows: [rdoc._id, ...state.rows],
|
|
||||||
items: {
|
items: {
|
||||||
...state.items,
|
...state.items,
|
||||||
[rdoc._id]: rdoc,
|
[rdoc._id]: rdoc,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
default:
|
||||||
...state,
|
return state;
|
||||||
items: {
|
|
||||||
...state.items,
|
|
||||||
[rdoc._id]: rdoc,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,93 +19,93 @@ export default function reducer(state = {
|
|||||||
isPosting: false,
|
isPosting: false,
|
||||||
}, action) {
|
}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SCRATCHPAD_UI_CHANGE_SIZE': {
|
case 'SCRATCHPAD_UI_CHANGE_SIZE': {
|
||||||
const { uiElement, size } = action.payload;
|
const { uiElement, size } = action.payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[uiElement]: {
|
[uiElement]: {
|
||||||
...state[uiElement],
|
...state[uiElement],
|
||||||
size,
|
size,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
case 'SCRATCHPAD_UI_SET_VISIBILITY': {
|
|
||||||
const { uiElement, visibility } = action.payload;
|
|
||||||
localStorage.setItem(`scratchpad/${uiElement}`, visibility.toString());
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
[uiElement]: {
|
|
||||||
...state[uiElement],
|
|
||||||
visible: visibility,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': {
|
|
||||||
const { uiElement } = action.payload;
|
|
||||||
localStorage.setItem(`scratchpad/${uiElement}`, (!state[uiElement].visible).toString());
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
[uiElement]: {
|
|
||||||
...state[uiElement],
|
|
||||||
visible: !state[uiElement].visible,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'SCRATCHPAD_POST_PRETEST_PENDING':
|
|
||||||
case 'SCRATCHPAD_POST_SUBMIT_PENDING': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isPosting: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'SCRATCHPAD_POST_PRETEST_FULFILLED':
|
|
||||||
case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': {
|
|
||||||
Notification.success(i18n('Submitted.'));
|
|
||||||
if (action.type === 'SCRATCHPAD_POST_SUBMIT_FULFILLED' && UiContext.canViewRecord) {
|
|
||||||
state.records.visible = true;
|
|
||||||
}
|
}
|
||||||
return {
|
case 'SCRATCHPAD_UI_SET_VISIBILITY': {
|
||||||
...state,
|
const { uiElement, visibility } = action.payload;
|
||||||
isPosting: false,
|
localStorage.setItem(`scratchpad/${uiElement}`, visibility.toString());
|
||||||
};
|
return {
|
||||||
}
|
...state,
|
||||||
case 'SCRATCHPAD_POST_PRETEST_REJECTED':
|
[uiElement]: {
|
||||||
case 'SCRATCHPAD_POST_SUBMIT_REJECTED': {
|
...state[uiElement],
|
||||||
Notification.error(action.payload.message);
|
visible: visibility,
|
||||||
return {
|
},
|
||||||
...state,
|
};
|
||||||
isPosting: false,
|
}
|
||||||
};
|
case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': {
|
||||||
}
|
const { uiElement } = action.payload;
|
||||||
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': {
|
localStorage.setItem(`scratchpad/${uiElement}`, (!state[uiElement].visible).toString());
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
records: {
|
[uiElement]: {
|
||||||
...state.records,
|
...state[uiElement],
|
||||||
isLoading: true,
|
visible: !state[uiElement].visible,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': {
|
case 'SCRATCHPAD_POST_PRETEST_PENDING':
|
||||||
Notification.error(action.payload.message);
|
case 'SCRATCHPAD_POST_SUBMIT_PENDING': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
records: {
|
isPosting: true,
|
||||||
...state.records,
|
};
|
||||||
isLoading: false,
|
}
|
||||||
},
|
case 'SCRATCHPAD_POST_PRETEST_FULFILLED':
|
||||||
};
|
case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': {
|
||||||
}
|
Notification.success(i18n('Submitted.'));
|
||||||
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
|
if (action.type === 'SCRATCHPAD_POST_SUBMIT_FULFILLED' && UiContext.canViewRecord) {
|
||||||
return {
|
state.records.visible = true;
|
||||||
...state,
|
}
|
||||||
records: {
|
return {
|
||||||
...state.records,
|
...state,
|
||||||
isLoading: false,
|
isPosting: false,
|
||||||
},
|
};
|
||||||
};
|
}
|
||||||
}
|
case 'SCRATCHPAD_POST_PRETEST_REJECTED':
|
||||||
default:
|
case 'SCRATCHPAD_POST_SUBMIT_REJECTED': {
|
||||||
return state;
|
Notification.error(action.payload.message);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isPosting: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
records: {
|
||||||
|
...state.records,
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': {
|
||||||
|
Notification.error(action.payload.message);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
records: {
|
||||||
|
...state.records,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
records: {
|
||||||
|
...state.records,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ export default class Sock {
|
|||||||
const i = new URL(url, window.location.href);
|
const i = new URL(url, window.location.href);
|
||||||
i.protocol = i.protocol.replace('http', 'ws');
|
i.protocol = i.protocol.replace('http', 'ws');
|
||||||
this.url = i.toString();
|
this.url = i.toString();
|
||||||
this.closed = false;
|
|
||||||
this.sock = new ReconnectingWebSocket(this.url);
|
this.sock = new ReconnectingWebSocket(this.url);
|
||||||
this.sock.onopen = () => {
|
this.sock.onopen = () => {
|
||||||
console.log('Connected');
|
console.log('Connected');
|
||||||
@@ -13,17 +12,14 @@ export default class Sock {
|
|||||||
};
|
};
|
||||||
this.sock.onclose = ({ code, reason }) => {
|
this.sock.onclose = ({ code, reason }) => {
|
||||||
console.warn('Connection closed, ', code, reason);
|
console.warn('Connection closed, ', code, reason);
|
||||||
if (code >= 4000) this.closed = true;
|
if (code >= 4000) this.close();
|
||||||
this.onclose?.(code, reason);
|
this.onclose?.(code, reason);
|
||||||
};
|
};
|
||||||
this.sock.onmessage = (message) => {
|
this.sock.onmessage = (message) => {
|
||||||
if (process.env.NODE_ENV !== 'production') console.log('Sock.onmessage: ', message);
|
if (process.env.NODE_ENV !== 'production') console.log('Sock.onmessage: ', message);
|
||||||
const msg = JSON.parse(message.data);
|
const msg = JSON.parse(message.data);
|
||||||
if (msg.event === 'auth') {
|
if (msg.error === 'PermissionError' || msg.error === 'PrivilegeError') this.close();
|
||||||
if (msg.error === 'PermissionError' || msg.error === 'PrivilegeError') {
|
else this.onmessage?.(message);
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
} else this.onmessage?.(message);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +28,6 @@ export default class Sock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.closed = true;
|
|
||||||
this.sock?.close?.();
|
this.sock?.close?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,7 @@ export default async function download(filename, targets) {
|
|||||||
stream,
|
stream,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
window.captureException?.(e);
|
||||||
stopDownload();
|
stopDownload();
|
||||||
Notification.error(i18n('Download Error', [target.filename, e.toString()]));
|
Notification.error(i18n('Download Error', [target.filename, e.toString()]));
|
||||||
}
|
}
|
||||||
@@ -144,6 +145,7 @@ export async function downloadProblemSet(pids, name = 'Export') {
|
|||||||
}
|
}
|
||||||
await download(`${name}.zip`, targets);
|
await download(`${name}.zip`, targets);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
window.captureException?.(e);
|
||||||
Notification.error(`${e.message} ${e.params?.[0]}`);
|
Notification.error(`${e.message} ${e.params?.[0]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ window.Hydro = {
|
|||||||
components: {},
|
components: {},
|
||||||
utils: {},
|
utils: {},
|
||||||
node_modules: {},
|
node_modules: {},
|
||||||
|
version: process.env.VERSION,
|
||||||
};
|
};
|
||||||
window.externalModules = {};
|
window.externalModules = {};
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ async function load() {
|
|||||||
try {
|
try {
|
||||||
await func(currentPageName, loadPage(1, type));
|
await func(currentPageName, loadPage(1, type));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
(window as any).captureException?.(e);
|
||||||
Notification.warn(`Failed to call '${type}Loading' of ${page.name}`);
|
Notification.warn(`Failed to call '${type}Loading' of ${page.name}`);
|
||||||
console.error(`Failed to call '${type}Loading' of ${page.name}\n${e.stack}`);
|
console.error(`Failed to call '${type}Loading' of ${page.name}\n${e.stack}`);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ Attend Contest: 参加比赛
|
|||||||
Attend contests: 参加比赛
|
Attend contests: 参加比赛
|
||||||
Attended: 已参加
|
Attended: 已参加
|
||||||
author: 作者
|
author: 作者
|
||||||
|
Auto Read Tasks: 自动识别任务
|
||||||
Be Copied: 被复制
|
Be Copied: 被复制
|
||||||
Begin at: 开始于
|
Begin at: 开始于
|
||||||
Begin Date: 开始日期
|
Begin Date: 开始日期
|
||||||
@@ -117,6 +118,8 @@ Change Email: 修改电子邮件
|
|||||||
Change Password: 修改密码
|
Change Password: 修改密码
|
||||||
Change Theme: 切换主题
|
Change Theme: 切换主题
|
||||||
Check In: 签到
|
Check In: 签到
|
||||||
|
Checker: 比较器
|
||||||
|
CheckerType: 比较器类型
|
||||||
Chinese: 中文
|
Chinese: 中文
|
||||||
Choose the background image in your profile page.: 选择您资料页面的背景图片。
|
Choose the background image in your profile page.: 选择您资料页面的背景图片。
|
||||||
Claim homework: 认领作业
|
Claim homework: 认领作业
|
||||||
@@ -138,6 +141,7 @@ Compile Error: 编译错误
|
|||||||
Compilers' Version and Parameters: 编译器版本及参数
|
Compilers' Version and Parameters: 编译器版本及参数
|
||||||
Complete: 完成
|
Complete: 完成
|
||||||
Completed: 已完成
|
Completed: 已完成
|
||||||
|
Config: 配置
|
||||||
Confirm deleting the selected roles?: 您确定删除所选角色吗?
|
Confirm deleting the selected roles?: 您确定删除所选角色吗?
|
||||||
Confirm deleting this comment? Its replies will be deleted as well.: 确认删除这个评论吗?回复会被同时删除。
|
Confirm deleting this comment? Its replies will be deleted as well.: 确认删除这个评论吗?回复会被同时删除。
|
||||||
Confirm deleting this reply?: 确认删除这个回复吗?
|
Confirm deleting this reply?: 确认删除这个回复吗?
|
||||||
@@ -268,6 +272,7 @@ domain: 域
|
|||||||
Domain: 域
|
Domain: 域
|
||||||
Don't have an account?: 还没有账户?
|
Don't have an account?: 还没有账户?
|
||||||
Done: 已结束
|
Done: 已结束
|
||||||
|
Download All: 下载全部
|
||||||
Download Dataset: 下载数据集
|
Download Dataset: 下载数据集
|
||||||
Download Selected: 下载选中
|
Download Selected: 下载选中
|
||||||
Download: 下载
|
Download: 下载
|
||||||
@@ -318,7 +323,10 @@ Export as CSV: 导出为 CSV
|
|||||||
Export as HTML: 导出为 HTML
|
Export as HTML: 导出为 HTML
|
||||||
Extension (days): 最长延期 (日)
|
Extension (days): 最长延期 (日)
|
||||||
Extension Score Penalty: 延期递交扣分规则
|
Extension Score Penalty: 延期递交扣分规则
|
||||||
|
Extra Files Config: 额外文件设置
|
||||||
Failed to join the domain. You are already a member.: 加入域失败,您已是该域的成员。
|
Failed to join the domain. You are already a member.: 加入域失败,您已是该域的成员。
|
||||||
|
Failed to parse subtask.: 无法解析子任务信息。
|
||||||
|
Failed to parse testcase.: 无法解析测试点信息。
|
||||||
Feedback: 反馈
|
Feedback: 反馈
|
||||||
Field {0} or {1} validation failed.: 字段 {0} 或 {1} 验证失败。
|
Field {0} or {1} validation failed.: 字段 {0} 或 {1} 验证失败。
|
||||||
Field {0} validation failed.: 字段 {0} 验证失败。
|
Field {0} validation failed.: 字段 {0} 验证失败。
|
||||||
@@ -382,6 +390,7 @@ Host: 主持人
|
|||||||
hour(s): 小时
|
hour(s): 小时
|
||||||
If enabled, source code will be emailed to you after the submission is accepted.: 如果启用,在您通过题目后,源代码会以 Email 的形式发送给您。
|
If enabled, source code will be emailed to you after the submission is accepted.: 如果启用,在您通过题目后,源代码会以 Email 的形式发送给您。
|
||||||
If left blank, the built-in template of the corresponding language will be used.: 若留空,则将使用对应语言的内置代码模板。
|
If left blank, the built-in template of the corresponding language will be used.: 若留空,则将使用对应语言的内置代码模板。
|
||||||
|
Ignore trailing space and enter.: 忽略行末空格与文件尾回车。
|
||||||
Images With Size: 指定大小的图片
|
Images With Size: 指定大小的图片
|
||||||
Images: 图片
|
Images: 图片
|
||||||
Import from: 导入自
|
Import from: 导入自
|
||||||
@@ -400,6 +409,7 @@ Information: 信息
|
|||||||
Initialize: 初始化
|
Initialize: 初始化
|
||||||
Inline codes: 行内代码
|
Inline codes: 行内代码
|
||||||
Inline formulas: 行内公式
|
Inline formulas: 行内公式
|
||||||
|
Input: 输入
|
||||||
Insert Code: 插入代码
|
Insert Code: 插入代码
|
||||||
Insert Image: 插入图像
|
Insert Image: 插入图像
|
||||||
Installed Modules: 已安装的模块
|
Installed Modules: 已安装的模块
|
||||||
@@ -414,12 +424,16 @@ Is rated: 计入排名
|
|||||||
It works!: 它成了!
|
It works!: 它成了!
|
||||||
Italic: 斜体
|
Italic: 斜体
|
||||||
Join: 加入
|
Join: 加入
|
||||||
|
Judge Config: 评测设置
|
||||||
Judge Status: 评测状态
|
Judge Status: 评测状态
|
||||||
|
judge_extra_files: 评测额外文件
|
||||||
judge_playground: 评测游乐场
|
judge_playground: 评测游乐场
|
||||||
Judged At: 评测时间
|
Judged At: 评测时间
|
||||||
Judged By: 评测机
|
Judged By: 评测机
|
||||||
Judging Queue: 评测队列
|
Judging Queue: 评测队列
|
||||||
Keep current expiration: 保持当前过期设置
|
Keep current expiration: 保持当前过期设置
|
||||||
|
Langs Config: 语言设置
|
||||||
|
langs: 语言
|
||||||
Language: 语言
|
Language: 语言
|
||||||
last active at: 最后活动于
|
last active at: 最后活动于
|
||||||
last login at: 最后登录于
|
last login at: 最后登录于
|
||||||
@@ -531,6 +545,7 @@ Or, with automatically filled invitation code: 或者,这是可以自动填写
|
|||||||
Ordered List: 有序列表
|
Ordered List: 有序列表
|
||||||
Original Score: 原始分数
|
Original Score: 原始分数
|
||||||
OS Info: 系统信息
|
OS Info: 系统信息
|
||||||
|
Output: 输出
|
||||||
Over {0} problems: 超过 {0} 道题目
|
Over {0} problems: 超过 {0} 道题目
|
||||||
Owner: 所有者
|
Owner: 所有者
|
||||||
page.problem_detail.sidebar.show_category: 点击显示
|
page.problem_detail.sidebar.show_category: 点击显示
|
||||||
@@ -582,6 +597,8 @@ Problem Solution List: 题解列表
|
|||||||
Problem Solution: 题解列表
|
Problem Solution: 题解列表
|
||||||
Problem Tags Visibility: 题目标签可见性
|
Problem Tags Visibility: 题目标签可见性
|
||||||
Problem Tags: 题目标签
|
Problem Tags: 题目标签
|
||||||
|
Problem Type: 题目类型
|
||||||
|
problem_config: 评测设置
|
||||||
problem_create: 创建题目
|
problem_create: 创建题目
|
||||||
problem_edit: 编辑题目
|
problem_edit: 编辑题目
|
||||||
problem_import: 导入题目
|
problem_import: 导入题目
|
||||||
@@ -755,6 +772,7 @@ SuperUser: 超级管理员
|
|||||||
Support: 支持
|
Support: 支持
|
||||||
Switch account: 切换账户
|
Switch account: 切换账户
|
||||||
Tags: 标签
|
Tags: 标签
|
||||||
|
Task Settings: 任务设置
|
||||||
tasks_list: 任务列表
|
tasks_list: 任务列表
|
||||||
Technical Information: 技术信息
|
Technical Information: 技术信息
|
||||||
Temporary session: 临时会话
|
Temporary session: 临时会话
|
||||||
@@ -811,9 +829,11 @@ Uncompleted: 未完成
|
|||||||
Unique: 唯一
|
Unique: 唯一
|
||||||
Unknown field {0}.: 未知字段 {0}。
|
Unknown field {0}.: 未知字段 {0}。
|
||||||
Unknown: 未知
|
Unknown: 未知
|
||||||
|
Unlimited: 无限制
|
||||||
Unordered List: 无序列表
|
Unordered List: 无序列表
|
||||||
Unstar Topic: 取消收藏
|
Unstar Topic: 取消收藏
|
||||||
Unstar: 取消收藏
|
Unstar: 取消收藏
|
||||||
|
Unsupported configure this type of problem. Please refer to the documentation.: 暂不支持快捷配置此类题目。请参照文档操作。
|
||||||
Update Permission: 更新权限
|
Update Permission: 更新权限
|
||||||
Update Settings: 更新设置
|
Update Settings: 更新设置
|
||||||
Update: 更新
|
Update: 更新
|
||||||
@@ -830,12 +850,14 @@ Usage exceeded.: 用量超限。
|
|||||||
Use admin specificed: 由管理员指定
|
Use admin specificed: 由管理员指定
|
||||||
Use algorithm calculated: 使用算法计算
|
Use algorithm calculated: 使用算法计算
|
||||||
Use average of above: 使用上面的平均值
|
Use average of above: 使用上面的平均值
|
||||||
|
Use Subtasks Mode: 使用子任务模式
|
||||||
User {0} already exists.: 用户 {0} 已存在。
|
User {0} already exists.: 用户 {0} 已存在。
|
||||||
User {0} not found.: 用户 {0} 不存在。
|
User {0} not found.: 用户 {0} 不存在。
|
||||||
User can join this domain by visiting the following URL: 用户可以访问此链接来加入此域
|
User can join this domain by visiting the following URL: 用户可以访问此链接来加入此域
|
||||||
User ID: 用户 ID
|
User ID: 用户 ID
|
||||||
User will no longer be allowed to join the domain after expiration.: 过期后,用户将不能加入此域。
|
User will no longer be allowed to join the domain after expiration.: 过期后,用户将不能加入此域。
|
||||||
user_detail: 用户
|
user_detail: 用户
|
||||||
|
user_extra_files: 编译额外文件
|
||||||
user_login: 登录
|
user_login: 登录
|
||||||
user_logout: 登出
|
user_logout: 登出
|
||||||
user_lostpass: 忘记密码
|
user_lostpass: 忘记密码
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/ui-default",
|
"name": "@hydrooj/ui-default",
|
||||||
"version": "4.35.0",
|
"version": "4.37.6",
|
||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "hydro.js",
|
"main": "hydro.js",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
"preferUnplugged": true,
|
"preferUnplugged": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint ."
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blueprintjs/core": "^3.54.0",
|
"@blueprintjs/core": "^4.3.0",
|
||||||
"@fontsource/dm-mono": "^4.5.7",
|
"@fontsource/dm-mono": "^4.5.8",
|
||||||
"@fontsource/fira-code": "^4.5.8",
|
"@fontsource/fira-code": "^4.5.9",
|
||||||
"@fontsource/inconsolata": "^4.5.4",
|
"@fontsource/inconsolata": "^4.5.6",
|
||||||
"@fontsource/jetbrains-mono": "^4.5.5",
|
"@fontsource/jetbrains-mono": "^4.5.8",
|
||||||
"@fontsource/pt-mono": "^4.5.5",
|
"@fontsource/pt-mono": "^4.5.8",
|
||||||
"@fontsource/roboto-mono": "^4.5.5",
|
"@fontsource/roboto-mono": "^4.5.7",
|
||||||
"@fontsource/source-code-pro": "^4.5.6",
|
"@fontsource/source-code-pro": "^4.5.9",
|
||||||
"@fontsource/ubuntu-mono": "^4.5.6",
|
"@fontsource/ubuntu-mono": "^4.5.9",
|
||||||
"@hydrooj/utils": "workspace:*",
|
"@hydrooj/utils": "workspace:*",
|
||||||
"@milkdown/core": "^6.0.1",
|
"@milkdown/core": "^6.0.1",
|
||||||
"@milkdown/plugin-clipboard": "^6.0.1",
|
"@milkdown/plugin-clipboard": "^6.0.1",
|
||||||
@@ -42,8 +42,8 @@
|
|||||||
"@types/json-schema": "^7.0.11",
|
"@types/json-schema": "^7.0.11",
|
||||||
"@types/katex": "^0.14.0",
|
"@types/katex": "^0.14.0",
|
||||||
"@types/qrcode": "^1.4.2",
|
"@types/qrcode": "^1.4.2",
|
||||||
|
"@types/react-dom": "^18.0.3",
|
||||||
"@types/redux-logger": "^3.0.9",
|
"@types/redux-logger": "^3.0.9",
|
||||||
"@undefined-moe/monaco-yaml": "^2.5.1",
|
|
||||||
"ansi_up": "^5.1.0",
|
"ansi_up": "^5.1.0",
|
||||||
"autoprefixer": "^9.8.8",
|
"autoprefixer": "^9.8.8",
|
||||||
"browser-update": "^3.3.38",
|
"browser-update": "^3.3.38",
|
||||||
@@ -51,19 +51,19 @@
|
|||||||
"chunk-progress-webpack-plugin": "^2.0.1",
|
"chunk-progress-webpack-plugin": "^2.0.1",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"clipboard": "^2.0.10",
|
"clipboard": "^2.0.11",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"css-loader": "^4.3.0",
|
"css-loader": "^4.3.0",
|
||||||
|
"diff": "^5.0.0",
|
||||||
"diff-dom": "^4.2.3",
|
"diff-dom": "^4.2.3",
|
||||||
"echarts": "^5.3.2",
|
"echarts": "^5.3.2",
|
||||||
"emojis-keywords": "2.0.0",
|
"emojis-keywords": "2.0.0",
|
||||||
"emojis-list": "2.1.0",
|
"emojis-list": "2.1.0",
|
||||||
"esbuild-loader": "^2.18.0",
|
"esbuild-loader": "^2.18.0",
|
||||||
"eslint": "^8.12.0",
|
|
||||||
"fancy-log": "^2.0.0",
|
"fancy-log": "^2.0.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"friendly-errors-webpack-plugin": "^1.7.0",
|
"friendly-errors-webpack-plugin": "^1.7.0",
|
||||||
"graphiql": "^1.8.3",
|
"graphiql": "^1.8.9",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-cli": "^2.3.0",
|
"gulp-cli": "^2.3.0",
|
||||||
"gulp-iconfont": "^11.0.1",
|
"gulp-iconfont": "^11.0.1",
|
||||||
@@ -76,31 +76,32 @@
|
|||||||
"jquery.transit": "^0.9.12",
|
"jquery.transit": "^0.9.12",
|
||||||
"material-icons": "^1.10.9",
|
"material-icons": "^1.10.9",
|
||||||
"mini-css-extract-plugin": "^1.6.2",
|
"mini-css-extract-plugin": "^1.6.2",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.3",
|
||||||
"monaco-editor": "^0.31.1",
|
"monaco-editor": "^0.33.0",
|
||||||
"monaco-editor-nls": "^2.0.0",
|
"monaco-editor-nls": "^2.0.0",
|
||||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||||
"monaco-themes": "^0.4.0",
|
"monaco-themes": "^0.4.2",
|
||||||
"nanoid": "^3.3.2",
|
"monaco-yaml": "^4.0.0-alpha.3",
|
||||||
|
"nanoid": "^3.3.4",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pickadate": "^3.6.4",
|
"pickadate": "^3.6.4",
|
||||||
"plugin-error": "^1.0.1",
|
"plugin-error": "^1.0.1",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"prism-themes": "^1.9.0",
|
"prism-themes": "^1.9.0",
|
||||||
"prismjs": "^1.27.0",
|
"prismjs": "^1.28.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"qrcode": "^1.5.0",
|
"qrcode": "^1.5.0",
|
||||||
"queue-microtask": "^1.2.3",
|
"queue-microtask": "^1.2.3",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^17.0.2",
|
"react": "^18.1.0",
|
||||||
"react-dnd": "^16.0.0",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.0",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.1.0",
|
||||||
"react-redux": "^7.2.8",
|
"react-redux": "^8.0.1",
|
||||||
"react-split-pane": "^0.1.92",
|
"react-split-pane": "^0.1.92",
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"redux": "^4.1.2",
|
"redux": "^4.2.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"redux-promise-middleware": "^6.1.2",
|
"redux-promise-middleware": "^6.1.2",
|
||||||
"redux-thunk": "^2.4.1",
|
"redux-thunk": "^2.4.1",
|
||||||
@@ -118,7 +119,7 @@
|
|||||||
"timeago-react": "^3.0.4",
|
"timeago-react": "^3.0.4",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
"web-streams-polyfill": "^3.2.0",
|
"web-streams-polyfill": "^3.2.1",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
"webpack-bundle-analyzer": "^4.5.0",
|
"webpack-bundle-analyzer": "^4.5.0",
|
||||||
"webpack-dev-server": "^3.11.3",
|
"webpack-dev-server": "^3.11.3",
|
||||||
@@ -126,13 +127,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "0.14.3",
|
"esbuild": "0.14.3",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.1.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsesc": "^3.0.2",
|
"jsesc": "^3.0.2",
|
||||||
"katex": "^0.15.3",
|
"katex": "^0.15.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-it": "^12.3.2",
|
"markdown-it": "^13.0.1",
|
||||||
"markdown-it-anchor": "^8.4.1",
|
"markdown-it-anchor": "^8.6.4",
|
||||||
"markdown-it-footnote": "^3.0.3",
|
"markdown-it-footnote": "^3.0.3",
|
||||||
"markdown-it-imsize": "^2.0.1",
|
"markdown-it-imsize": "^2.0.1",
|
||||||
"markdown-it-mark": "^3.0.1",
|
"markdown-it-mark": "^3.0.1",
|
||||||
|
|||||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user