daily-question 2.0.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 +41 -0
- package/LICENSE +21 -21
- package/README.md +110 -25
- package/client.js +432 -153
- package/cordis.patch.yml +1 -1
- package/icon.svg +10 -7
- package/index.js +8 -8
- package/lib/async.js +17 -0
- package/lib/bank.js +56 -39
- package/lib/exam-dates.js +101 -67
- package/lib/generate.js +131 -114
- package/lib/generation-history.js +67 -0
- package/lib/llm-json.js +17 -8
- package/lib/pdf-worker.js +5 -0
- package/lib/pdf.js +156 -34
- package/lib/reference-reader.js +178 -0
- package/lib/reference-sources.js +83 -0
- package/lib/references.js +159 -0
- package/lib/research.js +166 -0
- package/lib/routes.js +51 -86
- package/lib/service.js +258 -109
- package/lib/store.js +3 -8
- package/lib/subjects.js +31 -0
- package/lib/tutor.js +6 -5
- package/lib/updates.js +49 -0
- package/package.json +4 -2
package/lib/service.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { findSubject, allSubjects, QUESTION_TYPE_LABEL, CHOICE_TYPES, subjectTypes } 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
|
+
import { createUpdateChecker, PLUGIN_VERSION } from './updates.js'
|
|
9
|
+
import { withTimeout } from './async.js'
|
|
10
|
+
import { indexQuestionHistory,findQuestionRepeat } from './generation-history.js'
|
|
8
11
|
|
|
9
12
|
const MAX_STEM_SUMMARY = 140
|
|
10
13
|
const IMPORT_TTL_MS = 30 * 60 * 1000
|
|
@@ -13,6 +16,12 @@ function nowISO() {
|
|
|
13
16
|
return new Date().toISOString()
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
function attemptDate(attempt) {
|
|
20
|
+
if(/^\d{4}-\d{2}-\d{2}$/.test(attempt.practiceDate||''))return attempt.practiceDate
|
|
21
|
+
const date=new Date(attempt.revealedAt||attempt.at||'')
|
|
22
|
+
return Number.isFinite(date.getTime())?todayISO(date):''
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* 自定义科目的题型:内置键(single/multiple/fill/solution…)和用户自填的文字
|
|
18
27
|
* 都允许,只做去空、去重、限长限量。
|
|
@@ -47,6 +56,8 @@ function normalizeChoice(value, question) {
|
|
|
47
56
|
for (const letter of upper) {
|
|
48
57
|
if (!keys.includes(letter)) throw new Error(`选项 ${letter} 不在本题选项中`)
|
|
49
58
|
}
|
|
59
|
+
if (question.type !== 'multiple' && upper.length !== 1) throw new Error('本题只能选择一个选项')
|
|
60
|
+
if (new Set(upper).size !== upper.length) throw new Error('选项不能重复')
|
|
50
61
|
return upper.split('').sort().join('')
|
|
51
62
|
}
|
|
52
63
|
|
|
@@ -62,21 +73,65 @@ function sortLetters(value) {
|
|
|
62
73
|
* 后续可能的 Agent 工具共享同一套业务逻辑。
|
|
63
74
|
*/
|
|
64
75
|
export class DailyQuestionService {
|
|
65
|
-
constructor(ctx) {
|
|
76
|
+
constructor(ctx, { taskTimeoutMs } = {}) {
|
|
66
77
|
this.ctx = ctx
|
|
67
78
|
this.store = new Store(resolveDataDir())
|
|
68
79
|
this.inflightDaily = new Map()
|
|
80
|
+
this.inflightFollowups = new Map()
|
|
81
|
+
this.recovery = null
|
|
82
|
+
this.attemptIndexes = new WeakMap()
|
|
83
|
+
this.tasks = new Map()
|
|
84
|
+
this.taskTimeoutMs = taskTimeoutMs
|
|
69
85
|
this.imports = new Map()
|
|
86
|
+
this.checkUpdates = createUpdateChecker()
|
|
70
87
|
this.log = typeof ctx.logger === 'function' ? ctx.logger('daily-question') : (ctx.logger || console)
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
async init() {
|
|
74
|
-
await this.
|
|
91
|
+
await this.data()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
runTask(key, details, run) {
|
|
95
|
+
if (this.tasks.has(key)) return this.tasks.get(key).promise
|
|
96
|
+
const job = { ...details, controller: new AbortController() }
|
|
97
|
+
job.promise = withTimeout(run, { signal: job.controller.signal,
|
|
98
|
+
timeoutMs: this.taskTimeoutMs ?? (details.kind === 'daily' ? 15*60*1000 : details.kind === 'revise' ? 10*60*1000 : 5*60*1000),
|
|
99
|
+
message: '任务超时,已停止本次生成,可以重试',
|
|
100
|
+
}).finally(() => { if (this.tasks.get(key) === job) this.tasks.delete(key) })
|
|
101
|
+
this.tasks.set(key, job)
|
|
102
|
+
return job.promise
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async cancelTask({ kind, subjectId, qid, clientMessageId }) {
|
|
106
|
+
if (!['daily','revise','followup'].includes(kind)) throw new Error('不支持取消此类任务')
|
|
107
|
+
const jobs = [...this.tasks.values()].filter((job) => job.kind === kind && (kind === 'daily'
|
|
108
|
+
? Boolean(subjectId) && job.subjectId === subjectId
|
|
109
|
+
: Boolean(qid) && job.qid === qid && (kind !== 'followup' || !clientMessageId || job.clientMessageId === clientMessageId)))
|
|
110
|
+
for (const job of jobs) job.controller.abort(Object.assign(new Error('任务已取消,可以重试'), { code:'TASK_CANCELLED' }))
|
|
111
|
+
await Promise.allSettled(jobs.map((job) => job.promise))
|
|
112
|
+
return { ok: true, cancelled: jobs.length }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
dispose() {
|
|
116
|
+
for (const job of this.tasks.values()) job.controller.abort(new Error('插件已停止,本次任务已取消'))
|
|
75
117
|
}
|
|
76
118
|
|
|
77
119
|
/* ------------------------------ 读取与状态 ------------------------------ */
|
|
78
120
|
|
|
79
121
|
async data() {
|
|
122
|
+
if (!this.recovery) {
|
|
123
|
+
this.recovery = (async () => {
|
|
124
|
+
const data = await this.store.read()
|
|
125
|
+
if (Object.values(data.threads).some((thread) => thread.messages?.some((message) => message.status === 'pending'))) {
|
|
126
|
+
await this.store.update((draft) => {
|
|
127
|
+
for (const thread of Object.values(draft.threads)) {
|
|
128
|
+
for (const message of thread.messages || []) if (message.status === 'pending') message.status = 'failed'
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
})().catch((error) => { this.recovery = null; throw error })
|
|
133
|
+
}
|
|
134
|
+
await this.recovery
|
|
80
135
|
return this.store.read()
|
|
81
136
|
}
|
|
82
137
|
|
|
@@ -89,7 +144,9 @@ export class DailyQuestionService {
|
|
|
89
144
|
}
|
|
90
145
|
|
|
91
146
|
async bootstrap() {
|
|
92
|
-
const
|
|
147
|
+
const now = new Date()
|
|
148
|
+
const today = todayISO(now)
|
|
149
|
+
const nextDayAt = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString()
|
|
93
150
|
let data = await this.data()
|
|
94
151
|
const options = yearOptions(today, data.examDates)
|
|
95
152
|
// 旧设置可能落在窗口外(它的初试早已结束):自动收敛到可选的最早一年
|
|
@@ -112,12 +169,15 @@ export class DailyQuestionService {
|
|
|
112
169
|
return {
|
|
113
170
|
ok: true,
|
|
114
171
|
dataFile: this.store.file,
|
|
172
|
+
pluginVersion: PLUGIN_VERSION,
|
|
115
173
|
dataError: this.store.lastError,
|
|
116
174
|
today,
|
|
117
|
-
now:
|
|
175
|
+
now: now.toISOString(),
|
|
176
|
+
nextDayAt,
|
|
118
177
|
yearOptions: options,
|
|
119
178
|
settings: data.settings,
|
|
120
179
|
customSubjects: data.customSubjects,
|
|
180
|
+
referenceExams: REFERENCE_EXAMS,
|
|
121
181
|
examInfo,
|
|
122
182
|
countdown,
|
|
123
183
|
subjects: this.subjectViews(data),
|
|
@@ -140,10 +200,13 @@ export class DailyQuestionService {
|
|
|
140
200
|
types: subject.types,
|
|
141
201
|
typeLabels: subject.types.map((type) => QUESTION_TYPE_LABEL[type] || type),
|
|
142
202
|
custom: Boolean(subject.custom),
|
|
203
|
+
referenceSubjectId: subject.referenceSubjectId || '',
|
|
204
|
+
referenceExamName: subject.referenceExamName || '',
|
|
205
|
+
referenceExamCode: subject.referenceExamCode || '',
|
|
143
206
|
enabled: enabled.has(subject.id),
|
|
144
207
|
source: data.settings.subjectSource[subject.id] || 'auto',
|
|
145
208
|
bankItemCount: usableBanks.reduce((total, bank) => total
|
|
146
|
-
+ bank.items.filter((item) => item
|
|
209
|
+
+ bank.items.filter((item) => isUsableBankItem(item) && (!item.type || allowed.has(item.type))).length, 0),
|
|
147
210
|
}
|
|
148
211
|
})
|
|
149
212
|
}
|
|
@@ -157,35 +220,42 @@ export class DailyQuestionService {
|
|
|
157
220
|
subjectId: bank.subjectId || '',
|
|
158
221
|
importedAt: bank.importedAt,
|
|
159
222
|
itemCount: bank.items.length,
|
|
160
|
-
usable: bank.items.filter(
|
|
223
|
+
usable: bank.items.filter(isUsableBankItem).length,
|
|
161
224
|
pendingAnswers: bank.items.filter((item) => item.answerStatus !== 'verified').length,
|
|
162
225
|
}))
|
|
163
226
|
}
|
|
164
227
|
|
|
165
228
|
counts(data) {
|
|
166
|
-
const result =
|
|
167
|
-
for (const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
wrong: questions.filter((question) => this.isWrong(question, attempts)).length,
|
|
175
|
-
}
|
|
229
|
+
const result = Object.create(null)
|
|
230
|
+
for (const id of data.settings.subjects) result[id] = { all: 0, favorite: 0, wrong: 0 }
|
|
231
|
+
for(const question of Object.values(data.questions)) {
|
|
232
|
+
if (!question.subjectId) continue
|
|
233
|
+
const count = result[question.subjectId] ??= { all: 0, favorite: 0, wrong: 0 }
|
|
234
|
+
count.all++
|
|
235
|
+
if(question.favoriteAt)count.favorite++
|
|
236
|
+
if(this.isWrong(question,this.attemptsOf(data,question.id)))count.wrong++
|
|
176
237
|
}
|
|
177
|
-
return result
|
|
238
|
+
return { ...result }
|
|
178
239
|
}
|
|
179
240
|
|
|
180
241
|
isWrong(question, attempts) {
|
|
181
242
|
if (question.wrongBookRemoved) return false
|
|
182
243
|
if (question.wrongBookAdded) return true
|
|
183
|
-
|
|
184
|
-
return own.some((attempt) => attempt.result === 'wrong')
|
|
244
|
+
return attempts.some((attempt) => attempt.questionId === question.id && attempt.result === 'wrong')
|
|
185
245
|
}
|
|
186
246
|
|
|
187
247
|
attemptsOf(data, qid) {
|
|
188
|
-
|
|
248
|
+
let index=this.attemptIndexes.get(data)
|
|
249
|
+
if(!index||index.source!==data.attempts||index.length!==data.attempts.length) {
|
|
250
|
+
const byQuestion=new Map()
|
|
251
|
+
for(const attempt of data.attempts) {
|
|
252
|
+
if(!byQuestion.has(attempt.questionId))byQuestion.set(attempt.questionId,[])
|
|
253
|
+
byQuestion.get(attempt.questionId).push(attempt)
|
|
254
|
+
}
|
|
255
|
+
index={source:data.attempts,length:data.attempts.length,byQuestion}
|
|
256
|
+
this.attemptIndexes.set(data,index)
|
|
257
|
+
}
|
|
258
|
+
return index.byQuestion.get(qid)||[]
|
|
189
259
|
}
|
|
190
260
|
|
|
191
261
|
/* ------------------------------ 设置 ------------------------------ */
|
|
@@ -194,6 +264,9 @@ export class DailyQuestionService {
|
|
|
194
264
|
const data = await this.store.update((draft) => {
|
|
195
265
|
// 先落自定义科目,后面的 enabled 校验才能识别到它们。
|
|
196
266
|
if (Array.isArray(patch.customSubjects)) {
|
|
267
|
+
if (patch.customSubjects.some((item) => item?.referenceSubjectId === 'independent' && !(typeof item.referenceExamName === 'string' && item.referenceExamName.trim()))) {
|
|
268
|
+
throw new Error('独立参考考试请填写准确的考试名称(院校自命题请包含院校名称)')
|
|
269
|
+
}
|
|
197
270
|
draft.customSubjects = patch.customSubjects
|
|
198
271
|
.filter((item) => item && typeof item.id === 'string' && typeof item.name === 'string')
|
|
199
272
|
.map((item) => ({
|
|
@@ -203,6 +276,9 @@ export class DailyQuestionService {
|
|
|
203
276
|
types: normalizeCustomTypes(item.types),
|
|
204
277
|
outline: Array.isArray(item.outline) ? item.outline.filter((line) => typeof line === 'string').slice(0, 20) : [],
|
|
205
278
|
guidance: typeof item.guidance === 'string' ? item.guidance : '',
|
|
279
|
+
referenceSubjectId: ['independent','auto'].includes(item.referenceSubjectId) ? item.referenceSubjectId : REFERENCE_EXAMS.find((exam) => exam.id === item.referenceSubjectId)?.id || '',
|
|
280
|
+
referenceExamName: typeof item.referenceExamName === 'string' ? item.referenceExamName.trim().slice(0,100) : '',
|
|
281
|
+
referenceExamCode: typeof item.referenceExamCode === 'string' ? item.referenceExamCode.trim().slice(0,20) : '',
|
|
206
282
|
}))
|
|
207
283
|
}
|
|
208
284
|
if (patch.targetYear !== undefined) {
|
|
@@ -269,14 +345,15 @@ export class DailyQuestionService {
|
|
|
269
345
|
return { ok: true, question: this.questionView(fresh, fresh.questions[result.questionId]), thread: this.threadView(fresh, result.questionId), created: result.created }
|
|
270
346
|
}
|
|
271
347
|
|
|
272
|
-
const task = this.
|
|
348
|
+
const task = this.runTask('daily:'+key, {kind:'daily',subjectId:subject.id},
|
|
349
|
+
(signal) => this.createDaily(subject, date, key, signal)).finally(() => this.inflightDaily.delete(key))
|
|
273
350
|
this.inflightDaily.set(key, task)
|
|
274
351
|
const result = await task
|
|
275
352
|
const fresh = await this.data()
|
|
276
353
|
return { ok: true, question: this.questionView(fresh, fresh.questions[result.questionId]), thread: this.threadView(fresh, result.questionId), created: result.created }
|
|
277
354
|
}
|
|
278
355
|
|
|
279
|
-
async createDaily(subject, date, key) {
|
|
356
|
+
async createDaily(subject, date, key, signal) {
|
|
280
357
|
const data = await this.data()
|
|
281
358
|
const source = data.settings.subjectSource[subject.id] || 'auto'
|
|
282
359
|
const examInfo = resolveExamDate(data.settings.targetYear, data.examDates)
|
|
@@ -284,7 +361,7 @@ export class DailyQuestionService {
|
|
|
284
361
|
try {
|
|
285
362
|
if (source !== 'online') {
|
|
286
363
|
const picked = this.pickFromBank(data, subject, date)
|
|
287
|
-
if (picked) return await this.publishBankQuestion(picked, subject, date, key)
|
|
364
|
+
if (picked) return await this.publishBankQuestion(picked, subject, date, key, signal)
|
|
288
365
|
if (source === 'bank') {
|
|
289
366
|
throw new Error('题库中没有符合该科目要求的可用题目:请先导入题库,或把该科目的出题来源切换为在线出题')
|
|
290
367
|
}
|
|
@@ -294,6 +371,8 @@ export class DailyQuestionService {
|
|
|
294
371
|
ctx: this.ctx,
|
|
295
372
|
subject,
|
|
296
373
|
examInfo,
|
|
374
|
+
signal,
|
|
375
|
+
history:Object.values(data.questions).filter(question=>question.subjectId===subject.id),
|
|
297
376
|
})
|
|
298
377
|
const qid = `q_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`
|
|
299
378
|
const stamp = nowISO()
|
|
@@ -305,7 +384,9 @@ export class DailyQuestionService {
|
|
|
305
384
|
stem: generated.stem,
|
|
306
385
|
options: generated.options,
|
|
307
386
|
knowledge: generated.knowledge,
|
|
387
|
+
coverageTopic:generated.coverageTopic,
|
|
308
388
|
difficulty: generated.difficulty,
|
|
389
|
+
examAlignment: generated.examAlignment,
|
|
309
390
|
syllabusCheck: generated.syllabusCheck || '',
|
|
310
391
|
source: {
|
|
311
392
|
kind: 'online',
|
|
@@ -338,6 +419,10 @@ export class DailyQuestionService {
|
|
|
338
419
|
}
|
|
339
420
|
|
|
340
421
|
await this.store.update((draft) => {
|
|
422
|
+
signal?.throwIfAborted()
|
|
423
|
+
if (!draft.settings.subjects.includes(subject.id)) throw new Error('该科目已停用')
|
|
424
|
+
const history=Object.values(draft.questions).filter(question=>question.subjectId===subject.id)
|
|
425
|
+
if(findQuestionRepeat(generated,indexQuestionHistory(history)))throw new Error('题目与已发布题目重复,已阻止发布,请重试生成新题')
|
|
341
426
|
draft.questions[qid] = record
|
|
342
427
|
draft.daily[key] = { date, subjectId: subject.id, questionId: qid, status: 'ready', createdAt: stamp }
|
|
343
428
|
delete draft.failures[subject.id]
|
|
@@ -345,12 +430,12 @@ export class DailyQuestionService {
|
|
|
345
430
|
})
|
|
346
431
|
return { questionId: qid, created: true }
|
|
347
432
|
} catch (error) {
|
|
433
|
+
if (signal?.aborted) throw signal.reason
|
|
348
434
|
const message = error instanceof QuestionGenerationError
|
|
349
435
|
? error.message
|
|
350
436
|
: `在线出题失败:${error?.message || error}`
|
|
351
437
|
await this.store.update((draft) => {
|
|
352
|
-
draft.failures[subject.id] = { at: nowISO(), message }
|
|
353
|
-
return draft
|
|
438
|
+
if (draft.settings.subjects.includes(subject.id)) draft.failures[subject.id] = { at: nowISO(), message }
|
|
354
439
|
}).catch(() => {})
|
|
355
440
|
throw new Error(message)
|
|
356
441
|
}
|
|
@@ -362,7 +447,7 @@ export class DailyQuestionService {
|
|
|
362
447
|
for (const bank of data.banks) {
|
|
363
448
|
if (bank.subjectId && bank.subjectId !== subject.id) continue
|
|
364
449
|
for (const item of bank.items) {
|
|
365
|
-
if (!item
|
|
450
|
+
if (!isUsableBankItem(item)) continue
|
|
366
451
|
const qid = bankQuestionId(bank.id, item.id)
|
|
367
452
|
const existing = data.questions[qid]
|
|
368
453
|
if (existing && existing.subjectId !== subject.id) continue
|
|
@@ -378,10 +463,12 @@ export class DailyQuestionService {
|
|
|
378
463
|
return { ...chosen, repeated }
|
|
379
464
|
}
|
|
380
465
|
|
|
381
|
-
async publishBankQuestion(picked, subject, date, key) {
|
|
466
|
+
async publishBankQuestion(picked, subject, date, key, signal) {
|
|
382
467
|
const { bank, item, qid, repeated } = picked
|
|
383
468
|
const stamp = nowISO()
|
|
384
469
|
await this.store.update((draft) => {
|
|
470
|
+
signal?.throwIfAborted()
|
|
471
|
+
if (!draft.settings.subjects.includes(subject.id)) throw new Error('该科目已停用')
|
|
385
472
|
const existing = draft.questions[qid]
|
|
386
473
|
if (existing) {
|
|
387
474
|
existing.assignedCount = (existing.assignedCount || 1) + 1
|
|
@@ -454,11 +541,15 @@ export class DailyQuestionService {
|
|
|
454
541
|
|
|
455
542
|
/* ------------------------------ 题目视图 ------------------------------ */
|
|
456
543
|
|
|
457
|
-
questionView(data, question) {
|
|
544
|
+
questionView(data, question, { attemptId, date = todayISO() } = {}) {
|
|
458
545
|
const attempts = this.attemptsOf(data, question.id)
|
|
459
|
-
const
|
|
460
|
-
const
|
|
546
|
+
const isToday=data.daily[`${date}|${question.subjectId}`]?.questionId===question.id
|
|
547
|
+
const currentAttempts=isToday?attempts.filter(attempt=>attemptDate(attempt)===date):attempts
|
|
548
|
+
const revealed = currentAttempts.length > 0
|
|
549
|
+
const last = (attemptId ? currentAttempts.find((attempt) => attempt.id === attemptId) : currentAttempts[currentAttempts.length - 1]) || null
|
|
461
550
|
const answer = question.currentAnswer
|
|
551
|
+
const isChoice = CHOICE_TYPES.has(question.type)
|
|
552
|
+
const canSelect = isChoice && hasChoiceOptions(question.options)
|
|
462
553
|
return {
|
|
463
554
|
id: question.id,
|
|
464
555
|
subjectId: question.subjectId,
|
|
@@ -466,13 +557,23 @@ export class DailyQuestionService {
|
|
|
466
557
|
type: question.type,
|
|
467
558
|
typeLabel: QUESTION_TYPE_LABEL[question.type] || question.type,
|
|
468
559
|
/** 完形填空 / 阅读理解按选择题判分,前端据此渲染选项而不是“作答完毕”。 */
|
|
469
|
-
isChoice
|
|
560
|
+
isChoice,
|
|
561
|
+
canSelect,
|
|
470
562
|
stem: question.stem,
|
|
471
563
|
options: question.options || [],
|
|
472
564
|
knowledge: question.knowledge || [],
|
|
473
565
|
difficulty: question.difficulty || 3,
|
|
566
|
+
examAlignment: question.examAlignment ? {
|
|
567
|
+
...question.examAlignment,
|
|
568
|
+
rationale: revealed ? question.examAlignment.rationale : '',
|
|
569
|
+
warnings: revealed ? question.examAlignment.warnings || [] : [],
|
|
570
|
+
outlineEvidence: revealed ? question.examAlignment.outlineEvidence : [],
|
|
571
|
+
paperEvidence: revealed ? question.examAlignment.paperEvidence : [],
|
|
572
|
+
} : null,
|
|
474
573
|
source: question.source,
|
|
475
|
-
assignedDate: question.assignedDate,
|
|
574
|
+
assignedDate: isToday ? date : question.lastAssignedDate || question.assignedDate,
|
|
575
|
+
practiceDate:isToday?date:null,
|
|
576
|
+
todayAttemptCount:isToday?currentAttempts.length:0,
|
|
476
577
|
createdAt: question.createdAt,
|
|
477
578
|
assignedCount: question.assignedCount || 1,
|
|
478
579
|
repeated: Boolean(question.repeated),
|
|
@@ -485,6 +586,7 @@ export class DailyQuestionService {
|
|
|
485
586
|
mode: attempt.mode,
|
|
486
587
|
choice: attempt.choice || '',
|
|
487
588
|
judgedAt: attempt.judgedAt || null,
|
|
589
|
+
practiceDate:attemptDate(attempt),
|
|
488
590
|
})),
|
|
489
591
|
attemptCount: attempts.length,
|
|
490
592
|
lastAttempt: last ? { id: last.id, at: last.at, result: last.result, mode: last.mode, choice: last.choice || '' } : null,
|
|
@@ -505,7 +607,7 @@ export class DailyQuestionService {
|
|
|
505
607
|
? {
|
|
506
608
|
versionId: answer.versionId,
|
|
507
609
|
answer: answer.answer,
|
|
508
|
-
answerKey: answer.answerKey
|
|
610
|
+
answerKey: canSelect && validChoiceKey(question.type, answer.answerKey, question.options) ? answer.answerKey : '',
|
|
509
611
|
explanation: answer.explanation,
|
|
510
612
|
verified: Boolean(answer.verified),
|
|
511
613
|
basis: answer.basis,
|
|
@@ -541,7 +643,7 @@ export class DailyQuestionService {
|
|
|
541
643
|
typeLabel: QUESTION_TYPE_LABEL[question.type] || question.type,
|
|
542
644
|
summary: summariseStem(question.stem),
|
|
543
645
|
stem: question.stem,
|
|
544
|
-
assignedDate: question.assignedDate,
|
|
646
|
+
assignedDate: question.lastAssignedDate || question.assignedDate,
|
|
545
647
|
attemptCount: attempts.length,
|
|
546
648
|
favorite: Boolean(question.favoriteAt),
|
|
547
649
|
wrongEver: this.isWrong(question, attempts),
|
|
@@ -552,9 +654,11 @@ export class DailyQuestionService {
|
|
|
552
654
|
}
|
|
553
655
|
}
|
|
554
656
|
|
|
555
|
-
async listQuestions({ subjectId, kind = 'all' } = {}) {
|
|
657
|
+
async listQuestions({ subjectId, kind = 'all', page = 1, pageSize = 50, idsOnly = false } = {}) {
|
|
556
658
|
const data = await this.data()
|
|
557
659
|
if (!subjectId) throw new Error('缺少科目')
|
|
660
|
+
if(!['all','favorite','wrong','today'].includes(kind))throw new Error('未知题目栏目')
|
|
661
|
+
if(!Number.isInteger(page)||page<1||!Number.isInteger(pageSize)||pageSize<1||pageSize>100)throw new Error('分页参数无效,每页最多100道题')
|
|
558
662
|
let questions = Object.values(data.questions).filter((question) => question.subjectId === subjectId)
|
|
559
663
|
if (kind === 'favorite') questions = questions.filter((question) => question.favoriteAt)
|
|
560
664
|
if (kind === 'wrong') {
|
|
@@ -567,23 +671,28 @@ export class DailyQuestionService {
|
|
|
567
671
|
const record = data.daily[`${todayISO()}|${subjectId}`]
|
|
568
672
|
questions = questions.filter((question) => question.id === record?.questionId)
|
|
569
673
|
}
|
|
570
|
-
questions.sort((a, b) => String(b.assignedDate).localeCompare(String(a.assignedDate))
|
|
571
|
-
|| String(b.createdAt).localeCompare(String(a.createdAt)))
|
|
674
|
+
questions.sort((a, b) => String(b.lastAssignedDate || b.assignedDate).localeCompare(String(a.lastAssignedDate || a.assignedDate))
|
|
675
|
+
|| String(b.createdAt).localeCompare(String(a.createdAt)) || String(b.id).localeCompare(String(a.id)))
|
|
676
|
+
const total=questions.length,totalPages=Math.max(1,Math.ceil(total/pageSize))
|
|
677
|
+
if(idsOnly===true)return {ok:true,kind,subjectId,total,ids:questions.map(q=>q.id)}
|
|
678
|
+
page=Math.min(page,totalPages)
|
|
572
679
|
return {
|
|
573
680
|
ok: true,
|
|
574
681
|
kind,
|
|
575
682
|
subjectId,
|
|
576
|
-
|
|
683
|
+
total,page,pageSize,totalPages,
|
|
684
|
+
items: questions.slice((page-1)*pageSize,page*pageSize).map((question) => this.summaryView(data, question)),
|
|
577
685
|
}
|
|
578
686
|
}
|
|
579
687
|
|
|
580
|
-
async getQuestion({ qid }) {
|
|
688
|
+
async getQuestion({ qid, attemptId }) {
|
|
581
689
|
const data = await this.data()
|
|
582
690
|
const question = data.questions[qid]
|
|
583
691
|
if (!question) throw new Error('题目不存在(可能已随题库删除而失效)')
|
|
584
692
|
const thread = this.threadView(data, qid)
|
|
585
|
-
const
|
|
586
|
-
|
|
693
|
+
const date=todayISO()
|
|
694
|
+
const isToday = data.daily[`${date}|${question.subjectId}`]?.questionId === qid
|
|
695
|
+
return { ok: true, question: this.questionView(data, question, { date, attemptId }), thread, isToday }
|
|
587
696
|
}
|
|
588
697
|
|
|
589
698
|
threadView(data, qid) {
|
|
@@ -599,71 +708,67 @@ export class DailyQuestionService {
|
|
|
599
708
|
/* ------------------------------ 作答 ------------------------------ */
|
|
600
709
|
|
|
601
710
|
async reveal({ qid, mode = 'submit', choice = '', attemptId }) {
|
|
602
|
-
const
|
|
603
|
-
const question = data.questions[qid]
|
|
604
|
-
if (!question) throw new Error('题目不存在')
|
|
605
|
-
const isChoice = CHOICE_TYPES.has(question.type)
|
|
606
|
-
let result = 'pending'
|
|
607
|
-
let normalizedChoice = ''
|
|
608
|
-
if (mode === 'submit') {
|
|
609
|
-
if (!isChoice) throw new Error('主观题不支持提交选项')
|
|
610
|
-
normalizedChoice = normalizeChoice(choice, question)
|
|
611
|
-
const key = sortLetters(question.currentAnswer?.answerKey)
|
|
612
|
-
result = key && normalizedChoice === key ? 'correct' : 'wrong'
|
|
613
|
-
} else if (mode === 'giveup') {
|
|
614
|
-
result = 'wrong'
|
|
615
|
-
} else if (mode === 'done') {
|
|
616
|
-
result = 'pending'
|
|
617
|
-
} else {
|
|
618
|
-
throw new Error(`未知的作答方式:${mode}`)
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const stamp = nowISO()
|
|
622
|
-
const attempt = {
|
|
623
|
-
id: attemptId || randomUUID(),
|
|
624
|
-
questionId: qid,
|
|
625
|
-
at: stamp,
|
|
626
|
-
type: question.type,
|
|
627
|
-
mode,
|
|
628
|
-
choice: normalizedChoice,
|
|
629
|
-
result,
|
|
630
|
-
revealedAt: stamp,
|
|
631
|
-
judgedAt: mode === 'done' ? null : stamp,
|
|
632
|
-
}
|
|
633
|
-
|
|
711
|
+
const id = attemptId || randomUUID()
|
|
634
712
|
const duplicated = await this.store.update((draft) => {
|
|
635
|
-
const
|
|
636
|
-
if (!
|
|
637
|
-
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)
|
|
638
716
|
if (existing) {
|
|
639
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
|
+
}
|
|
640
721
|
return true
|
|
641
722
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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}`)
|
|
735
|
+
}
|
|
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
|
|
645
744
|
}
|
|
646
|
-
|
|
745
|
+
question.updatedAt = stamp
|
|
647
746
|
return false
|
|
648
747
|
})
|
|
649
748
|
const fresh = await this.data()
|
|
650
|
-
|
|
749
|
+
if (!fresh.questions[qid]) throw new Error('题目已删除')
|
|
750
|
+
return { ok: true, question: this.questionView(fresh, fresh.questions[qid], { attemptId: id }), duplicated }
|
|
651
751
|
}
|
|
652
752
|
|
|
653
|
-
async judge({ qid, verdict }) {
|
|
753
|
+
async judge({ qid, verdict, attemptId }) {
|
|
654
754
|
if (!['correct', 'wrong'].includes(verdict)) throw new Error('判定结果不合法')
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
755
|
+
if (typeof attemptId !== 'string' || !attemptId) throw new Error('缺少本次作答ID,请重新打开题目后判定')
|
|
756
|
+
const duplicated=await this.store.update((draft) => {
|
|
757
|
+
if (!draft.questions[qid]) throw new Error('题目不存在')
|
|
758
|
+
const target = draft.attempts.find((attempt) => attempt.id === attemptId && attempt.questionId === qid)
|
|
759
|
+
if (!target) throw new Error('本次作答不存在或不属于这道题')
|
|
760
|
+
if (target.result !== 'pending' || target.judgedAt) {
|
|
761
|
+
if (target.result === verdict && target.judgedAt) return true
|
|
762
|
+
throw new Error('本次作答已判定,不能覆盖或改判其他作答')
|
|
763
|
+
}
|
|
659
764
|
target.result = verdict
|
|
660
765
|
target.judgedAt = nowISO()
|
|
661
766
|
const question = draft.questions[qid]
|
|
662
767
|
if (question) question.updatedAt = nowISO()
|
|
663
|
-
return
|
|
768
|
+
return false
|
|
664
769
|
})
|
|
665
770
|
const data = await this.data()
|
|
666
|
-
return { ok: true, question: this.questionView(data, data.questions[qid]) }
|
|
771
|
+
return { ok: true, question: this.questionView(data, data.questions[qid], {attemptId}), duplicated }
|
|
667
772
|
}
|
|
668
773
|
|
|
669
774
|
async favorite({ qid, value }) {
|
|
@@ -701,7 +806,7 @@ export class DailyQuestionService {
|
|
|
701
806
|
if (!Object.hasOwn(draft.questions, id)) throw new Error('部分题目已不存在,请刷新列表后重试')
|
|
702
807
|
return draft.questions[id]
|
|
703
808
|
})
|
|
704
|
-
const protectedCount = questions.filter((q) => q.favoriteAt || this.isWrong(q, draft.
|
|
809
|
+
const protectedCount = questions.filter((q) => q.favoriteAt || this.isWrong(q, this.attemptsOf(draft,q.id))).length
|
|
705
810
|
if (action === 'delete' && protectedCount && confirmProtected !== true) {
|
|
706
811
|
return { requiresConfirmation: true, protectedCount }
|
|
707
812
|
}
|
|
@@ -734,6 +839,10 @@ export class DailyQuestionService {
|
|
|
734
839
|
/* ------------------------------ 答案修订 ------------------------------ */
|
|
735
840
|
|
|
736
841
|
async revise({ qid, reason = '' }) {
|
|
842
|
+
return this.runTask('revise:'+qid, {kind:'revise',qid}, (signal) => this.createRevision({qid,reason}, signal))
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async createRevision({ qid, reason }, signal) {
|
|
737
846
|
const data = await this.data()
|
|
738
847
|
const question = data.questions[qid]
|
|
739
848
|
if (!question) throw new Error('题目不存在')
|
|
@@ -746,6 +855,7 @@ export class DailyQuestionService {
|
|
|
746
855
|
question,
|
|
747
856
|
currentAnswer: question.currentAnswer,
|
|
748
857
|
reason,
|
|
858
|
+
signal,
|
|
749
859
|
})
|
|
750
860
|
const candidate = {
|
|
751
861
|
candidateId: `c_${randomUUID().slice(0, 8)}`,
|
|
@@ -758,6 +868,7 @@ export class DailyQuestionService {
|
|
|
758
868
|
}
|
|
759
869
|
await this.store.update((draft) => {
|
|
760
870
|
const target = draft.questions[qid]
|
|
871
|
+
signal.throwIfAborted()
|
|
761
872
|
if (!target) throw new Error('题目不存在')
|
|
762
873
|
target.pendingRevision = candidate
|
|
763
874
|
return draft
|
|
@@ -820,6 +931,24 @@ export class DailyQuestionService {
|
|
|
820
931
|
/* ------------------------------ 追问 ------------------------------ */
|
|
821
932
|
|
|
822
933
|
async followup({ qid, message, clientMessageId }) {
|
|
934
|
+
const messageId = clientMessageId || randomUUID()
|
|
935
|
+
const key = JSON.stringify([qid, messageId])
|
|
936
|
+
if (this.inflightFollowups.has(key)) return this.inflightFollowups.get(key)
|
|
937
|
+
const executionId = randomUUID()
|
|
938
|
+
const task = this.runTask('followup:'+key, {kind:'followup',qid,clientMessageId:messageId},
|
|
939
|
+
(signal) => this.createFollowup({ qid, message, clientMessageId: messageId, executionId }, signal))
|
|
940
|
+
.catch(async (error) => {
|
|
941
|
+
await this.store.update((draft) => {
|
|
942
|
+
const target = draft.threads[qid]?.messages.find((item) => item.id === messageId)
|
|
943
|
+
if (target?.executionId === executionId && target.status === 'pending') target.status = 'failed'
|
|
944
|
+
}).catch(() => {})
|
|
945
|
+
throw error
|
|
946
|
+
}).finally(() => this.inflightFollowups.delete(key))
|
|
947
|
+
this.inflightFollowups.set(key, task)
|
|
948
|
+
return task
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async createFollowup({ qid, message, clientMessageId, executionId }, signal) {
|
|
823
952
|
const text = String(message || '').trim()
|
|
824
953
|
if (!text) throw new Error('追问内容不能为空')
|
|
825
954
|
if (text.length > 4000) throw new Error('追问内容过长,请精简后再发送')
|
|
@@ -838,16 +967,20 @@ export class DailyQuestionService {
|
|
|
838
967
|
const messageId = failed ? failed.id : (clientMessageId || randomUUID())
|
|
839
968
|
|
|
840
969
|
// 先落盘用户消息:即使回复失败,用户输入也不会丢,并且可以按同一 id 重试。
|
|
841
|
-
await this.store.update((draft) => {
|
|
970
|
+
const queued = await this.store.update((draft) => {
|
|
971
|
+
signal.throwIfAborted()
|
|
842
972
|
if (!draft.questions[qid]) throw new Error('题目已删除')
|
|
843
973
|
if (!draft.threads[qid]) {
|
|
844
|
-
draft.threads[qid] = { threadId: qid, questionId: qid, messages: [], updatedAt: stamp }
|
|
974
|
+
draft.threads[qid] = { threadId: qid, questionId: qid, generation: randomUUID(), messages: [], updatedAt: stamp }
|
|
845
975
|
}
|
|
846
976
|
const thread = draft.threads[qid]
|
|
977
|
+
if (!thread.generation) thread.generation = randomUUID()
|
|
847
978
|
const target = thread.messages.find((item) => item.id === messageId)
|
|
848
979
|
if (target) {
|
|
980
|
+
if (target.status !== 'failed') return { duplicated: true, generation: thread.generation }
|
|
849
981
|
target.text = text
|
|
850
982
|
target.status = 'pending'
|
|
983
|
+
target.executionId = executionId
|
|
851
984
|
} else {
|
|
852
985
|
thread.messages.push({
|
|
853
986
|
id: messageId,
|
|
@@ -857,17 +990,19 @@ export class DailyQuestionService {
|
|
|
857
990
|
text,
|
|
858
991
|
createdAt: stamp,
|
|
859
992
|
status: 'pending',
|
|
993
|
+
executionId,
|
|
860
994
|
})
|
|
861
995
|
}
|
|
862
996
|
thread.updatedAt = stamp
|
|
863
|
-
return
|
|
997
|
+
return { duplicated: false, generation: thread.generation }
|
|
864
998
|
})
|
|
999
|
+
if (queued.duplicated) return { ok: true, thread: this.threadView(await this.data(), qid), duplicated: true }
|
|
865
1000
|
|
|
866
1001
|
const history = (await this.data()).threads[qid]?.messages || []
|
|
867
1002
|
const setStatus = async (status) => {
|
|
868
1003
|
await this.store.update((draft) => {
|
|
869
1004
|
const target = draft.threads[qid]?.messages.find((item) => item.id === messageId)
|
|
870
|
-
if (target) target.status = status
|
|
1005
|
+
if (target?.executionId === executionId) target.status = status
|
|
871
1006
|
return draft
|
|
872
1007
|
}).catch(() => {})
|
|
873
1008
|
}
|
|
@@ -879,6 +1014,7 @@ export class DailyQuestionService {
|
|
|
879
1014
|
answerDetail: question.currentAnswer,
|
|
880
1015
|
history: history.filter((item) => item.id !== messageId),
|
|
881
1016
|
message: text,
|
|
1017
|
+
signal,
|
|
882
1018
|
})
|
|
883
1019
|
const assistantMessage = {
|
|
884
1020
|
id: randomUUID(),
|
|
@@ -890,11 +1026,12 @@ export class DailyQuestionService {
|
|
|
890
1026
|
status: 'sent',
|
|
891
1027
|
}
|
|
892
1028
|
await this.store.update((draft) => {
|
|
1029
|
+
signal.throwIfAborted()
|
|
893
1030
|
if (!draft.questions[qid]) throw new Error('题目已删除')
|
|
894
|
-
if (!draft.threads[qid]) draft.threads[qid] = { threadId: qid, questionId: qid, messages: [], updatedAt: nowISO() }
|
|
895
1031
|
const thread = draft.threads[qid]
|
|
896
|
-
const target = thread
|
|
897
|
-
if (target
|
|
1032
|
+
const target = thread?.messages.find((item) => item.id === messageId)
|
|
1033
|
+
if (!thread || thread.generation !== queued.generation || !target || target.executionId !== executionId || target.status !== 'pending') throw new Error('本次追问已清空或失效,已丢弃迟到的回复')
|
|
1034
|
+
target.status = 'sent'
|
|
898
1035
|
thread.messages.push(assistantMessage)
|
|
899
1036
|
thread.updatedAt = assistantMessage.createdAt
|
|
900
1037
|
return draft
|
|
@@ -909,9 +1046,14 @@ export class DailyQuestionService {
|
|
|
909
1046
|
|
|
910
1047
|
async clearThread({ qid }) {
|
|
911
1048
|
await this.store.update((draft) => {
|
|
912
|
-
if (draft.threads[qid])
|
|
1049
|
+
if (draft.threads[qid]) {
|
|
1050
|
+
draft.threads[qid].messages = []
|
|
1051
|
+
draft.threads[qid].generation = randomUUID()
|
|
1052
|
+
draft.threads[qid].updatedAt = nowISO()
|
|
1053
|
+
}
|
|
913
1054
|
return draft
|
|
914
1055
|
})
|
|
1056
|
+
await this.cancelTask({kind:'followup',qid})
|
|
915
1057
|
const data = await this.data()
|
|
916
1058
|
return { ok: true, thread: this.threadView(data, qid) }
|
|
917
1059
|
}
|
|
@@ -925,7 +1067,7 @@ export class DailyQuestionService {
|
|
|
925
1067
|
if (!base64) throw new Error('缺少 PDF 文件内容')
|
|
926
1068
|
const bytes = Buffer.from(String(base64), 'base64')
|
|
927
1069
|
if (bytes.length > 40 * 1024 * 1024) throw new Error('PDF 文件过大(上限 40MB)')
|
|
928
|
-
const extracted = extractBankPdf(bytes, fileName || name || '')
|
|
1070
|
+
const extracted = await extractBankPdf(bytes, fileName || name || '')
|
|
929
1071
|
content = extracted.text
|
|
930
1072
|
warnings = extracted.warnings
|
|
931
1073
|
} else {
|
|
@@ -966,12 +1108,13 @@ export class DailyQuestionService {
|
|
|
966
1108
|
knowledge: item.knowledge,
|
|
967
1109
|
difficulty: item.difficulty,
|
|
968
1110
|
issues: item.issues,
|
|
1111
|
+
usable: isUsableBankItem(item),
|
|
969
1112
|
answerStatus: item.answerStatus,
|
|
970
1113
|
})),
|
|
971
1114
|
stats: {
|
|
972
1115
|
total: parsed.items.length,
|
|
973
1116
|
withAnswer: parsed.items.filter((item) => item.answer).length,
|
|
974
|
-
blocked: parsed.items.
|
|
1117
|
+
blocked: parsed.items.length - parsed.usable,
|
|
975
1118
|
},
|
|
976
1119
|
}
|
|
977
1120
|
}
|
|
@@ -983,10 +1126,14 @@ export class DailyQuestionService {
|
|
|
983
1126
|
}
|
|
984
1127
|
}
|
|
985
1128
|
|
|
986
|
-
async commitBank({ importId, name, subjectId }) {
|
|
1129
|
+
async commitBank({ importId, name, subjectId, excludedIndexes = [] }) {
|
|
987
1130
|
const pending = this.imports.get(importId)
|
|
988
1131
|
if (!pending) throw new Error('导入结果已过期,请重新选择文件导入')
|
|
989
|
-
|
|
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))
|
|
990
1137
|
if (!usable.length) throw new Error('没有可用的题目可以入库:请检查文件结构后重新导入')
|
|
991
1138
|
const bankId = `bank_${randomUUID().slice(0, 8)}`
|
|
992
1139
|
const bank = {
|
|
@@ -1010,11 +1157,13 @@ export class DailyQuestionService {
|
|
|
1010
1157
|
answerStatus: item.answer ? 'unverified' : 'missing',
|
|
1011
1158
|
})),
|
|
1012
1159
|
}
|
|
1013
|
-
await this.store.update((draft) => {
|
|
1014
|
-
draft.banks.push(bank)
|
|
1015
|
-
return draft
|
|
1016
|
-
})
|
|
1017
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
|
+
}
|
|
1018
1167
|
const data = await this.data()
|
|
1019
1168
|
return { ok: true, bank: this.bankViews(data).find((item) => item.id === bankId), banks: this.bankViews(data), subjects: this.subjectViews(data) }
|
|
1020
1169
|
}
|
|
@@ -1091,11 +1240,11 @@ export class DailyQuestionService {
|
|
|
1091
1240
|
const kept = Object.values(data.questions).filter((question) => question.subjectId === subjectId).length
|
|
1092
1241
|
const result = await this.saveSettings({
|
|
1093
1242
|
subjects: remaining,
|
|
1094
|
-
activeSubject: data.settings.activeSubject === subjectId ? (remaining[0] || null) : data.settings.activeSubject,
|
|
1243
|
+
activeSubject: data.settings.activeSubject === subjectId && !kept ? (remaining[0] || null) : data.settings.activeSubject,
|
|
1095
1244
|
})
|
|
1096
1245
|
return {
|
|
1097
1246
|
...result,
|
|
1098
|
-
message: `科目已移出每日出题,历史数据保留:${kept}
|
|
1247
|
+
message: `科目已移出每日出题,历史数据保留:${kept} 道题目及其作答记录可通过“已停用科目”查看。`,
|
|
1099
1248
|
}
|
|
1100
1249
|
}
|
|
1101
1250
|
}
|