mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-11-09 16:38:41 +00:00
feat: Remote Judge for Luogu (#34)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
commit
dfc57f13d3
42
remote_judger/package-lock.json
generated
42
remote_judger/package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"crlf-normalize": "^1.0.18",
|
||||
"fs-extra": "^11.1.0",
|
||||
"jsdom": "^21.0.0",
|
||||
"lodash.flattendeep": "^4.4.0",
|
||||
"math-sum": "^2.0.0",
|
||||
"reggol": "^1.3.4",
|
||||
"superagent": "^8.0.6",
|
||||
@ -21,6 +22,7 @@
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsdom": "^20.0.1",
|
||||
"@types/lodash.flattendeep": "^4.4.7",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/superagent": "^4.1.16",
|
||||
"@types/superagent-proxy": "^3.0.0",
|
||||
@ -78,6 +80,21 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash.flattendeep": {
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.7.tgz",
|
||||
"integrity": "sha512-1h6GW/AeZw/Wej6uxrqgmdTDZX1yFS39lRsXYkg+3kWvOWWrlGCI6H7lXxlUHOzxDT4QeYGmgPpQ3BX9XevzOg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
@ -790,6 +807,11 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.flattendeep": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
|
||||
"integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@ -1567,6 +1589,21 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash.flattendeep": {
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.7.tgz",
|
||||
"integrity": "sha512-1h6GW/AeZw/Wej6uxrqgmdTDZX1yFS39lRsXYkg+3kWvOWWrlGCI6H7lXxlUHOzxDT4QeYGmgPpQ3BX9XevzOg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
@ -2113,6 +2150,11 @@
|
||||
"type-check": "~0.3.2"
|
||||
}
|
||||
},
|
||||
"lodash.flattendeep": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
|
||||
"integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ=="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
|
@ -15,6 +15,7 @@
|
||||
"crlf-normalize": "^1.0.18",
|
||||
"fs-extra": "^11.1.0",
|
||||
"jsdom": "^21.0.0",
|
||||
"lodash.flattendeep": "^4.4.0",
|
||||
"math-sum": "^2.0.0",
|
||||
"reggol": "^1.3.4",
|
||||
"superagent": "^8.0.6",
|
||||
@ -24,6 +25,7 @@
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsdom": "^20.0.1",
|
||||
"@types/lodash.flattendeep": "^4.4.7",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/superagent": "^4.1.16",
|
||||
"@types/superagent-proxy": "^3.0.0",
|
||||
|
@ -7,6 +7,7 @@ import * as TIME from './utils/time';
|
||||
import { apply } from './vjudge';
|
||||
import path from 'path';
|
||||
import child from 'child_process';
|
||||
import htmlspecialchars from './utils/htmlspecialchars';
|
||||
|
||||
proxy(superagent);
|
||||
|
||||
@ -146,7 +147,8 @@ export default async function daemon(config: UOJConfig) {
|
||||
config.remote_problem_id,
|
||||
config.answer_language,
|
||||
code,
|
||||
judge_time
|
||||
judge_time,
|
||||
config
|
||||
);
|
||||
} catch (err) {
|
||||
await request('/submit', {
|
||||
@ -157,7 +159,7 @@ export default async function daemon(config: UOJConfig) {
|
||||
status: 'Judged',
|
||||
score: 0,
|
||||
error: 'Judgment Failed',
|
||||
details: `<error>No details.</error>`,
|
||||
details: `<error>${htmlspecialchars(err.message)}</error>`,
|
||||
}),
|
||||
judge_time,
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
export interface RemoteAccount {
|
||||
type: string;
|
||||
cookie?: string[];
|
||||
handle: string;
|
||||
password: string;
|
||||
handle?: string;
|
||||
password?: string;
|
||||
endpoint?: string;
|
||||
proxy?: string;
|
||||
}
|
||||
|
@ -73,6 +73,10 @@ export default class AtcoderProvider implements IBasicProvider {
|
||||
this.account.endpoint ||= 'https://atcoder.jp';
|
||||
}
|
||||
|
||||
static constructFromAccountData(data) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
cookie: string[] = ['language=en'];
|
||||
csrf: string;
|
||||
|
||||
|
@ -87,6 +87,10 @@ export default class CodeforcesProvider implements IBasicProvider {
|
||||
this.account.endpoint ||= 'https://codeforces.com';
|
||||
}
|
||||
|
||||
static constructFromAccountData(data) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
cookie: string[] = [];
|
||||
csrf: string;
|
||||
|
||||
|
@ -156,6 +156,10 @@ export default class LibreojProvider implements IBasicProvider {
|
||||
this.account.endpoint ||= 'https://api.loj.ac.cn/api';
|
||||
}
|
||||
|
||||
static constructFromAccountData(data) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
get(url: string) {
|
||||
logger.debug('get', url);
|
||||
if (!url.includes('//')) url = `${this.account.endpoint}${url}`;
|
||||
|
350
remote_judger/src/providers/luogu.ts
Normal file
350
remote_judger/src/providers/luogu.ts
Normal file
@ -0,0 +1,350 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import superagent from 'superagent';
|
||||
import proxy from 'superagent-proxy';
|
||||
import Logger from '../utils/logger';
|
||||
import { IBasicProvider, RemoteAccount, USER_AGENT } from '../interface';
|
||||
import sleep from '../utils/sleep';
|
||||
import flattenDeep from 'lodash.flattendeep';
|
||||
import htmlspecialchars from '../utils/htmlspecialchars';
|
||||
|
||||
proxy(superagent);
|
||||
const logger = new Logger('remote/luogu');
|
||||
|
||||
const STATUS_MAP = [
|
||||
'Waiting', // WAITING,
|
||||
'Judging', // JUDGING,
|
||||
'Compile Error', // CE
|
||||
'Output Limit Exceeded', // OLE
|
||||
'Memory Limit Exceeded', // MLE
|
||||
'Time Limit Exceeded', // TLE
|
||||
'Wrong Answer', // WA
|
||||
'Runtime Error', // RE
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
'Judgment Failed', // UKE
|
||||
'Accepted', // AC
|
||||
0,
|
||||
'Wrong Answer', // WA
|
||||
];
|
||||
|
||||
const LANGS_MAP = {
|
||||
C: {
|
||||
id: 2,
|
||||
name: 'C',
|
||||
comment: '//',
|
||||
},
|
||||
'C++98': {
|
||||
id: 3,
|
||||
name: 'C++98',
|
||||
comment: '//',
|
||||
},
|
||||
'C++11': {
|
||||
id: 4,
|
||||
name: 'C++11',
|
||||
comment: '//',
|
||||
},
|
||||
'C++': {
|
||||
id: 11,
|
||||
name: 'C++14',
|
||||
comment: '//',
|
||||
},
|
||||
'C++17': {
|
||||
id: 12,
|
||||
name: 'C++17',
|
||||
comment: '//',
|
||||
},
|
||||
'C++20': {
|
||||
id: 27,
|
||||
name: 'C++20',
|
||||
comment: '//',
|
||||
},
|
||||
Python3: {
|
||||
id: 7,
|
||||
name: 'Python 3',
|
||||
comment: '#',
|
||||
},
|
||||
Java8: {
|
||||
id: 8,
|
||||
name: 'Java 8',
|
||||
comment: '//',
|
||||
},
|
||||
Pascal: {
|
||||
id: 1,
|
||||
name: 'Pascal',
|
||||
comment: '//',
|
||||
},
|
||||
};
|
||||
|
||||
function buildLuoguTestCaseInfoBlock(test) {
|
||||
let res = '';
|
||||
|
||||
res += `<test num="${test.id + 1}" info="${STATUS_MAP[test.status]}" time="${
|
||||
test.time || -1
|
||||
}" memory="${test.memory || -1}" score="${test.score || ''}">`;
|
||||
res += `<res>${htmlspecialchars(test.description || '')}</res>`;
|
||||
res += '</test>';
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export default class LuoguProvider implements IBasicProvider {
|
||||
constructor(public account: RemoteAccount) {
|
||||
if (account.cookie) this.cookie = account.cookie;
|
||||
}
|
||||
|
||||
static constructFromAccountData(data) {
|
||||
return new this({
|
||||
type: 'luogu',
|
||||
cookie: Object.entries(data).map(([key, value]) => `${key}=${value}`),
|
||||
});
|
||||
}
|
||||
|
||||
cookie: string[] = [];
|
||||
csrf: string;
|
||||
|
||||
get(url: string) {
|
||||
logger.debug('get', url, this.cookie);
|
||||
|
||||
if (!url.includes('//'))
|
||||
url = `${this.account.endpoint || 'https://www.luogu.com.cn'}${url}`;
|
||||
|
||||
const req = superagent
|
||||
.get(url)
|
||||
.set('Cookie', this.cookie)
|
||||
.set('User-Agent', USER_AGENT);
|
||||
|
||||
if (this.account.proxy) return req.proxy(this.account.proxy);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
async safeGet(url: string) {
|
||||
const res = await this.get(url);
|
||||
|
||||
if (res.text.startsWith('<html><script>document.location.reload()')) {
|
||||
const sec = this.getCookie.call(
|
||||
{ cookie: res.header['set-cookie'] },
|
||||
'sec'
|
||||
);
|
||||
this.setCookie('sec', sec);
|
||||
logger.debug('sec', sec);
|
||||
|
||||
return await this.get(url);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
post(url: string) {
|
||||
logger.debug('post', url, this.cookie);
|
||||
|
||||
if (!url.includes('//'))
|
||||
url = `${this.account.endpoint || 'https://www.luogu.com.cn'}${url}`;
|
||||
|
||||
const req = superagent
|
||||
.post(url)
|
||||
.set('Cookie', this.cookie)
|
||||
.set('x-csrf-token', this.csrf)
|
||||
.set('User-Agent', USER_AGENT)
|
||||
.set('x-requested-with', 'XMLHttpRequest')
|
||||
.set('origin', 'https://www.luogu.com.cn');
|
||||
|
||||
if (this.account.proxy) return req.proxy(this.account.proxy);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
getCookie(target: string) {
|
||||
return this.cookie
|
||||
.find(i => i.startsWith(`${target}=`))
|
||||
?.split('=')[1]
|
||||
?.split(';')[0];
|
||||
}
|
||||
|
||||
setCookie(target: string, value: string) {
|
||||
this.cookie = this.cookie.filter(i => !i.startsWith(`${target}=`));
|
||||
this.cookie.push(`${target}=${value}`);
|
||||
}
|
||||
|
||||
async getCsrfToken(url: string) {
|
||||
let { text: html } = await this.safeGet(url);
|
||||
|
||||
const $dom = new JSDOM(html);
|
||||
|
||||
this.csrf = $dom.window.document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
.getAttribute('content');
|
||||
|
||||
logger.info('csrf-token=', this.csrf);
|
||||
}
|
||||
|
||||
get loggedIn() {
|
||||
return this.safeGet('/user/setting?_contentOnly=1').then(
|
||||
({ body }) => body.currentTemplate !== 'AuthLogin'
|
||||
);
|
||||
}
|
||||
|
||||
async ensureLogin() {
|
||||
if (await this.loggedIn) {
|
||||
await this.getCsrfToken('/user/setting');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.info('retry login');
|
||||
|
||||
// TODO login;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async submitProblem(
|
||||
id: string,
|
||||
lang: string,
|
||||
code: string,
|
||||
submissionId: number,
|
||||
next,
|
||||
end
|
||||
) {
|
||||
if (!(await this.ensureLogin())) {
|
||||
end({
|
||||
error: true,
|
||||
status: 'Judgment Failed',
|
||||
message: 'Login failed',
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (code.length < 10) {
|
||||
end({
|
||||
error: true,
|
||||
status: 'Compile Error',
|
||||
message: 'Code too short',
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const programType = LANGS_MAP[lang] || LANGS_MAP['C++'];
|
||||
const comment = programType.comment;
|
||||
|
||||
if (comment) {
|
||||
const msg = `S2OJ Submission #${submissionId} @ ${new Date().getTime()}`;
|
||||
if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`;
|
||||
else if (comment instanceof Array)
|
||||
code = `${comment[0]} ${msg} ${comment[1]}\n${code}`;
|
||||
}
|
||||
|
||||
const result = await this.post(`/fe/api/problem/submit/${id}`)
|
||||
.set('referer', `https://www.luogu.com.cn/problem/${id}`)
|
||||
.send({
|
||||
code,
|
||||
lang: programType.id,
|
||||
enableO2: 1,
|
||||
});
|
||||
|
||||
logger.info('RecordID:', result.body.rid);
|
||||
|
||||
return result.body.rid;
|
||||
}
|
||||
|
||||
async waitForSubmission(problem_id: string, id: string, next, end) {
|
||||
let fail = 0;
|
||||
let count = 0;
|
||||
|
||||
while (count < 120 && fail < 5) {
|
||||
await sleep(1500);
|
||||
count++;
|
||||
|
||||
try {
|
||||
const { body } = await this.safeGet(`/record/${id}?_contentOnly=1`);
|
||||
const data = body.currentData.record;
|
||||
|
||||
if (
|
||||
data.detail.compileResult &&
|
||||
data.detail.compileResult.success === false
|
||||
) {
|
||||
return await end({
|
||||
error: true,
|
||||
id: `R${id}`,
|
||||
status: 'Compile Error',
|
||||
message: data.detail.compileResult.message,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('Fetched with length', JSON.stringify(body).length);
|
||||
const total = flattenDeep(
|
||||
Object.entries(body.currentData.testCaseGroup || {}).map(o => o[1])
|
||||
).length;
|
||||
|
||||
if (!data.detail.judgeResult?.subtasks) continue;
|
||||
|
||||
await next({
|
||||
status: `Judging (${
|
||||
data.detail.judgeResult?.finishedCaseCount || '?'
|
||||
}/${total})`,
|
||||
});
|
||||
|
||||
if (data.status < 2) continue;
|
||||
|
||||
logger.info('RecordID:', id, 'done');
|
||||
|
||||
const status = STATUS_MAP[data.status];
|
||||
let details = '';
|
||||
|
||||
details += `<info-block>REMOTE_SUBMISSION_ID = ${id}\nVERDICT = ${status}</info-block>`;
|
||||
|
||||
if (data.detail.judgeResult.subtasks.length === 1) {
|
||||
details += Object.entries(
|
||||
data.detail.judgeResult.subtasks[0].testCases
|
||||
)
|
||||
.map(o => o[1])
|
||||
.map(buildLuoguTestCaseInfoBlock)
|
||||
.join('\n');
|
||||
} else {
|
||||
details += Object.entries(data.detail.judgeResult.subtasks)
|
||||
.map(o => o[1])
|
||||
.map(
|
||||
(subtask: any, index) =>
|
||||
`<subtask num="${index}" info="${
|
||||
STATUS_MAP[subtask.status]
|
||||
}" time="${subtask.time || -1}" memory="${
|
||||
subtask.memory || -1
|
||||
}" score="${subtask.score || ''}">${Object.entries(
|
||||
subtask.testCases
|
||||
)
|
||||
.map(o => o[1])
|
||||
.map(buildLuoguTestCaseInfoBlock)
|
||||
.join('\n')}</subtask>`
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return await end({
|
||||
id: `R${id}`,
|
||||
status,
|
||||
score:
|
||||
status === 'Accepted'
|
||||
? 100
|
||||
: (data.score / data.problem.fullScore) * 100,
|
||||
time: data.time,
|
||||
memory: data.memory,
|
||||
details: `<div>${details}</div>`,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
return await end({
|
||||
error: true,
|
||||
id: `R${id}`,
|
||||
status: 'Judgment Failed',
|
||||
message: 'Failed to fetch submission details.',
|
||||
});
|
||||
}
|
||||
}
|
@ -99,6 +99,10 @@ export default class UOJProvider implements IBasicProvider {
|
||||
if (account.cookie) this.cookie = account.cookie;
|
||||
}
|
||||
|
||||
static constructFromAccountData(data) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
cookie: string[] = [];
|
||||
csrf: string;
|
||||
|
||||
|
@ -8,11 +8,7 @@ const logger = new Logger('vjudge');
|
||||
class AccountService {
|
||||
api: IBasicProvider;
|
||||
|
||||
constructor(
|
||||
public Provider: BasicProvider,
|
||||
public account: RemoteAccount,
|
||||
private request: any
|
||||
) {
|
||||
constructor(public Provider: BasicProvider, public account: RemoteAccount) {
|
||||
this.api = new Provider(account);
|
||||
this.main().catch(e =>
|
||||
logger.error(`Error occured in ${account.type}/${account.handle}`, e)
|
||||
@ -24,7 +20,81 @@ class AccountService {
|
||||
problem_id: string,
|
||||
language: string,
|
||||
code: string,
|
||||
judge_time: string
|
||||
next,
|
||||
end
|
||||
) {
|
||||
try {
|
||||
const rid = await this.api.submitProblem(
|
||||
problem_id,
|
||||
language,
|
||||
code,
|
||||
id,
|
||||
next,
|
||||
end
|
||||
);
|
||||
|
||||
if (!rid) return;
|
||||
|
||||
await this.api.waitForSubmission(problem_id, rid, next, end);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
||||
await end({ error: true, status: 'Judgment Failed', message: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
async login() {
|
||||
const login = await this.api.ensureLogin();
|
||||
if (login === true) {
|
||||
logger.info(`${this.account.type}/${this.account.handle}: logged in`);
|
||||
return true;
|
||||
}
|
||||
logger.warn(
|
||||
`${this.account.type}/${this.account.handle}: login fail`,
|
||||
login || ''
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
async main() {
|
||||
const res = await this.login();
|
||||
if (!res) return;
|
||||
setInterval(() => this.login(), Time.hour);
|
||||
}
|
||||
}
|
||||
|
||||
class VJudge {
|
||||
private p_imports: Record<string, any> = {};
|
||||
private providers: Record<string, AccountService> = {};
|
||||
|
||||
constructor(private request: any) {}
|
||||
|
||||
async importProvider(type: string) {
|
||||
if (this.p_imports[type]) throw new Error(`duplicate provider ${type}`);
|
||||
const provider = await import(`./providers/${type}`);
|
||||
|
||||
this.p_imports[type] = provider.default;
|
||||
}
|
||||
|
||||
async addProvider(type: string) {
|
||||
if (this.p_imports[type]) throw new Error(`duplicate provider ${type}`);
|
||||
const provider = await import(`./providers/${type}`);
|
||||
const account = provider.getAccountInfoFromEnv();
|
||||
|
||||
if (!account) throw new Error(`no account info for ${type}`);
|
||||
|
||||
this.p_imports[type] = provider.default;
|
||||
this.providers[type] = new AccountService(provider.default, account);
|
||||
}
|
||||
|
||||
async judge(
|
||||
id: number,
|
||||
type: string,
|
||||
problem_id: string,
|
||||
language: string,
|
||||
code: string,
|
||||
judge_time: string,
|
||||
config
|
||||
) {
|
||||
const next = async payload => {
|
||||
return await this.request('/submit', {
|
||||
@ -69,16 +139,35 @@ class AccountService {
|
||||
details:
|
||||
payload.details ||
|
||||
'<div>' +
|
||||
`<info-block>ID = ${payload.id || 'None'}</info-block>` +
|
||||
`<info-block>VERDICT = ${payload.status}</info-block>` +
|
||||
`<info-block>REMOTE_SUBMISSION_ID = ${
|
||||
payload.id || 'None'
|
||||
}\nVERDICT = ${payload.status}</info-block>` +
|
||||
'</div>',
|
||||
}),
|
||||
judge_time,
|
||||
});
|
||||
};
|
||||
|
||||
if (!config.remote_submit_type || config.remote_submit_type == 'bot') {
|
||||
if (!this.providers[type]) throw new Error(`No provider ${type}`);
|
||||
|
||||
await this.providers[type].judge(
|
||||
id,
|
||||
problem_id,
|
||||
language,
|
||||
code,
|
||||
next,
|
||||
end
|
||||
);
|
||||
} else if (config.remote_submit_type == 'my') {
|
||||
if (!this.p_imports[type]) throw new Error(`No provider ${type}`);
|
||||
|
||||
try {
|
||||
const rid = await this.api.submitProblem(
|
||||
const provider = this.p_imports[type].constructFromAccountData(
|
||||
JSON.parse(config.remote_account_data)
|
||||
);
|
||||
|
||||
const rid = await provider.submitProblem(
|
||||
problem_id,
|
||||
language,
|
||||
code,
|
||||
@ -89,63 +178,21 @@ class AccountService {
|
||||
|
||||
if (!rid) return;
|
||||
|
||||
await this.api.waitForSubmission(problem_id, rid, next, end);
|
||||
await provider.waitForSubmission(problem_id, rid, next, end);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
await end({ error: true, status: 'Judgment Failed', message: e.message });
|
||||
|
||||
await end({
|
||||
error: true,
|
||||
status: 'Judgment Failed',
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async login() {
|
||||
const login = await this.api.ensureLogin();
|
||||
if (login === true) {
|
||||
logger.info(`${this.account.type}/${this.account.handle}: logged in`);
|
||||
return true;
|
||||
}
|
||||
logger.warn(
|
||||
`${this.account.type}/${this.account.handle}: login fail`,
|
||||
login || ''
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
async main() {
|
||||
const res = await this.login();
|
||||
if (!res) return;
|
||||
setInterval(() => this.login(), Time.hour);
|
||||
}
|
||||
}
|
||||
|
||||
class VJudge {
|
||||
private providers: Record<string, AccountService> = {};
|
||||
|
||||
constructor(private request: any) {}
|
||||
|
||||
async addProvider(type: string) {
|
||||
if (this.providers[type]) throw new Error(`duplicate provider ${type}`);
|
||||
const provider = await import(`./providers/${type}`);
|
||||
const account = provider.getAccountInfoFromEnv();
|
||||
|
||||
if (!account) throw new Error(`no account info for ${type}`);
|
||||
|
||||
this.providers[type] = new AccountService(
|
||||
provider.default,
|
||||
account,
|
||||
this.request
|
||||
} else {
|
||||
throw new Error(
|
||||
'Unsupported remote submit type: ' + config.remote_submit_type
|
||||
);
|
||||
}
|
||||
|
||||
async judge(
|
||||
id: number,
|
||||
type: string,
|
||||
problem_id: string,
|
||||
language: string,
|
||||
code: string,
|
||||
judge_time: string
|
||||
) {
|
||||
if (!this.providers[type]) throw new Error(`no provider ${type}`);
|
||||
|
||||
await this.providers[type].judge(id, problem_id, language, code, judge_time);
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,6 +203,7 @@ export async function apply(request: any) {
|
||||
await vjudge.addProvider('atcoder');
|
||||
await vjudge.addProvider('uoj');
|
||||
await vjudge.addProvider('loj');
|
||||
await vjudge.importProvider('luogu');
|
||||
|
||||
return vjudge;
|
||||
}
|
||||
|
@ -51,6 +51,16 @@ $new_remote_problem_form->addInput('remote_problem_id', [
|
||||
|
||||
$vdata['remote_problem_id'] = $id;
|
||||
|
||||
return '';
|
||||
} else if ($remote_oj === 'luogu') {
|
||||
$id = trim(strtoupper($id));
|
||||
|
||||
if (!validateLuoguProblemId($id)) {
|
||||
return '不合法的题目 ID';
|
||||
}
|
||||
|
||||
$vdata['remote_problem_id'] = $id;
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -73,14 +83,7 @@ $new_remote_problem_form->handle = function (&$vdata) {
|
||||
UOJResponse::page500('题目抓取失败,可能是题目不存在或者没有题面!如果题目没有问题,请稍后再试。<a href="">返回</a>');
|
||||
}
|
||||
|
||||
$submission_requirement = [
|
||||
[
|
||||
"name" => "answer",
|
||||
"type" => "source code",
|
||||
"file_name" => "answer.code",
|
||||
"languages" => $remote_provider['languages'],
|
||||
]
|
||||
];
|
||||
$submission_requirement = UOJRemoteProblem::getSubmissionRequirements($remote_online_judge);
|
||||
$enc_submission_requirement = json_encode($submission_requirement);
|
||||
|
||||
$extra_config = [
|
||||
|
@ -104,6 +104,19 @@ $custom_test_enabled = $custom_test_requirement && $pre_submit_check_ret === tru
|
||||
|
||||
function handleUpload($zip_file_name, $content, $tot_size) {
|
||||
global $is_participating;
|
||||
|
||||
$remote_oj = UOJProblem::cur()->getExtraConfig('remote_online_judge');
|
||||
$remote_provider = UOJRemoteProblem::$providers[$remote_oj];
|
||||
|
||||
if (UOJProblem::info('type') == 'remote') {
|
||||
$submit_type = in_array($_POST['answer_remote_submit_type'], $remote_provider['submit_type']) ? $_POST['answer_remote_submit_type'] : $remote_provider['submit_type'][0];
|
||||
if ($submit_type != 'bot') {
|
||||
$content['no_rejudge'] = true;
|
||||
}
|
||||
$content['config'][] = ['remote_submit_type', $submit_type];
|
||||
$content['config'][] = ['remote_account_data', $_POST['answer_remote_account_data']];
|
||||
}
|
||||
|
||||
UOJSubmission::onUpload($zip_file_name, $content, $tot_size, $is_participating);
|
||||
}
|
||||
function handleCustomTestUpload($zip_file_name, $content, $tot_size) {
|
||||
@ -182,7 +195,7 @@ if ($pre_submit_check_ret === true && !$no_more_submission) {
|
||||
|
||||
if (UOJProblem::cur()->userCanUploadSubmissionViaZip(Auth::user())) {
|
||||
$zip_answer_form = newZipSubmissionForm(
|
||||
'zip-answer',
|
||||
'zip_answer',
|
||||
$submission_requirement,
|
||||
'FS::randomAvailableSubmissionFileName',
|
||||
'handleUpload'
|
||||
@ -198,6 +211,24 @@ if ($pre_submit_check_ret === true && !$no_more_submission) {
|
||||
'FS::randomAvailableSubmissionFileName',
|
||||
'handleUpload'
|
||||
);
|
||||
|
||||
if (UOJProblem::info('type') == 'remote') {
|
||||
$remote_oj = UOJProblem::cur()->getExtraConfig('remote_online_judge');
|
||||
$remote_pid = UOJProblem::cur()->getExtraConfig('remote_problem_id');
|
||||
$remote_url = UOJRemoteProblem::getProblemRemoteUrl($remote_oj, $remote_pid);
|
||||
$submit_type = json_encode(UOJRemoteProblem::$providers[$remote_oj]['submit_type']);
|
||||
|
||||
$answer_form->addNoVal('answer_remote_submit_type', '');
|
||||
$answer_form->addNoVal('answer_remote_account_data', '');
|
||||
$answer_form->appendHTML(<<<EOD
|
||||
<h5>Remote Judge 配置</h5>
|
||||
<div class="" id="answer-remote_submit_group"></div>
|
||||
<script>
|
||||
$('#answer-remote_submit_group').remote_submit_type_group("{$remote_oj}", "{$remote_pid}", "{$remote_url}", {$submit_type});
|
||||
</script>
|
||||
EOD);
|
||||
}
|
||||
|
||||
$answer_form->extra_validator = $submission_extra_validator;
|
||||
$answer_form->succ_href = $is_participating ? '/contest/' . UOJContest::info('id') . '/submissions' : '/submissions';
|
||||
$answer_form->runAtServer();
|
||||
|
@ -118,7 +118,7 @@ if (UOJProblem::info('type') == 'remote') {
|
||||
</ul>
|
||||
EOD);
|
||||
$re_crawl_form->config['submit_button']['text'] = '重新爬取';
|
||||
$re_crawl_form->handle = function () use ($remote_online_judge, $remote_problem_id, $remote_provider) {
|
||||
$re_crawl_form->handle = function () use ($remote_online_judge, $remote_problem_id) {
|
||||
try {
|
||||
$data = UOJRemoteProblem::getProblemBasicInfo($remote_online_judge, $remote_problem_id);
|
||||
} catch (Exception $e) {
|
||||
@ -134,14 +134,7 @@ if (UOJProblem::info('type') == 'remote') {
|
||||
$data['difficulty'] = UOJProblem::info('difficulty');
|
||||
}
|
||||
|
||||
$submission_requirement = [
|
||||
[
|
||||
"name" => "answer",
|
||||
"type" => "source code",
|
||||
"file_name" => "answer.code",
|
||||
"languages" => $remote_provider['languages'],
|
||||
]
|
||||
];
|
||||
$submission_requirement = UOJRemoteProblem::getSubmissionRequirements($remote_online_judge);
|
||||
$enc_submission_requirement = json_encode($submission_requirement);
|
||||
|
||||
$extra_config = [
|
||||
|
@ -46,6 +46,7 @@ function newAddDelCmdForm($form_name, $validate, $handle, $final = null) {
|
||||
|
||||
function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle) {
|
||||
$form = new UOJForm($form_name);
|
||||
|
||||
foreach ($requirement as $req) {
|
||||
if ($req['type'] == "source code") {
|
||||
$languages = UOJLang::getAvailableLanguages(isset($req['languages']) ? $req['languages'] : null);
|
||||
@ -74,6 +75,7 @@ function newSubmissionForm($form_name, $requirement, $zip_file_name_gen, $handle
|
||||
$content = [];
|
||||
$content['file_name'] = $zip_file_name;
|
||||
$content['config'] = [];
|
||||
|
||||
foreach ($requirement as $req) {
|
||||
if ($req['type'] == "source code") {
|
||||
$content['config'][] = ["{$req['name']}_language", $_POST["{$form_name}_{$req['name']}_language"]];
|
||||
|
@ -403,7 +403,7 @@ function echoSubmissionContent($submission, $requirement) {
|
||||
echo '</div>';
|
||||
echo '<div class="card-footer">' . $footer_text . '</div>';
|
||||
echo '</div>';
|
||||
} elseif ($req['type'] == "text") {
|
||||
} else if ($req['type'] == "text") {
|
||||
$file_content = $zip_file->getFromName("{$req['file_name']}", 504);
|
||||
$file_content = strOmit($file_content, 500);
|
||||
$file_content = uojTextEncode($file_content, array('allow_CR' => true, 'html_escape' => true));
|
||||
|
@ -79,3 +79,7 @@ function is_short_string($str) {
|
||||
function validateCodeforcesProblemId($str) {
|
||||
return preg_match('/(|GYM)[1-9][0-9]{0,5}[A-Z][1-9]?/', $str) !== true;
|
||||
}
|
||||
|
||||
function validateLuoguProblemId($str) {
|
||||
return preg_match('/P[1-9][0-9]{4,5}/', $str) !== true;
|
||||
}
|
||||
|
@ -665,6 +665,7 @@ class UOJForm {
|
||||
EOD;
|
||||
} else {
|
||||
echo <<<EOD
|
||||
$("#button-submit-{$this->form_name}").addClass('disabled');
|
||||
return ok;
|
||||
EOD;
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ class UOJRemoteProblem {
|
||||
'ограничение по времени на тест',
|
||||
],
|
||||
'languages' => ['C', 'C++', 'C++17', 'C++20', 'Java17', 'Pascal', 'Python2', 'Python3'],
|
||||
'submit_type' => ['bot'],
|
||||
],
|
||||
'atcoder' => [
|
||||
'name' => 'AtCoder',
|
||||
@ -24,6 +25,7 @@ class UOJRemoteProblem {
|
||||
'指定されたタスクが見つかりません',
|
||||
],
|
||||
'languages' => ['C', 'C++', 'Java11', 'Python3', 'Pascal'],
|
||||
'submit_type' => ['bot'],
|
||||
],
|
||||
'uoj' => [
|
||||
'name' => 'UniversalOJ',
|
||||
@ -33,12 +35,21 @@ class UOJRemoteProblem {
|
||||
'未找到该页面',
|
||||
],
|
||||
'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java8', 'Java11', 'Java17', 'Pascal'],
|
||||
'submit_type' => ['bot'],
|
||||
],
|
||||
'loj' => [
|
||||
'name' => 'LibreOJ',
|
||||
'short_name' => 'LOJ',
|
||||
'url' => 'https://loj.ac',
|
||||
'languages' => ['C', 'C++03', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Python2.7', 'Java17', 'Pascal'],
|
||||
'submit_type' => ['bot'],
|
||||
],
|
||||
'luogu' => [
|
||||
'name' => '洛谷',
|
||||
'short_name' => '洛谷',
|
||||
'url' => 'https://www.luogu.com.cn',
|
||||
'languages' => ['C', 'C++98', 'C++11', 'C++', 'C++17', 'C++20', 'Python3', 'Java8', 'Pascal'],
|
||||
'submit_type' => ['my'],
|
||||
],
|
||||
];
|
||||
|
||||
@ -90,6 +101,10 @@ class UOJRemoteProblem {
|
||||
return static::$providers['loj']['url'] . '/p/' . $id;
|
||||
}
|
||||
|
||||
static function getLuoguProblemUrl($id) {
|
||||
return static::$providers['luogu']['url'] . '/problem/' . $id;
|
||||
}
|
||||
|
||||
static function getCodeforcesProblemBasicInfoFromHtml($id, $html) {
|
||||
$remote_provider = static::$providers['codeforces'];
|
||||
|
||||
@ -394,6 +409,72 @@ class UOJRemoteProblem {
|
||||
];
|
||||
}
|
||||
|
||||
static function getLuoguProblemBasicInfo($id) {
|
||||
$remote_provider = static::$providers['luogu'];
|
||||
$res = static::curl_get(static::getLuoguProblemUrl($id) . '?_contentOnly=1');
|
||||
|
||||
if (!$res) return null;
|
||||
|
||||
// Convert stdClass to array
|
||||
$res = json_decode(json_encode($res['response']), true);
|
||||
|
||||
if (!isset($res['code']) || $res['code'] != 200) return null;
|
||||
|
||||
$problem = $res['currentData']['problem'];
|
||||
$statement = '';
|
||||
|
||||
if ($problem['background']) {
|
||||
$statement .= "\n### 题目背景\n\n";
|
||||
$statement .= $problem['background'] . "\n";
|
||||
}
|
||||
|
||||
$statement .= "\n### 题目描述\n\n";
|
||||
$statement .= $problem['description'] . "\n";
|
||||
|
||||
$statement .= "\n### 输入格式\n\n";
|
||||
$statement .= $problem['inputFormat'] . "\n";
|
||||
|
||||
$statement .= "\n### 输出格式\n\n";
|
||||
$statement .= $problem['outputFormat'] . "\n";
|
||||
|
||||
$statement .= "\n### 输入输出样例\n\n";
|
||||
|
||||
foreach ($problem['samples'] as $id => $sample) {
|
||||
$display_sample_id = $id + 1;
|
||||
|
||||
$statement .= "\n#### 样例输入 #{$display_sample_id}\n\n";
|
||||
$statement .= "\n```text\n{$sample[0]}\n```\n\n";
|
||||
|
||||
$statement .= "\n#### 样例输出 #{$display_sample_id}\n\n";
|
||||
$statement .= "\n```text\n{$sample[1]}\n```\n\n";
|
||||
}
|
||||
|
||||
$statement .= "\n### 说明/提示\n\n";
|
||||
$statement .= $problem['hint'] . "\n";
|
||||
|
||||
return [
|
||||
'type' => 'html',
|
||||
'title' => "【{$remote_provider['short_name']}{$problem['pid']}】{$problem['title']}",
|
||||
'time_limit' => (float)max($problem['limits']['time']) / 1000.0,
|
||||
'memory_limit' => (float)max($problem['limits']['memory']) / 1024.0,
|
||||
'difficulty' => -1,
|
||||
'statement' => HTML::parsedown()->text($statement),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getSubmissionRequirements($oj) {
|
||||
$remote_provider = UOJRemoteProblem::$providers[$oj];
|
||||
|
||||
return [
|
||||
[
|
||||
"name" => "answer",
|
||||
"type" => "source code",
|
||||
"file_name" => "answer.code",
|
||||
"languages" => $remote_provider['languages'],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static function getProblemRemoteUrl($oj, $id) {
|
||||
if ($oj === 'codeforces') {
|
||||
return static::getCodeforcesProblemUrl($id);
|
||||
@ -403,6 +484,8 @@ class UOJRemoteProblem {
|
||||
return static::getUojProblemUrl($id);
|
||||
} else if ($oj === 'loj') {
|
||||
return static::getLojProblemUrl($id);
|
||||
} else if ($oj === 'luogu') {
|
||||
return static::getLuoguProblemUrl($id);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -418,6 +501,8 @@ class UOJRemoteProblem {
|
||||
return static::getUojProblemBasicInfo($id);
|
||||
} else if ($oj === 'loj') {
|
||||
return static::getLojProblemBasicInfo($id);
|
||||
} else if ($oj === 'luogu') {
|
||||
return static::getLuoguProblemBasicInfo($id);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -418,6 +418,10 @@ class UOJSubmission {
|
||||
}
|
||||
|
||||
public function userCanRejudge(array $user = null) {
|
||||
if ($this->getContent('no_rejudge')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSuperUser($user)) {
|
||||
return true;
|
||||
}
|
||||
|
104
web/js/uoj.js
104
web/js/uoj.js
@ -947,6 +947,110 @@ $.fn.text_file_form_group = function(name, text) {
|
||||
});
|
||||
}
|
||||
|
||||
// remote judge submit type group
|
||||
$.fn.remote_submit_type_group = function(oj, pid, url, submit_type) {
|
||||
return this.each(function() {
|
||||
var input_submit_type_bot_id = 'input-submit_type_bot';
|
||||
var input_submit_type_my_id = 'input-submit_type_my';
|
||||
var div_submit_type_bot_id = 'div-submit_type_bot';
|
||||
var div_submit_type_my_id = 'div-submit_type_my';
|
||||
|
||||
var input_submit_type_bot = $('<input class="form-check-input" type="radio" name="answer_remote_submit_type" id="' + input_submit_type_bot_id + '" value="bot" />');
|
||||
var input_submit_type_my = $('<input class="form-check-input" type="radio" name="answer_remote_submit_type" id="' + input_submit_type_my_id + '" value="my" />');
|
||||
var input_my_account_data = $('<input type="hidden" name="answer_remote_account_data" value="" />');
|
||||
|
||||
var div_submit_type_bot = $('<div id="' + div_submit_type_bot_id + '" />')
|
||||
.append('<div class="mt-3">将使用公用账号提交本题。</div>');
|
||||
var div_submit_type_my = $('<div id="' + div_submit_type_my_id + '" />')
|
||||
.append('<div class="mt-3">将使用您的账号提交本题。</div>');
|
||||
|
||||
input_submit_type_bot.click(function() {
|
||||
div_submit_type_my.hide('fast');
|
||||
div_submit_type_bot.show('fast');
|
||||
});
|
||||
input_submit_type_my.click(function() {
|
||||
div_submit_type_bot.hide('fast');
|
||||
div_submit_type_my.show('fast');
|
||||
});
|
||||
|
||||
if (submit_type[0] == 'bot') {
|
||||
div_submit_type_my.hide();
|
||||
input_submit_type_bot[0].checked = true;
|
||||
} else if (submit_type[0] == 'my') {
|
||||
div_submit_type_bot.hide();
|
||||
input_submit_type_my[0].checked = true;
|
||||
}
|
||||
|
||||
if (submit_type.indexOf('bot') == -1) {
|
||||
input_submit_type_bot.attr('disabled', 'disabled');
|
||||
}
|
||||
if (submit_type.indexOf('my') == -1) {
|
||||
input_submit_type_my.attr('disabled', 'disabled');
|
||||
}
|
||||
|
||||
if (oj == 'luogu') {
|
||||
var luogu_account_data = {"_uid": "", "__client_id": ""};
|
||||
var input_luogu_uid = $('<input class="form-control" type="text" name="luogu_uid" id="input-luogu_uid" />');
|
||||
var input_luogu_client_id = $('<input class="form-control" type="text" name="luogu_client_id" id="input-luogu_client_id" />');
|
||||
|
||||
if ('localStorage' in window) {
|
||||
try {
|
||||
var luogu_account_data_str = localStorage.getItem('uoj_remote_judge_luogu_account_data');
|
||||
if (luogu_account_data_str) {
|
||||
luogu_account_data = JSON.parse(luogu_account_data_str);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
var save_luogu_account_data = function() {
|
||||
localStorage.setItem('uoj_remote_judge_luogu_account_data', JSON.stringify(luogu_account_data));
|
||||
}
|
||||
} else {
|
||||
var save_luogu_account_data = function() {};
|
||||
}
|
||||
|
||||
input_luogu_uid.change(function() {
|
||||
luogu_account_data._uid = $(this).val();
|
||||
input_my_account_data.val(JSON.stringify(luogu_account_data));
|
||||
save_luogu_account_data();
|
||||
});
|
||||
|
||||
input_luogu_client_id.change(function() {
|
||||
luogu_account_data.__client_id = $(this).val();
|
||||
input_my_account_data.val(JSON.stringify(luogu_account_data));
|
||||
save_luogu_account_data();
|
||||
});
|
||||
|
||||
input_my_account_data.val(JSON.stringify(luogu_account_data));
|
||||
input_luogu_uid.val(luogu_account_data._uid);
|
||||
input_luogu_client_id.val(luogu_account_data.__client_id);
|
||||
|
||||
div_submit_type_my.append(
|
||||
$('<div class="row mt-3" />')
|
||||
.append($('<div class="col-sm-2" />').append('<label for="input-luogu_uid" class="form-col-label">_uid</label>'))
|
||||
.append($('<div class="col-sm-4" />').append(input_luogu_uid))
|
||||
.append($('<div class="col-sm-6" />').append($('<div class="form-text" />').append('请填入 Cookie 中的 <code>_uid</code>。')))
|
||||
).append(
|
||||
$('<div class="row mt-3" />')
|
||||
.append($('<div class="col-sm-2" />').append('<label for="input-luogu_client_id" class="form-col-label">__client_id</label>'))
|
||||
.append($('<div class="col-sm-4" />').append(input_luogu_client_id))
|
||||
.append($('<div class="col-sm-6" />').append($('<div class="form-text" />').append('请填入 Cookie 中的 <code>__client_id</code>。')))
|
||||
).append(input_my_account_data);
|
||||
}
|
||||
|
||||
$(this).append(
|
||||
$('<div class="mt-3" />').append(
|
||||
$('<div class="form-check d-inline-block" />')
|
||||
.append(input_submit_type_bot)
|
||||
.append($('<label class="form-check-label" for="' + input_submit_type_bot_id + '" />').append(' 公用账号'))
|
||||
).append(
|
||||
$('<div class="form-check d-inline-block ms-3" />')
|
||||
.append(input_submit_type_my)
|
||||
.append($('<label class="form-check-label" for="' + input_submit_type_my_id + '" />').append(' 自有账号'))
|
||||
)
|
||||
).append(div_submit_type_bot).append(div_submit_type_my);
|
||||
});
|
||||
}
|
||||
|
||||
// custom test
|
||||
function custom_test_onsubmit(response_text, div_result, url) {
|
||||
if (response_text != '') {
|
||||
|
Loading…
Reference in New Issue
Block a user