daily-question 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +24 -17
- package/client.js +158 -99
- package/cordis.patch.yml +1 -1
- package/icon.svg +10 -7
- package/index.js +6 -6
- package/lib/bank.js +53 -36
- package/lib/exam-dates.js +101 -67
- package/lib/generate.js +11 -14
- package/lib/routes.js +49 -86
- package/lib/service.js +72 -72
- package/lib/store.js +3 -8
- package/lib/subjects.js +12 -0
- package/package.json +1 -1
package/lib/service.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { findSubject, allSubjects, QUESTION_TYPE_LABEL, CHOICE_TYPES, subjectTypes, REFERENCE_EXAMS } from './subjects.js'
|
|
2
|
+
import { findSubject, allSubjects, QUESTION_TYPE_LABEL, CHOICE_TYPES, hasChoiceOptions, validChoiceKey, subjectTypes, REFERENCE_EXAMS } from './subjects.js'
|
|
3
3
|
import { computeCountdown, resolveExamDate, todayISO, verifyExamDateOnline, yearOptions } from './exam-dates.js'
|
|
4
4
|
import { createVerifiedQuestion, QuestionGenerationError, reviseAnswer } from './generate.js'
|
|
5
5
|
import { answerFollowup } from './tutor.js'
|
|
6
|
-
import { parseBankText, extractBankPdf } from './bank.js'
|
|
6
|
+
import { parseBankText, extractBankPdf, isUsableBankItem } from './bank.js'
|
|
7
7
|
import { Store, resolveDataDir } from './store.js'
|
|
8
8
|
import { createUpdateChecker, PLUGIN_VERSION } from './updates.js'
|
|
9
9
|
import { withTimeout } from './async.js'
|
|
@@ -56,6 +56,8 @@ function normalizeChoice(value, question) {
|
|
|
56
56
|
for (const letter of upper) {
|
|
57
57
|
if (!keys.includes(letter)) throw new Error(`选项 ${letter} 不在本题选项中`)
|
|
58
58
|
}
|
|
59
|
+
if (question.type !== 'multiple' && upper.length !== 1) throw new Error('本题只能选择一个选项')
|
|
60
|
+
if (new Set(upper).size !== upper.length) throw new Error('选项不能重复')
|
|
59
61
|
return upper.split('').sort().join('')
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -204,7 +206,7 @@ export class DailyQuestionService {
|
|
|
204
206
|
enabled: enabled.has(subject.id),
|
|
205
207
|
source: data.settings.subjectSource[subject.id] || 'auto',
|
|
206
208
|
bankItemCount: usableBanks.reduce((total, bank) => total
|
|
207
|
-
+ bank.items.filter((item) => item
|
|
209
|
+
+ bank.items.filter((item) => isUsableBankItem(item) && (!item.type || allowed.has(item.type))).length, 0),
|
|
208
210
|
}
|
|
209
211
|
})
|
|
210
212
|
}
|
|
@@ -218,23 +220,22 @@ export class DailyQuestionService {
|
|
|
218
220
|
subjectId: bank.subjectId || '',
|
|
219
221
|
importedAt: bank.importedAt,
|
|
220
222
|
itemCount: bank.items.length,
|
|
221
|
-
usable: bank.items.filter(
|
|
223
|
+
usable: bank.items.filter(isUsableBankItem).length,
|
|
222
224
|
pendingAnswers: bank.items.filter((item) => item.answerStatus !== 'verified').length,
|
|
223
225
|
}))
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
counts(data) {
|
|
227
|
-
const result =
|
|
228
|
-
const
|
|
229
|
-
for(const id of enabled)result[id]={all:0,favorite:0,wrong:0}
|
|
229
|
+
const result = Object.create(null)
|
|
230
|
+
for (const id of data.settings.subjects) result[id] = { all: 0, favorite: 0, wrong: 0 }
|
|
230
231
|
for(const question of Object.values(data.questions)) {
|
|
231
|
-
if(!
|
|
232
|
-
const count=result[question.subjectId]
|
|
232
|
+
if (!question.subjectId) continue
|
|
233
|
+
const count = result[question.subjectId] ??= { all: 0, favorite: 0, wrong: 0 }
|
|
233
234
|
count.all++
|
|
234
235
|
if(question.favoriteAt)count.favorite++
|
|
235
236
|
if(this.isWrong(question,this.attemptsOf(data,question.id)))count.wrong++
|
|
236
237
|
}
|
|
237
|
-
return result
|
|
238
|
+
return { ...result }
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
isWrong(question, attempts) {
|
|
@@ -419,6 +420,7 @@ export class DailyQuestionService {
|
|
|
419
420
|
|
|
420
421
|
await this.store.update((draft) => {
|
|
421
422
|
signal?.throwIfAborted()
|
|
423
|
+
if (!draft.settings.subjects.includes(subject.id)) throw new Error('该科目已停用')
|
|
422
424
|
const history=Object.values(draft.questions).filter(question=>question.subjectId===subject.id)
|
|
423
425
|
if(findQuestionRepeat(generated,indexQuestionHistory(history)))throw new Error('题目与已发布题目重复,已阻止发布,请重试生成新题')
|
|
424
426
|
draft.questions[qid] = record
|
|
@@ -433,8 +435,7 @@ export class DailyQuestionService {
|
|
|
433
435
|
? error.message
|
|
434
436
|
: `在线出题失败:${error?.message || error}`
|
|
435
437
|
await this.store.update((draft) => {
|
|
436
|
-
draft.failures[subject.id] = { at: nowISO(), message }
|
|
437
|
-
return draft
|
|
438
|
+
if (draft.settings.subjects.includes(subject.id)) draft.failures[subject.id] = { at: nowISO(), message }
|
|
438
439
|
}).catch(() => {})
|
|
439
440
|
throw new Error(message)
|
|
440
441
|
}
|
|
@@ -446,7 +447,7 @@ export class DailyQuestionService {
|
|
|
446
447
|
for (const bank of data.banks) {
|
|
447
448
|
if (bank.subjectId && bank.subjectId !== subject.id) continue
|
|
448
449
|
for (const item of bank.items) {
|
|
449
|
-
if (!item
|
|
450
|
+
if (!isUsableBankItem(item)) continue
|
|
450
451
|
const qid = bankQuestionId(bank.id, item.id)
|
|
451
452
|
const existing = data.questions[qid]
|
|
452
453
|
if (existing && existing.subjectId !== subject.id) continue
|
|
@@ -467,6 +468,7 @@ export class DailyQuestionService {
|
|
|
467
468
|
const stamp = nowISO()
|
|
468
469
|
await this.store.update((draft) => {
|
|
469
470
|
signal?.throwIfAborted()
|
|
471
|
+
if (!draft.settings.subjects.includes(subject.id)) throw new Error('该科目已停用')
|
|
470
472
|
const existing = draft.questions[qid]
|
|
471
473
|
if (existing) {
|
|
472
474
|
existing.assignedCount = (existing.assignedCount || 1) + 1
|
|
@@ -546,6 +548,8 @@ export class DailyQuestionService {
|
|
|
546
548
|
const revealed = currentAttempts.length > 0
|
|
547
549
|
const last = (attemptId ? currentAttempts.find((attempt) => attempt.id === attemptId) : currentAttempts[currentAttempts.length - 1]) || null
|
|
548
550
|
const answer = question.currentAnswer
|
|
551
|
+
const isChoice = CHOICE_TYPES.has(question.type)
|
|
552
|
+
const canSelect = isChoice && hasChoiceOptions(question.options)
|
|
549
553
|
return {
|
|
550
554
|
id: question.id,
|
|
551
555
|
subjectId: question.subjectId,
|
|
@@ -553,7 +557,8 @@ export class DailyQuestionService {
|
|
|
553
557
|
type: question.type,
|
|
554
558
|
typeLabel: QUESTION_TYPE_LABEL[question.type] || question.type,
|
|
555
559
|
/** 完形填空 / 阅读理解按选择题判分,前端据此渲染选项而不是“作答完毕”。 */
|
|
556
|
-
isChoice
|
|
560
|
+
isChoice,
|
|
561
|
+
canSelect,
|
|
557
562
|
stem: question.stem,
|
|
558
563
|
options: question.options || [],
|
|
559
564
|
knowledge: question.knowledge || [],
|
|
@@ -566,7 +571,7 @@ export class DailyQuestionService {
|
|
|
566
571
|
paperEvidence: revealed ? question.examAlignment.paperEvidence : [],
|
|
567
572
|
} : null,
|
|
568
573
|
source: question.source,
|
|
569
|
-
assignedDate: isToday?date:question.assignedDate,
|
|
574
|
+
assignedDate: isToday ? date : question.lastAssignedDate || question.assignedDate,
|
|
570
575
|
practiceDate:isToday?date:null,
|
|
571
576
|
todayAttemptCount:isToday?currentAttempts.length:0,
|
|
572
577
|
createdAt: question.createdAt,
|
|
@@ -602,7 +607,7 @@ export class DailyQuestionService {
|
|
|
602
607
|
? {
|
|
603
608
|
versionId: answer.versionId,
|
|
604
609
|
answer: answer.answer,
|
|
605
|
-
answerKey: answer.answerKey
|
|
610
|
+
answerKey: canSelect && validChoiceKey(question.type, answer.answerKey, question.options) ? answer.answerKey : '',
|
|
606
611
|
explanation: answer.explanation,
|
|
607
612
|
verified: Boolean(answer.verified),
|
|
608
613
|
basis: answer.basis,
|
|
@@ -638,7 +643,7 @@ export class DailyQuestionService {
|
|
|
638
643
|
typeLabel: QUESTION_TYPE_LABEL[question.type] || question.type,
|
|
639
644
|
summary: summariseStem(question.stem),
|
|
640
645
|
stem: question.stem,
|
|
641
|
-
assignedDate: question.assignedDate,
|
|
646
|
+
assignedDate: question.lastAssignedDate || question.assignedDate,
|
|
642
647
|
attemptCount: attempts.length,
|
|
643
648
|
favorite: Boolean(question.favoriteAt),
|
|
644
649
|
wrongEver: this.isWrong(question, attempts),
|
|
@@ -666,7 +671,7 @@ export class DailyQuestionService {
|
|
|
666
671
|
const record = data.daily[`${todayISO()}|${subjectId}`]
|
|
667
672
|
questions = questions.filter((question) => question.id === record?.questionId)
|
|
668
673
|
}
|
|
669
|
-
questions.sort((a, b) => String(b.assignedDate).localeCompare(String(a.assignedDate))
|
|
674
|
+
questions.sort((a, b) => String(b.lastAssignedDate || b.assignedDate).localeCompare(String(a.lastAssignedDate || a.assignedDate))
|
|
670
675
|
|| String(b.createdAt).localeCompare(String(a.createdAt)) || String(b.id).localeCompare(String(a.id)))
|
|
671
676
|
const total=questions.length,totalPages=Math.max(1,Math.ceil(total/pageSize))
|
|
672
677
|
if(idsOnly===true)return {ok:true,kind,subjectId,total,ids:questions.map(q=>q.id)}
|
|
@@ -680,14 +685,14 @@ export class DailyQuestionService {
|
|
|
680
685
|
}
|
|
681
686
|
}
|
|
682
687
|
|
|
683
|
-
async getQuestion({ qid }) {
|
|
688
|
+
async getQuestion({ qid, attemptId }) {
|
|
684
689
|
const data = await this.data()
|
|
685
690
|
const question = data.questions[qid]
|
|
686
691
|
if (!question) throw new Error('题目不存在(可能已随题库删除而失效)')
|
|
687
692
|
const thread = this.threadView(data, qid)
|
|
688
693
|
const date=todayISO()
|
|
689
694
|
const isToday = data.daily[`${date}|${question.subjectId}`]?.questionId === qid
|
|
690
|
-
return { ok: true, question: this.questionView(data, question, {date}), thread, isToday }
|
|
695
|
+
return { ok: true, question: this.questionView(data, question, { date, attemptId }), thread, isToday }
|
|
691
696
|
}
|
|
692
697
|
|
|
693
698
|
threadView(data, qid) {
|
|
@@ -703,58 +708,46 @@ export class DailyQuestionService {
|
|
|
703
708
|
/* ------------------------------ 作答 ------------------------------ */
|
|
704
709
|
|
|
705
710
|
async reveal({ qid, mode = 'submit', choice = '', attemptId }) {
|
|
706
|
-
const
|
|
707
|
-
const question = data.questions[qid]
|
|
708
|
-
if (!question) throw new Error('题目不存在')
|
|
709
|
-
const isChoice = CHOICE_TYPES.has(question.type)
|
|
710
|
-
let result = 'pending'
|
|
711
|
-
let normalizedChoice = ''
|
|
712
|
-
if (mode === 'submit') {
|
|
713
|
-
if (!isChoice) throw new Error('主观题不支持提交选项')
|
|
714
|
-
normalizedChoice = normalizeChoice(choice, question)
|
|
715
|
-
const key = sortLetters(question.currentAnswer?.answerKey)
|
|
716
|
-
const canGrade = key && key.split('').every((letter) => question.options?.some((option) => option.key === letter))
|
|
717
|
-
&& (question.type !== 'single' || key.length === 1)
|
|
718
|
-
result = canGrade ? (normalizedChoice === key ? 'correct' : 'wrong') : 'pending'
|
|
719
|
-
} else if (mode === 'giveup') {
|
|
720
|
-
result = 'wrong'
|
|
721
|
-
} else if (mode === 'done') {
|
|
722
|
-
result = 'pending'
|
|
723
|
-
} else {
|
|
724
|
-
throw new Error(`未知的作答方式:${mode}`)
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const stamp = nowISO()
|
|
728
|
-
const attempt = {
|
|
729
|
-
id: attemptId || randomUUID(),
|
|
730
|
-
questionId: qid,
|
|
731
|
-
at: stamp,
|
|
732
|
-
practiceDate:todayISO(new Date(stamp)),
|
|
733
|
-
type: question.type,
|
|
734
|
-
mode,
|
|
735
|
-
choice: normalizedChoice,
|
|
736
|
-
result,
|
|
737
|
-
revealedAt: stamp,
|
|
738
|
-
judgedAt: result === 'pending' ? null : stamp,
|
|
739
|
-
}
|
|
740
|
-
|
|
711
|
+
const id = attemptId || randomUUID()
|
|
741
712
|
const duplicated = await this.store.update((draft) => {
|
|
742
|
-
const
|
|
743
|
-
if (!
|
|
744
|
-
const existing = draft.attempts.find((item) => item.id ===
|
|
713
|
+
const question = draft.questions[qid]
|
|
714
|
+
if (!question) throw new Error('题目不存在')
|
|
715
|
+
const existing = draft.attempts.find((item) => item.id === id)
|
|
745
716
|
if (existing) {
|
|
746
717
|
if (existing.questionId !== qid) throw new Error('作答 ID 已用于其他题目')
|
|
718
|
+
if (existing.mode !== mode || (existing.choice || '') !== (mode === 'submit' ? sortLetters(choice) : '')) {
|
|
719
|
+
throw Object.assign(new Error('本轮已保存另一份作答,请查看已保存的结果'), { code: 'ATTEMPT_CONFLICT' })
|
|
720
|
+
}
|
|
747
721
|
return true
|
|
748
722
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
723
|
+
let result = 'pending'
|
|
724
|
+
let normalizedChoice = ''
|
|
725
|
+
if (mode === 'submit') {
|
|
726
|
+
if (!CHOICE_TYPES.has(question.type)) throw new Error('主观题不支持提交选项')
|
|
727
|
+
if (!hasChoiceOptions(question.options)) throw new Error('选项不完整,请使用“作答完毕”查看答案')
|
|
728
|
+
normalizedChoice = normalizeChoice(choice, question)
|
|
729
|
+
const key = question.currentAnswer?.answerKey
|
|
730
|
+
if (validChoiceKey(question.type, key, question.options)) result = normalizedChoice === sortLetters(key) ? 'correct' : 'wrong'
|
|
731
|
+
} else if (mode === 'giveup') {
|
|
732
|
+
result = 'wrong'
|
|
733
|
+
} else if (mode !== 'done') {
|
|
734
|
+
throw new Error(`未知的作答方式:${mode}`)
|
|
752
735
|
}
|
|
753
|
-
|
|
736
|
+
const stamp = nowISO()
|
|
737
|
+
draft.attempts.push({
|
|
738
|
+
id, questionId: qid, at: stamp, practiceDate: todayISO(new Date(stamp)),
|
|
739
|
+
type: question.type, mode, choice: normalizedChoice, result,
|
|
740
|
+
revealedAt: stamp, judgedAt: result === 'pending' ? null : stamp,
|
|
741
|
+
})
|
|
742
|
+
if (question.currentAnswer && !question.currentAnswer.firstShownAt) {
|
|
743
|
+
question.currentAnswer.firstShownAt = stamp
|
|
744
|
+
}
|
|
745
|
+
question.updatedAt = stamp
|
|
754
746
|
return false
|
|
755
747
|
})
|
|
756
748
|
const fresh = await this.data()
|
|
757
|
-
|
|
749
|
+
if (!fresh.questions[qid]) throw new Error('题目已删除')
|
|
750
|
+
return { ok: true, question: this.questionView(fresh, fresh.questions[qid], { attemptId: id }), duplicated }
|
|
758
751
|
}
|
|
759
752
|
|
|
760
753
|
async judge({ qid, verdict, attemptId }) {
|
|
@@ -1115,12 +1108,13 @@ export class DailyQuestionService {
|
|
|
1115
1108
|
knowledge: item.knowledge,
|
|
1116
1109
|
difficulty: item.difficulty,
|
|
1117
1110
|
issues: item.issues,
|
|
1111
|
+
usable: isUsableBankItem(item),
|
|
1118
1112
|
answerStatus: item.answerStatus,
|
|
1119
1113
|
})),
|
|
1120
1114
|
stats: {
|
|
1121
1115
|
total: parsed.items.length,
|
|
1122
1116
|
withAnswer: parsed.items.filter((item) => item.answer).length,
|
|
1123
|
-
blocked: parsed.items.
|
|
1117
|
+
blocked: parsed.items.length - parsed.usable,
|
|
1124
1118
|
},
|
|
1125
1119
|
}
|
|
1126
1120
|
}
|
|
@@ -1132,10 +1126,14 @@ export class DailyQuestionService {
|
|
|
1132
1126
|
}
|
|
1133
1127
|
}
|
|
1134
1128
|
|
|
1135
|
-
async commitBank({ importId, name, subjectId }) {
|
|
1129
|
+
async commitBank({ importId, name, subjectId, excludedIndexes = [] }) {
|
|
1136
1130
|
const pending = this.imports.get(importId)
|
|
1137
1131
|
if (!pending) throw new Error('导入结果已过期,请重新选择文件导入')
|
|
1138
|
-
|
|
1132
|
+
if (!Array.isArray(excludedIndexes) || excludedIndexes.some((index) => !Number.isInteger(index) || index < 1 || index > pending.items.length)) {
|
|
1133
|
+
throw new Error('导入勾选有误,请重新选择题目')
|
|
1134
|
+
}
|
|
1135
|
+
const excluded = new Set(excludedIndexes)
|
|
1136
|
+
const usable = pending.items.filter((item) => !excluded.has(item.index) && isUsableBankItem(item))
|
|
1139
1137
|
if (!usable.length) throw new Error('没有可用的题目可以入库:请检查文件结构后重新导入')
|
|
1140
1138
|
const bankId = `bank_${randomUUID().slice(0, 8)}`
|
|
1141
1139
|
const bank = {
|
|
@@ -1159,11 +1157,13 @@ export class DailyQuestionService {
|
|
|
1159
1157
|
answerStatus: item.answer ? 'unverified' : 'missing',
|
|
1160
1158
|
})),
|
|
1161
1159
|
}
|
|
1162
|
-
await this.store.update((draft) => {
|
|
1163
|
-
draft.banks.push(bank)
|
|
1164
|
-
return draft
|
|
1165
|
-
})
|
|
1166
1160
|
this.imports.delete(importId)
|
|
1161
|
+
try {
|
|
1162
|
+
await this.store.update((draft) => { draft.banks.push(bank) })
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
this.imports.set(importId, pending)
|
|
1165
|
+
throw error
|
|
1166
|
+
}
|
|
1167
1167
|
const data = await this.data()
|
|
1168
1168
|
return { ok: true, bank: this.bankViews(data).find((item) => item.id === bankId), banks: this.bankViews(data), subjects: this.subjectViews(data) }
|
|
1169
1169
|
}
|
|
@@ -1240,11 +1240,11 @@ export class DailyQuestionService {
|
|
|
1240
1240
|
const kept = Object.values(data.questions).filter((question) => question.subjectId === subjectId).length
|
|
1241
1241
|
const result = await this.saveSettings({
|
|
1242
1242
|
subjects: remaining,
|
|
1243
|
-
activeSubject: data.settings.activeSubject === subjectId ? (remaining[0] || null) : data.settings.activeSubject,
|
|
1243
|
+
activeSubject: data.settings.activeSubject === subjectId && !kept ? (remaining[0] || null) : data.settings.activeSubject,
|
|
1244
1244
|
})
|
|
1245
1245
|
return {
|
|
1246
1246
|
...result,
|
|
1247
|
-
message: `科目已移出每日出题,历史数据保留:${kept}
|
|
1247
|
+
message: `科目已移出每日出题,历史数据保留:${kept} 道题目及其作答记录可通过“已停用科目”查看。`,
|
|
1248
1248
|
}
|
|
1249
1249
|
}
|
|
1250
1250
|
}
|
package/lib/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import { todayISO, yearOptions } from './exam-dates.js'
|
|
4
5
|
|
|
5
6
|
/** Bump when the persisted shape changes incompatibly. */
|
|
6
7
|
export const DATA_VERSION = 1
|
|
@@ -27,7 +28,7 @@ export function emptyData() {
|
|
|
27
28
|
updatedAt: now,
|
|
28
29
|
settings: {
|
|
29
30
|
/** 目标考研年份(入学年份),例如 2028 表示 2028 年入学。 */
|
|
30
|
-
targetYear:
|
|
31
|
+
targetYear: yearOptions()[0],
|
|
31
32
|
/** 已启用科目 id,按用户顺序排列。 */
|
|
32
33
|
subjects: [],
|
|
33
34
|
/** 科目 id -> 'online' | 'bank' | 'auto' */
|
|
@@ -54,12 +55,6 @@ export function emptyData() {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/** 默认目标年份:若 12 月考试尚未过去则取今年入学,否则取明年。 */
|
|
58
|
-
function defaultTargetYear() {
|
|
59
|
-
const now = new Date()
|
|
60
|
-
return now.getMonth() >= 11 ? now.getFullYear() + 2 : now.getFullYear() + 1
|
|
61
|
-
}
|
|
62
|
-
|
|
63
58
|
function isPlainObject(value) {
|
|
64
59
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
65
60
|
}
|
|
@@ -85,7 +80,7 @@ function normalize(raw) {
|
|
|
85
80
|
data.settings[key] = base.settings[key]
|
|
86
81
|
}
|
|
87
82
|
}
|
|
88
|
-
if (!Number.isFinite(
|
|
83
|
+
if (!Number.isFinite(raw.settings?.targetYear)) data.settings.targetYear = yearOptions(todayISO(), data.examDates)[0]
|
|
89
84
|
data.version = DATA_VERSION
|
|
90
85
|
return data
|
|
91
86
|
}
|
package/lib/subjects.js
CHANGED
|
@@ -22,6 +22,18 @@ export const QUESTION_TYPE_LABEL = {
|
|
|
22
22
|
/** 选择题类题型(判分依据唯一选项)。完形填空与阅读理解同样按四选一判分。 */
|
|
23
23
|
export const CHOICE_TYPES = new Set(['single', 'multiple', 'cloze', 'reading'])
|
|
24
24
|
|
|
25
|
+
export function hasChoiceOptions(options) {
|
|
26
|
+
return Array.isArray(options) && options.length >= 2
|
|
27
|
+
&& options.every((option) => /^[A-H]$/.test(option?.key) && typeof option.text === 'string' && option.text.trim())
|
|
28
|
+
&& new Set(options.map((option) => option.key)).size === options.length
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function validChoiceKey(type, key, options) {
|
|
32
|
+
return typeof key === 'string' && /^[A-H]+$/.test(key) && (type === 'multiple' || key.length === 1)
|
|
33
|
+
&& new Set(key).size === key.length
|
|
34
|
+
&& [...key].every((letter) => options?.some((option) => option?.key === letter))
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
export const SUBJECT_CATALOG = [
|
|
26
38
|
{
|
|
27
39
|
id: 'politics',
|