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/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',
|
|
@@ -100,6 +112,22 @@ export const SUBJECT_CATALOG = [
|
|
|
100
112
|
|
|
101
113
|
const CATALOG_BY_ID = new Map(SUBJECT_CATALOG.map((item) => [item.id, item]))
|
|
102
114
|
|
|
115
|
+
/** 参考考试独立于学习科目;不会因此在每日科目列表中新增408。 */
|
|
116
|
+
export const REFERENCE_EXAMS = [
|
|
117
|
+
...SUBJECT_CATALOG.map(({id,name,code}) => ({id,name,code})),
|
|
118
|
+
{id:'cs408',name:'计算机学科专业基础',code:'408'},
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
export function referenceExamFor(subject) {
|
|
122
|
+
if (!subject?.custom) return subject
|
|
123
|
+
if (!subject.referenceSubjectId || subject.referenceSubjectId === 'auto') return {id:'auto_'+subject.id,name:subject.name,code:subject.code || ''}
|
|
124
|
+
if (subject.referenceSubjectId === 'independent') {
|
|
125
|
+
const name = typeof subject.referenceExamName === 'string' ? subject.referenceExamName.trim() : ''
|
|
126
|
+
return name ? {id:'independent_'+subject.id,name,code:subject.referenceExamCode || ''} : undefined
|
|
127
|
+
}
|
|
128
|
+
return REFERENCE_EXAMS.find((exam) => exam.id === subject.referenceSubjectId)
|
|
129
|
+
}
|
|
130
|
+
|
|
103
131
|
/** 全部可选科目 = 内置目录 + 用户自定义。 */
|
|
104
132
|
export function allSubjects(customSubjects = []) {
|
|
105
133
|
const extra = Array.isArray(customSubjects)
|
|
@@ -113,6 +141,9 @@ export function allSubjects(customSubjects = []) {
|
|
|
113
141
|
outline: Array.isArray(item.outline) ? item.outline : [],
|
|
114
142
|
guidance: typeof item.guidance === 'string' ? item.guidance : '',
|
|
115
143
|
custom: true,
|
|
144
|
+
referenceSubjectId: typeof item.referenceSubjectId === 'string' ? item.referenceSubjectId : '',
|
|
145
|
+
referenceExamName: typeof item.referenceExamName === 'string' ? item.referenceExamName : '',
|
|
146
|
+
referenceExamCode: typeof item.referenceExamCode === 'string' ? item.referenceExamCode : '',
|
|
116
147
|
}))
|
|
117
148
|
: []
|
|
118
149
|
return [...SUBJECT_CATALOG, ...extra]
|
package/lib/tutor.js
CHANGED
|
@@ -7,7 +7,7 @@ const TUTOR_SYSTEM = `你是中国大陆硕士研究生招生考试的辅导老
|
|
|
7
7
|
- 只讨论当前这道题:题干、选项、标准答案、解析以及由它们直接延伸的知识点。如果学生的提问明显超出这道题的范围,先说明它与本题的关系,再简短回答,不要把它当成跨题的通用聊天。
|
|
8
8
|
- 用 Markdown 回答;数学公式用 LaTeX(行内 $...$,独立公式 $$...$$)。
|
|
9
9
|
- 解释要一步步来,指出关键依据,不要只给结论。
|
|
10
|
-
-
|
|
10
|
+
- 你给出的解释是对当前题目参考答案的补充说明,不能宣称修改了已保存的答案与解析;模型生成、导入或修订的答案都不能称为“官方答案”。
|
|
11
11
|
- 如果题目资料里标记答案尚未核验或缺少参考答案,必须如实说明,不要把未核验的内容说成确定结论。
|
|
12
12
|
- 不要编造与题目无关的事实。`
|
|
13
13
|
|
|
@@ -29,12 +29,13 @@ export function describeQuestionContext(question, answerDetail) {
|
|
|
29
29
|
for (const option of question.options) lines.push(`${option.key}. ${option.text}`)
|
|
30
30
|
}
|
|
31
31
|
lines.push('')
|
|
32
|
-
if (answerDetail) {
|
|
33
|
-
|
|
32
|
+
if (answerDetail?.answer?.trim()) {
|
|
33
|
+
const status=answerDetail.verified===true?'已通过独立校验,仍非官方认证':'尚未独立核验,可能有误,请先核对'
|
|
34
|
+
lines.push(`--- 参考答案(${status})---`)
|
|
34
35
|
lines.push(answerDetail.answer)
|
|
35
36
|
lines.push('')
|
|
36
|
-
lines.push(
|
|
37
|
-
lines.push(answerDetail.explanation)
|
|
37
|
+
lines.push(`--- 参考解析(${status})---`)
|
|
38
|
+
lines.push(answerDetail.explanation||'(尚未提供解析)')
|
|
38
39
|
} else {
|
|
39
40
|
lines.push('--- 答案状态 ---')
|
|
40
41
|
lines.push('本题的参考答案与解析尚未通过独立校验或尚未生成,请如实说明,不要给出确定的“标准答案”。')
|
package/lib/updates.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
export const PLUGIN_VERSION=JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version
|
|
4
|
+
export const UPDATE_INTERVAL=6*60*60*1000
|
|
5
|
+
const REGISTRY='https://registry.npmjs.org/daily-question/latest'
|
|
6
|
+
|
|
7
|
+
function versionParts(value) {
|
|
8
|
+
if(typeof value!=='string')throw new Error('npm 返回的版本号无效')
|
|
9
|
+
const match=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/.exec(value)
|
|
10
|
+
if(!match||match[4]?.split('.').some((p)=>/^0\d+$/.test(p)))throw new Error('npm 返回的版本号无效')
|
|
11
|
+
return {numbers:match.slice(1,4).map(BigInt),pre:match[4]?.split('.')}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function newerVersion(latest,current) {
|
|
15
|
+
const a=versionParts(latest),b=versionParts(current)
|
|
16
|
+
for(let i=0;i<3;i++)if(a.numbers[i]!==b.numbers[i])return a.numbers[i]>b.numbers[i]
|
|
17
|
+
if(!a.pre||!b.pre)return Boolean(!a.pre&&b.pre)
|
|
18
|
+
for(let i=0;i<Math.max(a.pre.length,b.pre.length);i++) {
|
|
19
|
+
const x=a.pre[i],y=b.pre[i]
|
|
20
|
+
if(x===y)continue
|
|
21
|
+
if(x===undefined||y===undefined)return y===undefined
|
|
22
|
+
const nx=/^\d+$/.test(x),ny=/^\d+$/.test(y)
|
|
23
|
+
return nx&&ny?BigInt(x)>BigInt(y):nx!==ny?!nx:x>y
|
|
24
|
+
}
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createUpdateChecker({fetchImpl=globalThis.fetch,currentVersion=PLUGIN_VERSION,now=Date.now}={}) {
|
|
29
|
+
let cached,pending
|
|
30
|
+
return async ({force=false}={})=>{
|
|
31
|
+
if(pending)return {...await pending}
|
|
32
|
+
if(force!==true&&cached&&now()-cached.at>=0&&now()-cached.at<UPDATE_INTERVAL)return {...cached.result}
|
|
33
|
+
pending=(async()=>{
|
|
34
|
+
try {
|
|
35
|
+
const response=await fetchImpl(REGISTRY,{signal:AbortSignal.timeout(10000),redirect:'error',headers:{accept:'application/json'}})
|
|
36
|
+
if(!response.ok)throw new Error(`npm 服务返回 ${response.status}`)
|
|
37
|
+
const metadata=await response.json()
|
|
38
|
+
if(metadata?.name!=='daily-question')throw new Error('npm 返回的包信息不匹配')
|
|
39
|
+
const updateAvailable=newerVersion(metadata.version,currentVersion)
|
|
40
|
+
const at=now(),result={ok:true,currentVersion,latestVersion:metadata.version,updateAvailable,checkedAt:new Date(at).toISOString(),packageUrl:'https://www.npmjs.com/package/daily-question'}
|
|
41
|
+
cached={at,result}
|
|
42
|
+
return {...result}
|
|
43
|
+
} catch(error) {
|
|
44
|
+
throw new Error(`检查更新失败:${error.name==='TimeoutError'?'连接 npm 超时,请稍后重试':error.message}`)
|
|
45
|
+
}
|
|
46
|
+
})()
|
|
47
|
+
try{return {...await pending}}finally{pending=null}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "daily-question",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "考研每日一题 — DSH 插件:每科每天一道独立校验过的考研题,含倒计时、错题本、收藏与按题隔离的追问。",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"node": ">=20.3.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"test": "node test/check.mjs && node test/service-check.mjs && node test/render-check.mjs"
|
|
34
|
+
"test": "node test/check.mjs && node test/service-check.mjs && node test/tutor-status-check.mjs && node test/attempt-binding-check.mjs && node test/list-performance-check.mjs && node test/daily-attempt-check.mjs && node test/render-check.mjs && node test/ui-race-check.mjs && node test/followup-check.mjs && node test/task-check.mjs && node test/generation-check.mjs && node test/generation-history-check.mjs && node test/reference-check.mjs && node test/reference-reader-check.mjs && node test/research-check.mjs && node test/pdf-safety-check.mjs && node test/update-check.mjs",
|
|
35
|
+
"test:live": "node test/live-check.mjs"
|
|
35
36
|
},
|
|
36
37
|
"exports": {
|
|
37
38
|
".": "./index.js",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"cordis.patch.yml",
|
|
46
47
|
"locale/*.json",
|
|
47
48
|
"icon.svg",
|
|
49
|
+
"CHANGELOG.md",
|
|
48
50
|
"lib/*.js",
|
|
49
51
|
"vendor/mathjax/*"
|
|
50
52
|
],
|