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/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: defaultTargetYear(),
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(data.settings.targetYear)) data.settings.targetYear = base.settings.targetYear
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
- lines.push('--- 标准答案(已校验的正式版本)---')
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.0.0",
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
  ],