feat: support desktop exams with strict answer text verification
This commit is contained in:
+23
-13
@@ -5,11 +5,19 @@
|
||||
const cache = new WeakMap();
|
||||
|
||||
function normalize(value) {
|
||||
return String(value == null ? '' : value).normalize('NFKC')
|
||||
.replace(/[−﹣]/g, '-').replace(/\u00a0/g, ' ')
|
||||
.replace(/[\s\u200b\uFEFF]+/g, ' ').trim().toLowerCase();
|
||||
// Strict answer identity: MPa/mPa, m²/m2 and Ⅰ/I are distinct.
|
||||
return String(value == null ? '' : value)
|
||||
.replace(/[\u200b-\u200d\u2060\uFEFF]/g, '')
|
||||
.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
// Search is intentionally more tolerant; these keys never authorize clicks.
|
||||
function searchNormalize(value) {
|
||||
return normalize(value).normalize('NFKC').replace(/[−﹣]/g, '-').toLowerCase();
|
||||
}
|
||||
|
||||
function courseNormalize(value) { return searchNormalize(value); }
|
||||
|
||||
function normalizeType(value) {
|
||||
const type = String(value || '').trim().replace(/^[\[【]|[\]】]$/g, '').replace(/题$/, '');
|
||||
return ({ 单选: '单选', 单项选择: '单选', single: '单选', radio: '单选', 多选: '多选', 多项选择: '多选', multiple: '多选', checkbox: '多选', 判断: '判断', 判断正误: '判断', boolean: '判断' })[type.toLowerCase()] || type;
|
||||
@@ -77,9 +85,9 @@
|
||||
const exactSource = records.filter(record => record.sourceUrl && sourceIdentity(record.sourceUrl) === identity);
|
||||
if (exactSource.length) { records = exactSource; sourceMatched = true; }
|
||||
}
|
||||
const course = normalize(context.course);
|
||||
const course = courseNormalize(context.course);
|
||||
if (course) {
|
||||
const exactCourse = records.filter(record => normalize(record.course) === course);
|
||||
const exactCourse = records.filter(record => courseNormalize(record.course) === course);
|
||||
if (exactCourse.length) { records = exactCourse; sourceMatched = true; }
|
||||
}
|
||||
const answers = new Set(records.map(correctKey));
|
||||
@@ -89,7 +97,9 @@
|
||||
if (!wantedKeys.length || new Set(wantedKeys).size !== wantedKeys.length || wantedKeys.some(text => !currentTexts.includes(text))) return result('unsupported', '题库参考答案无法唯一对应当前选项', records);
|
||||
const selected = question.options.filter(option => wantedKeys.includes(normalize(option.text)));
|
||||
if (normalizeType(question.type) !== '多选' && selected.length !== 1) return result('unsupported', '单选/判断题的参考答案数量不正确', records);
|
||||
return result('matched', sourceMatched ? '岗位、课程、题干及选项完全匹配' : '岗位、题干及选项完全匹配,所有来源答案一致', records, selected.map(option => normalizedLabel(option.label)), selected.map(option => option.text));
|
||||
// Keep label/text pairs in current-page order, but obtain each text from bank evidence.
|
||||
const evidenceTexts = selected.map(option => wanted.find(text => normalize(text) === normalize(option.text)));
|
||||
return result('matched', sourceMatched ? '岗位、课程、题干及选项完全匹配' : '岗位、题干及选项完全匹配,所有来源答案一致', records, selected.map(option => normalizedLabel(option.label)), evidenceTexts);
|
||||
}
|
||||
|
||||
function search(query, settings, snapshot) {
|
||||
@@ -97,14 +107,14 @@
|
||||
const role = settings.role;
|
||||
const empty = reason => ({ items: [], total: 0, reason, role: role || '', query: String(query || ''), hasMore: false });
|
||||
if (!ROLES.includes(role)) return empty('请先选择主操、班长或副操');
|
||||
const fullQuery = normalize(query);
|
||||
const fullQuery = searchNormalize(query);
|
||||
if (!fullQuery) return empty('请输入题干、关键词或选项文字');
|
||||
const tokens = fullQuery.split(/\s+/).filter(Boolean);
|
||||
const course = normalize(settings.course);
|
||||
const course = courseNormalize(settings.course);
|
||||
const records = snapshot && Array.isArray(snapshot.records) ? snapshot.records : [];
|
||||
const groups = new Map(), conflicts = new Map();
|
||||
records.forEach(record => {
|
||||
if (record.role !== role || (course && normalize(record.course) !== course)) return;
|
||||
if (record.role !== role || (course && courseNormalize(record.course) !== course)) return;
|
||||
const baseKey = groupKey(record), answerKey = correctKey(record), key = baseKey + '\n' + answerKey;
|
||||
if (!conflicts.has(baseKey)) conflicts.set(baseKey, new Set());
|
||||
conflicts.get(baseKey).add(answerKey);
|
||||
@@ -116,9 +126,9 @@
|
||||
});
|
||||
const items = [];
|
||||
groups.forEach(group => {
|
||||
const stem = normalize(group.question);
|
||||
const optionText = (group.options || []).map(option => normalize(option.text)).join('\n');
|
||||
const answerText = (group.correctTexts || []).map(normalize).join('\n');
|
||||
const stem = searchNormalize(group.question);
|
||||
const optionText = (group.options || []).map(option => searchNormalize(option.text)).join('\n');
|
||||
const answerText = (group.correctTexts || []).map(searchNormalize).join('\n');
|
||||
const haystack = stem + '\n' + optionText + '\n' + answerText;
|
||||
if (!tokens.every(token => haystack.includes(token))) return;
|
||||
let score = stem === fullQuery ? 10000 : stem.includes(fullQuery) ? 5000 : 0;
|
||||
@@ -134,7 +144,7 @@
|
||||
return { items: items.slice(offset, offset + limit), total, role, query: String(query), reason: total ? '' : '当前岗位未找到符合条件的题目', hasMore: offset + limit < total };
|
||||
}
|
||||
|
||||
const api = Object.freeze({ version: '2.0.0', normalize, normalizeType, optionKey, questionKey, sourceIdentity, match, search });
|
||||
const api = Object.freeze({ version: '2.0.0', normalize, searchNormalize, normalizeType, optionKey, questionKey, sourceIdentity, match, search });
|
||||
root.YianAnswerMatcher = api;
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
|
||||
Reference in New Issue
Block a user