@zhin.js/plugin-answers 0.0.0-stage → 0.0.2

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.
Files changed (57) hide show
  1. package/README.md +61 -2
  2. package/commands//345/245/275/346/227/240/350/201/212/index.js +18 -0
  3. package/commands//345/245/275/346/227/240/350/201/212/index.ts +17 -0
  4. package/commands//347/247/221/347/233/256/344/270/200/index.js +19 -0
  5. package/commands//347/247/221/347/233/256/344/270/200/index.ts +17 -0
  6. package/commands//347/255/224/346/241/210/344/271/213/344/271/246/index.js +22 -0
  7. package/commands//347/255/224/346/241/210/344/271/213/344/271/246/index.ts +21 -0
  8. package/lib/book-data.d.ts +6 -0
  9. package/lib/book-data.d.ts.map +1 -0
  10. package/lib/book-data.js +87 -0
  11. package/lib/book-data.js.map +1 -0
  12. package/lib/book.d.ts +17 -0
  13. package/lib/book.d.ts.map +1 -0
  14. package/lib/book.js +37 -0
  15. package/lib/book.js.map +1 -0
  16. package/lib/config.d.ts +23 -0
  17. package/lib/config.d.ts.map +1 -0
  18. package/lib/config.js +19 -0
  19. package/lib/config.js.map +1 -0
  20. package/lib/driving-quiz-data.d.ts +11 -0
  21. package/lib/driving-quiz-data.d.ts.map +1 -0
  22. package/lib/driving-quiz-data.js +36 -0
  23. package/lib/driving-quiz-data.js.map +1 -0
  24. package/lib/fun-quiz-data.d.ts +7 -0
  25. package/lib/fun-quiz-data.d.ts.map +1 -0
  26. package/lib/fun-quiz-data.js +47 -0
  27. package/lib/fun-quiz-data.js.map +1 -0
  28. package/lib/index.d.ts +9 -0
  29. package/lib/index.d.ts.map +1 -0
  30. package/lib/index.js +9 -0
  31. package/lib/index.js.map +1 -0
  32. package/lib/quiz-types.d.ts +14 -0
  33. package/lib/quiz-types.d.ts.map +1 -0
  34. package/lib/quiz-types.js +2 -0
  35. package/lib/quiz-types.js.map +1 -0
  36. package/lib/quiz.d.ts +52 -0
  37. package/lib/quiz.d.ts.map +1 -0
  38. package/lib/quiz.js +111 -0
  39. package/lib/quiz.js.map +1 -0
  40. package/lib/runtime.d.ts +4 -0
  41. package/lib/runtime.d.ts.map +1 -0
  42. package/lib/runtime.js +4 -0
  43. package/lib/runtime.js.map +1 -0
  44. package/middlewares/quiz-answer/index.js +21 -0
  45. package/middlewares/quiz-answer/index.ts +22 -0
  46. package/package.json +92 -3
  47. package/plugin.js +21 -0
  48. package/schema.json +41 -0
  49. package/src/book-data.ts +86 -0
  50. package/src/book.ts +48 -0
  51. package/src/config.ts +23 -0
  52. package/src/driving-quiz-data.ts +37 -0
  53. package/src/fun-quiz-data.ts +48 -0
  54. package/src/index.ts +20 -0
  55. package/src/quiz-types.ts +15 -0
  56. package/src/quiz.ts +136 -0
  57. package/src/runtime.ts +5 -0
@@ -0,0 +1,37 @@
1
+ import type { QuizQuestion } from './quiz-types.js';
2
+
3
+ /**
4
+ * 「科目一」交规小测。
5
+ *
6
+ * 题面、选项与解析均为本仓库原创(MIT),依据公开的法律法规整理:
7
+ * 《中华人民共和国道路交通安全法》(下称「道交法」)与《道路交通安全法实施条例》
8
+ * (下称「实施条例」)。法律法规文本本身不受著作权保护;本文件未复制任何驾考题库的题目或图片。
9
+ * 仅供娱乐和自测,具体规定以现行有效的法律法规和当地交管部门要求为准。
10
+ */
11
+ export const DRIVING_QUESTIONS: readonly QuizQuestion[] = Object.freeze([
12
+ { id: 'drv-01', category: '酒驾', question: '驾驶人每 100 毫升血液中的酒精含量达到多少毫克,属于醉酒驾驶?', options: ['20', '50', '80', '100'], answer: 'C', explanation: '按国家标准,20mg/100mL 以上不满 80 属于饮酒驾驶,80mg/100mL 以上属于醉酒驾驶。' },
13
+ { id: 'drv-02', category: '酒驾', question: '醉酒驾驶机动车(非营运),除依法追究刑事责任外,多少年内不得重新取得驾驶证?', options: ['1 年', '3 年', '5 年', '10 年'], answer: 'C', explanation: '道交法第九十一条:醉酒驾驶机动车的,吊销驾驶证,依法追究刑事责任;五年内不得重新取得机动车驾驶证。' },
14
+ { id: 'drv-03', category: '酒驾', question: '饮酒后驾驶机动车(非营运、首次),驾驶证会被暂扣多久?', options: ['1 个月', '3 个月', '6 个月', '12 个月'], answer: 'C', explanation: '道交法第九十一条:饮酒后驾驶机动车的,处暂扣六个月机动车驾驶证,并处一千元以上二千元以下罚款。' },
15
+ { id: 'drv-04', category: '事故', question: '造成交通事故后逃逸的,驾驶人会受到怎样的驾驶资格处理?', options: ['暂扣 6 个月', '吊销后 2 年可重考', '吊销且终生不得重新取得', '只罚款不处理驾照'], answer: 'C', explanation: '道交法第一百零一条:造成交通事故后逃逸的,吊销机动车驾驶证,且终生不得重新取得。' },
16
+ { id: 'drv-05', category: '事故', question: '发生仅造成轻微财产损失、基本事实清楚的交通事故,当事人应当?', options: ['原地等待交警到场', '先撤离现场再协商处理', '各自离开不用处理', '拍照后继续占道协商'], answer: 'B', explanation: '道交法第七十条:未造成人身伤亡、基本事实清楚的,应当先撤离现场再进行协商处理。' },
17
+ { id: 'drv-06', category: '记分', question: '机动车驾驶证记分周期为 12 个月,满分是多少分?', options: ['6 分', '10 分', '12 分', '24 分'], answer: 'C', explanation: '记分周期为 12 个月,满分为 12 分;一个周期内累计达到 12 分的,需参加学习和考试。' },
18
+ { id: 'drv-07', category: '驾驶证', question: '初次申领机动车驾驶证后的实习期是多久?', options: ['3 个月', '6 个月', '12 个月', '24 个月'], answer: 'C', explanation: '实施条例第二十二条:机动车驾驶人初次申领机动车驾驶证后的 12 个月为实习期。' },
19
+ { id: 'drv-08', category: '驾驶证', question: '实习期内驾驶机动车上高速公路行驶,应当怎样?', options: ['禁止上高速', '可以随意上高速', '由持相应或更高准驾车型驾驶证 3 年以上的驾驶人陪同', '只能在夜间上高速'], answer: 'C', explanation: '实施条例第二十二条:实习期内驾驶机动车上高速公路行驶,应当由持相应或者更高准驾车型驾驶证三年以上的驾驶人陪同。' },
20
+ { id: 'drv-09', category: '驾驶证', question: '初次取得的机动车驾驶证有效期为几年?', options: ['3 年', '6 年', '10 年', '长期'], answer: 'B', explanation: '驾驶证有效期分为 6 年、10 年和长期,初次取得为 6 年。' },
21
+ { id: 'drv-10', category: '车速', question: '高速公路上小型载客汽车的最高车速不得超过?', options: ['100 km/h', '110 km/h', '120 km/h', '130 km/h'], answer: 'C', explanation: '实施条例第七十八条:高速公路小型载客汽车最高车速不得超过每小时 120 公里,最低车速不得低于每小时 60 公里。' },
22
+ { id: 'drv-11', category: '车速', question: '高速公路上,机动车的最低车速不得低于?', options: ['40 km/h', '50 km/h', '60 km/h', '80 km/h'], answer: 'C', explanation: '实施条例第七十八条:高速公路最低车速不得低于每小时 60 公里。' },
23
+ { id: 'drv-12', category: '车速', question: '在没有道路中心线的城市道路上,机动车最高时速不得超过?', options: ['30 km/h', '40 km/h', '50 km/h', '70 km/h'], answer: 'A', explanation: '实施条例第四十五条:没有道路中心线的道路,城市道路为每小时 30 公里,公路为每小时 40 公里。' },
24
+ { id: 'drv-13', category: '车速', question: '机动车掉头、转弯、下陡坡时,最高时速不得超过?', options: ['20 km/h', '30 km/h', '40 km/h', '50 km/h'], answer: 'B', explanation: '实施条例第四十六条:掉头、转弯、下陡坡时,以及能见度 50 米以内等情形,最高时速不得超过 30 公里。' },
25
+ { id: 'drv-14', category: '高速', question: '高速公路上车速超过每小时 100 公里时,应与同车道前车保持多少米以上的距离?', options: ['50 米', '80 米', '100 米', '200 米'], answer: 'C', explanation: '实施条例第八十条:车速超过每小时 100 公里时,应当与同车道前车保持 100 米以上的距离。' },
26
+ { id: 'drv-15', category: '高速', question: '高速公路上能见度小于 200 米时,车速不得超过每小时多少公里?', options: ['40', '60', '80', '100'], answer: 'B', explanation: '实施条例第八十一条:能见度小于 200 米时,开启雾灯、近光灯、示廓灯和前后位灯,车速不得超过 60 公里,与同车道前车保持 100 米以上距离。' },
27
+ { id: 'drv-16', category: '高速', question: '在高速公路上,下列哪种行为是允许的?', options: ['倒车', '逆行', '穿越中央分隔带掉头', '按规定在应急车道停车处理故障'], answer: 'D', explanation: '实施条例第八十二条禁止在高速公路上倒车、逆行、穿越中央分隔带掉头;发生故障时可按规定停靠并设置警告标志。' },
28
+ { id: 'drv-17', category: '故障', question: '在高速公路上发生故障需要停车时,警告标志应设置在来车方向多少米以外?', options: ['50 米', '100 米', '150 米', '200 米'], answer: 'C', explanation: '道交法第六十八条:在高速公路上发生故障时,应当在故障车来车方向 150 米以外设置警告标志。' },
29
+ { id: 'drv-18', category: '故障', question: '在普通道路上发生故障难以移动时,警告标志一般设置在来车方向多远?', options: ['10–20 米', '30–50 米', '50–100 米', '200 米以外'], answer: 'C', explanation: '实施条例第六十条:在故障车来车方向 50 米至 100 米之间设置警告标志,夜间还应开启示廓灯和后位灯。' },
30
+ { id: 'drv-19', category: '行人', question: '机动车行经人行横道时,遇行人正在通过,应当?', options: ['鸣喇叭提醒后通过', '加速抢先通过', '停车让行', '从行人身后绕行'], answer: 'C', explanation: '道交法第四十七条:机动车行经人行横道时应当减速行驶;遇行人正在通过人行横道,应当停车让行。' },
31
+ { id: 'drv-20', category: '信号', question: '黄灯亮时,已越过停止线的车辆应当?', options: ['立即停车', '倒回停止线内', '可以继续通行', '鸣笛示意后停车'], answer: 'C', explanation: '实施条例第三十八条:黄灯亮时,已越过停止线的车辆可以继续通行。' },
32
+ { id: 'drv-21', category: '路口', question: '通过没有交通信号灯、交通标志和标线控制的交叉路口时,应当让哪个方向的来车先行?', options: ['左方道路', '右方道路', '车型较小的一方', '速度较快的一方'], answer: 'B', explanation: '实施条例第五十二条:进入路口前停车瞭望,让右方道路的来车先行。' },
33
+ { id: 'drv-22', category: '灯光', question: '夜间会车时,应当在距相对方向来车多远以外改用近光灯?', options: ['50 米', '100 米', '150 米', '300 米'], answer: 'C', explanation: '实施条例第四十八条:夜间会车应当在距相对方向来车 150 米以外改用近光灯。' },
34
+ { id: 'drv-23', category: '疲劳', question: '连续驾驶机动车超过 4 小时,停车休息时间不得少于多久?', options: ['5 分钟', '10 分钟', '20 分钟', '60 分钟'], answer: 'C', explanation: '实施条例第六十二条:不得连续驾驶机动车超过 4 小时未停车休息或者停车休息时间少于 20 分钟。' },
35
+ { id: 'drv-24', category: '倒车', question: '下列哪个地点可以倒车?', options: ['铁路道口', '隧道中', '单行路上', '允许停车的路边空地'], answer: 'D', explanation: '实施条例第五十条:在铁路道口、交叉路口、单行路、桥梁、急弯、陡坡或者隧道中不得倒车。' },
36
+ { id: 'drv-25', category: '安全', question: '机动车行驶中,驾驶人和乘车人应当?', options: ['只有驾驶人系安全带', '只有前排系安全带', '按规定使用安全带', '短途可以不系'], answer: 'C', explanation: '道交法第五十一条:机动车行驶时,驾驶人、乘坐人员应当按规定使用安全带。' },
37
+ ]);
@@ -0,0 +1,48 @@
1
+ import type { QuizQuestion } from './quiz-types.js';
2
+
3
+ /**
4
+ * 「好无聊」趣味选择题。题面、选项与解析均为本仓库原创(MIT),
5
+ * 内容是公共常识类知识点,不来自任何第三方题库。
6
+ */
7
+ export const FUN_QUESTIONS: readonly QuizQuestion[] = Object.freeze([
8
+ { id: 'fun-01', category: '天文', question: '北半球一年中白昼最长的节气是?', options: ['春分', '夏至', '秋分', '冬至'], answer: 'B', explanation: '夏至时太阳直射北回归线附近,北半球白昼最长。' },
9
+ { id: 'fun-02', category: '动物', question: '章鱼有几颗心脏?', options: ['1 颗', '2 颗', '3 颗', '4 颗'], answer: 'C', explanation: '章鱼有 1 颗体心脏和 2 颗鳃心脏,一共 3 颗。' },
10
+ { id: 'fun-03', category: '动物', question: '大熊猫抓竹子用的「伪拇指」本质上是什么?', options: ['腕部特化的籽骨', '第六根手指骨', '加厚的指甲', '一块软骨垫'], answer: 'A', explanation: '那是腕部桡侧籽骨增大形成的结构,并不是真正的手指。' },
11
+ { id: 'fun-04', category: '地理', question: '按「年降水量很少」的定义,世界上面积最大的沙漠是?', options: ['撒哈拉沙漠', '南极洲', '戈壁沙漠', '阿拉伯沙漠'], answer: 'B', explanation: '沙漠按降水量定义,南极洲极其干燥,面积也最大;撒哈拉是最大的热沙漠。' },
12
+ { id: 'fun-05', category: '天文', question: '太阳系中哪颗行星自转一圈比绕太阳公转一圈还要久?', options: ['金星', '火星', '木星', '水星'], answer: 'A', explanation: '金星自转约 243 个地球日,公转约 225 个地球日。' },
13
+ { id: 'fun-06', category: '化学', question: '金刚石和石墨都由哪种元素组成?', options: ['硅', '碳', '硼', '硫'], answer: 'B', explanation: '两者都是碳的同素异形体,只是原子排列方式不同。' },
14
+ { id: 'fun-07', category: '生物', question: '人体面积最大的器官是?', options: ['肝脏', '肺', '皮肤', '大肠'], answer: 'C', explanation: '成年人的皮肤面积大约 1.5–2 平方米,是最大的器官。' },
15
+ { id: 'fun-08', category: '数学', question: '0.999…(9 无限循环)和 1 相比?', options: ['比 1 小一点点', '等于 1', '比 1 大', '无法比较'], answer: 'B', explanation: '1/3 = 0.333…,两边乘以 3 就得到 0.999… = 1。' },
16
+ { id: 'fun-09', category: '物理', question: '真空中的光速大约是每秒多少公里?', options: ['3 万', '30 万', '300 万', '3000'], answer: 'B', explanation: '约 299,792 公里每秒,常记作 30 万公里每秒。' },
17
+ { id: 'fun-10', category: '生活', question: '微波炉加热食物,主要是让食物中的什么分子剧烈运动?', options: ['水分子', '盐分子', '氧分子', '糖分子'], answer: 'A', explanation: '微波让极性的水分子来回转动摩擦生热,所以很干的食物不容易热透。' },
18
+ { id: 'fun-11', category: '历史', question: '下面哪一项不属于中国古代「四大发明」?', options: ['造纸术', '印刷术', '火药', '算盘'], answer: 'D', explanation: '四大发明是造纸术、印刷术、火药和指南针。' },
19
+ { id: 'fun-12', category: '语言', question: '网络常用的「囧」字,本义是什么?', options: ['光明', '窘迫', '困倦', '圆形'], answer: 'A', explanation: '「囧」本义是窗户透光、明亮,后来因字形像表情才被用来表示窘迫。' },
20
+ { id: 'fun-13', category: '动物', question: '考拉的主要食物是?', options: ['竹子', '桉树叶', '昆虫', '水果'], answer: 'B', explanation: '考拉几乎只吃桉树叶,桉树叶营养少,所以考拉一天要睡很久。' },
21
+ { id: 'fun-14', category: '地理', question: '中国陆地面积最大的省级行政区是?', options: ['西藏', '内蒙古', '青海', '新疆'], answer: 'D', explanation: '新疆面积约 166 万平方公里,约占全国陆地面积的六分之一。' },
22
+ { id: 'fun-15', category: '天文', question: '月球绕地球公转一周(恒星月)大约需要多久?', options: ['7 天', '15 天', '27.3 天', '35 天'], answer: 'C', explanation: '恒星月约 27.3 天;从满月到下一次满月(朔望月)约 29.5 天。' },
23
+ { id: 'fun-16', category: '动物', question: '蜜蜂跳「摇摆舞」主要是在告诉同伴什么?', options: ['自己饿了', '蜜源的方向和距离', '天气要变了', '蜂后在哪里'], answer: 'B', explanation: '摇摆的角度对应蜜源相对太阳的方向,持续时间对应距离。' },
24
+ { id: 'fun-17', category: '物理', question: '下列介质中,声音传播最快的是?', options: ['空气', '水', '钢铁', '真空'], answer: 'C', explanation: '声音在固体中通常最快,钢铁中约 5000 米每秒;真空中无法传播。' },
25
+ { id: 'fun-18', category: '生活', question: '家用冰箱冷藏室比较推荐的温度大约是?', options: ['-18℃', '4℃ 左右', '15℃', '25℃'], answer: 'B', explanation: '冷藏一般保持在 0–5℃,-18℃ 是冷冻室的常用温度。' },
26
+ { id: 'fun-19', category: '动物', question: '现存体型最大的动物是?', options: ['非洲象', '蓝鲸', '鲸鲨', '长颈鹿'], answer: 'B', explanation: '蓝鲸体长可超过 30 米,是已知体型最大的动物。' },
27
+ { id: 'fun-20', category: '数学', question: '正六边形的内角和是多少度?', options: ['540°', '720°', '900°', '1080°'], answer: 'B', explanation: 'n 边形内角和为 (n−2)×180°,六边形就是 720°。' },
28
+ { id: 'fun-21', category: '化学', question: '地球大气中含量最多的气体是?', options: ['氧气', '二氧化碳', '氮气', '氩气'], answer: 'C', explanation: '氮气约占干燥空气体积的 78%,氧气约 21%。' },
29
+ { id: 'fun-22', category: '生物', question: '人类正常体细胞中有多少条染色体?', options: ['23 条', '44 条', '46 条', '48 条'], answer: 'C', explanation: '23 对共 46 条,其中 1 对是性染色体。' },
30
+ { id: 'fun-23', category: '地理', question: '世界最高峰珠穆朗玛峰的最新测定海拔约是多少米?', options: ['8611', '8848.86', '8516', '8201'], answer: 'B', explanation: '2020 年中尼两国共同宣布珠峰雪面高程为 8848.86 米。' },
31
+ { id: 'fun-24', category: '冷知识', question: '从植物学分类看,香蕉属于哪种果实?', options: ['浆果', '坚果', '核果', '荚果'], answer: 'A', explanation: '香蕉由单个子房发育而来、果皮肉质,植物学上算浆果。' },
32
+ { id: 'fun-25', category: '冷知识', question: '草莓表面那些小「籽」其实是什么?', options: ['真正的果实(瘦果)', '花粉', '退化的叶子', '刺'], answer: 'A', explanation: '每一粒都是一个瘦果,我们吃的红色部分是膨大的花托。' },
33
+ { id: 'fun-26', category: '动物', question: '火烈鸟的粉红色羽毛主要来自?', options: ['天生基因', '食物中的类胡萝卜素', '阳光照射', '泥浆染色'], answer: 'B', explanation: '它们吃的藻类和小虾含类胡萝卜素,色素沉积在羽毛里。' },
34
+ { id: 'fun-27', category: '物理', question: '主虹(常见的那道彩虹)最外圈是什么颜色?', options: ['紫色', '蓝色', '绿色', '红色'], answer: 'D', explanation: '主虹外红内紫;副虹的颜色顺序正好相反。' },
35
+ { id: 'fun-28', category: '常识', question: '在标准大气压下,纯水的沸点是?', options: ['90℃', '100℃', '110℃', '120℃'], answer: 'B', explanation: '海拔越高气压越低,沸点也会随之降低。' },
36
+ { id: 'fun-29', category: '体育', question: '足球比赛常规时间里,每个半场多少分钟?', options: ['30 分钟', '40 分钟', '45 分钟', '50 分钟'], answer: 'C', explanation: '常规时间两个半场各 45 分钟,另加补时。' },
37
+ { id: 'fun-30', category: '计算机', question: '1 KiB 等于多少字节?', options: ['1000', '1024', '1048', '512'], answer: 'B', explanation: 'KiB 是二进制前缀,等于 2 的 10 次方字节;kB 则通常指 1000 字节。' },
38
+ { id: 'fun-31', category: '计算机', question: '网页打不开时常见的 HTTP 404 表示什么?', options: ['服务器内部错误', '没有权限', '请求的资源不存在', '请求太频繁'], answer: 'C', explanation: '404 Not Found;500 是服务器内部错误,403 是禁止访问,429 是请求过多。' },
39
+ { id: 'fun-32', category: '动物', question: '野生企鹅主要分布在?', options: ['北极圈', '南半球', '赤道雨林', '北美五大湖'], answer: 'B', explanation: '企鹅几乎都生活在南半球,北极没有野生企鹅。' },
40
+ { id: 'fun-33', category: '生物', question: '植物光合作用会释放哪种气体?', options: ['二氧化碳', '氧气', '氮气', '氢气'], answer: 'B', explanation: '光合作用吸收二氧化碳和水,产生有机物并释放氧气。' },
41
+ { id: 'fun-34', category: '天文', question: '离太阳最近的行星是?', options: ['金星', '地球', '水星', '火星'], answer: 'C', explanation: '水星离太阳最近,但最热的行星是有浓厚大气的金星。' },
42
+ { id: 'fun-35', category: '冷知识', question: '蜗牛有牙齿吗?', options: ['没有', '有,长在齿舌上,数量很多', '只有一颗', '只有幼年有'], answer: 'B', explanation: '蜗牛的齿舌上排列着成千上万颗细小的角质齿,用来刮食。' },
43
+ { id: 'fun-36', category: '语言', question: '与「一石二鸟」意思最接近的成语是?', options: ['一箭双雕', '一叶障目', '一鸣惊人', '一言九鼎'], answer: 'A', explanation: '都比喻做一件事得到两种收获。' },
44
+ { id: 'fun-37', category: '数学', question: '圆周率 π 小数点后前四位是?', options: ['1415', '1416', '1592', '1428'], answer: 'A', explanation: 'π ≈ 3.14159…,前四位小数是 1415。' },
45
+ { id: 'fun-38', category: '地理', question: '赤道没有经过下列哪个大洲?', options: ['非洲', '南美洲', '亚洲', '欧洲'], answer: 'D', explanation: '赤道穿过非洲、南美洲和亚洲(印度尼西亚)等,不经过欧洲。' },
46
+ { id: 'fun-39', category: '生活', question: '炒菜时油锅起火,正确的第一反应是?', options: ['立刻泼水', '盖上锅盖并关火', '用嘴吹灭', '端着锅跑出去'], answer: 'B', explanation: '盖锅盖隔绝空气、关火断热源;泼水会让热油飞溅,火势更大。' },
47
+ { id: 'fun-40', category: '动物', question: '猫的胡须主要有什么作用?', options: ['纯粹装饰', '感知周围环境和空间宽度', '帮助吃饭', '调节体温'], answer: 'B', explanation: '胡须根部神经丰富,能感知气流和障碍,帮猫判断能否钻过缝隙。' },
48
+ ]);
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export { BOOK_ANSWERS } from './book-data.js';
2
+ export { BOOK_USAGE, MAX_QUESTION_LENGTH, calendarDay, normalizeQuestion, pickBookAnswer, replyBook, type BookPickInput } from './book.js';
3
+ export { DEFAULT_ANSWERS_CONFIG, answersConfig, resolveAnswersConfig, type AnswersConfig } from './config.js';
4
+ export { DRIVING_QUESTIONS } from './driving-quiz-data.js';
5
+ export { FUN_QUESTIONS } from './fun-quiz-data.js';
6
+ export {
7
+ MAX_PENDING_QUIZZES,
8
+ QuizService,
9
+ formatJudgement,
10
+ formatQuestion,
11
+ parseQuizLetter,
12
+ pickQuestion,
13
+ quizKey,
14
+ type PendingQuiz,
15
+ type QuizConversationInput,
16
+ type QuizJudgement,
17
+ type QuizKind,
18
+ } from './quiz.js';
19
+ export { QUIZ_LETTERS, type QuizLetter, type QuizQuestion } from './quiz-types.js';
20
+ export { quizServiceToken } from './runtime.js';
@@ -0,0 +1,15 @@
1
+ export type QuizLetter = 'A' | 'B' | 'C' | 'D';
2
+
3
+ export interface QuizQuestion {
4
+ /** 题库内唯一、稳定的题号,用于测试与去重。 */
5
+ readonly id: string;
6
+ readonly category: string;
7
+ readonly question: string;
8
+ /** 2–4 个选项,按 A/B/C/D 顺序排列,不含字母前缀。 */
9
+ readonly options: readonly string[];
10
+ readonly answer: QuizLetter;
11
+ /** 作答后展示的解析。 */
12
+ readonly explanation: string;
13
+ }
14
+
15
+ export const QUIZ_LETTERS: readonly QuizLetter[] = ['A', 'B', 'C', 'D'];
package/src/quiz.ts ADDED
@@ -0,0 +1,136 @@
1
+ import { randomInt } from 'node:crypto';
2
+ import { QUIZ_LETTERS, type QuizLetter, type QuizQuestion } from './quiz-types.js';
3
+
4
+ export type QuizKind = 'fun' | 'driving';
5
+
6
+ /** Structural subset of the Runtime / Command message used to scope pending questions. */
7
+ export interface QuizConversationInput {
8
+ readonly conversation?: {
9
+ readonly endpoint?: { readonly id?: string; readonly adapter?: string };
10
+ readonly kind?: string;
11
+ readonly id?: string;
12
+ readonly threadId?: string;
13
+ };
14
+ readonly sender?: { readonly id?: string } | string;
15
+ }
16
+
17
+ export interface PendingQuiz {
18
+ readonly kind: QuizKind;
19
+ readonly question: QuizQuestion;
20
+ readonly expiresAt: number;
21
+ }
22
+
23
+ export interface QuizJudgement {
24
+ readonly correct: boolean;
25
+ readonly chosen: QuizLetter;
26
+ readonly pending: PendingQuiz;
27
+ }
28
+
29
+ /** Upper bound on simultaneously pending questions; the oldest one is dropped first. */
30
+ export const MAX_PENDING_QUIZZES = 1000;
31
+
32
+ /**
33
+ * 题目只按「同一会话 + 同一发送者」隔离:A 出的题不能由 B 回答,
34
+ * 同一用户在两个群里的题互不影响。返回 undefined 表示缺少可信身份,不出题。
35
+ */
36
+ export function quizKey(input: QuizConversationInput | undefined): string | undefined {
37
+ const conversation = input?.conversation;
38
+ const sender = typeof input?.sender === 'string' ? input.sender : input?.sender?.id;
39
+ if (!conversation?.id || !conversation.kind || !sender) return undefined;
40
+ return JSON.stringify(['answers-quiz-v1', conversation.endpoint?.adapter ?? null, conversation.endpoint?.id ?? null,
41
+ conversation.kind, conversation.id, conversation.threadId ?? null, sender]);
42
+ }
43
+
44
+ /** 只接受单个选项字母(大小写、全角均可),其余文本一律放行给后续中间件。 */
45
+ export function parseQuizLetter(raw: string | undefined): QuizLetter | undefined {
46
+ const text = (raw ?? '').trim().replace(/[A-Da-d]/gu, (char) => String.fromCharCode(char.charCodeAt(0) - 0xfee0));
47
+ if (!/^[A-Da-d]$/u.test(text)) return undefined;
48
+ return text.toUpperCase() as QuizLetter;
49
+ }
50
+
51
+ export function pickQuestion(bank: readonly QuizQuestion[], exceptId?: string): QuizQuestion {
52
+ if (bank.length === 0) throw new Error('题库为空');
53
+ if (bank.length === 1) return bank[0]!;
54
+ let question = bank[randomInt(bank.length)]!;
55
+ // 避免同一人连续两次抽到同一道题。
56
+ if (question.id === exceptId) question = bank[(bank.indexOf(question) + 1 + randomInt(bank.length - 1)) % bank.length]!;
57
+ return question;
58
+ }
59
+
60
+ export function formatQuestion(kind: QuizKind, question: QuizQuestion, timeoutSeconds: number): string {
61
+ const head = kind === 'fun' ? '😴 好无聊?请听题!' : '🚗 科目一小测,请听题!';
62
+ const options = question.options.map((option, index) => `${QUIZ_LETTERS[index]}. ${option}`);
63
+ const letters = QUIZ_LETTERS.slice(0, question.options.length).join('/');
64
+ return [`${head}【${question.category}】`, '', question.question, '', ...options, '',
65
+ `直接回复 ${letters} 作答,${timeoutSeconds} 秒内有效。`].join('\n');
66
+ }
67
+
68
+ export function formatJudgement(judgement: QuizJudgement): string {
69
+ const { question, kind } = judgement.pending;
70
+ const answerText = `${question.answer}. ${question.options[QUIZ_LETTERS.indexOf(question.answer)]}`;
71
+ const verdict = judgement.correct ? '✅ 答对了!' : `❌ 答错了,正确答案是 ${answerText}`;
72
+ const footer = kind === 'driving' ? '\n(原创题面,依据公开法规整理,仅供娱乐参考,以现行规定为准)' : '';
73
+ return `${verdict}\n💡 ${question.explanation}${footer}`;
74
+ }
75
+
76
+ /** Generation-owned, in-memory only: pending questions vanish on reload, which is acceptable for a party game. */
77
+ export class QuizService {
78
+ readonly #pending = new Map<string, PendingQuiz>();
79
+ readonly #lastQuestion = new Map<string, string>();
80
+ #disposed = false;
81
+
82
+ ask(key: string, kind: QuizKind, bank: readonly QuizQuestion[], timeoutSeconds: number, now = Date.now()): QuizQuestion {
83
+ this.#assertLive();
84
+ this.sweep(now);
85
+ const question = pickQuestion(bank, this.#lastQuestion.get(key));
86
+ this.#pending.delete(key);
87
+ this.#pending.set(key, { kind, question, expiresAt: now + timeoutSeconds * 1000 });
88
+ this.#lastQuestion.delete(key);
89
+ this.#lastQuestion.set(key, question.id);
90
+ while (this.#pending.size > MAX_PENDING_QUIZZES) this.#pending.delete(this.#pending.keys().next().value!);
91
+ while (this.#lastQuestion.size > MAX_PENDING_QUIZZES) this.#lastQuestion.delete(this.#lastQuestion.keys().next().value!);
92
+ return question;
93
+ }
94
+
95
+ peek(key: string, now = Date.now()): PendingQuiz | undefined {
96
+ const pending = this.#pending.get(key);
97
+ if (!pending) return undefined;
98
+ if (pending.expiresAt <= now) {
99
+ this.#pending.delete(key);
100
+ return undefined;
101
+ }
102
+ return pending;
103
+ }
104
+
105
+ /** Judge and consume the pending question; letters beyond the option count are ignored (undefined). */
106
+ answer(key: string, letter: QuizLetter, now = Date.now()): QuizJudgement | undefined {
107
+ if (this.#disposed) return undefined;
108
+ const pending = this.peek(key, now);
109
+ if (!pending) return undefined;
110
+ if (QUIZ_LETTERS.indexOf(letter) >= pending.question.options.length) return undefined;
111
+ this.#pending.delete(key);
112
+ return { correct: letter === pending.question.answer, chosen: letter, pending };
113
+ }
114
+
115
+ cancel(key: string): boolean {
116
+ return this.#pending.delete(key);
117
+ }
118
+
119
+ sweep(now = Date.now()): void {
120
+ for (const [key, pending] of this.#pending) if (pending.expiresAt <= now) this.#pending.delete(key);
121
+ }
122
+
123
+ get size(): number {
124
+ return this.#pending.size;
125
+ }
126
+
127
+ dispose(): void {
128
+ this.#disposed = true;
129
+ this.#pending.clear();
130
+ this.#lastQuestion.clear();
131
+ }
132
+
133
+ #assertLive(): void {
134
+ if (this.#disposed) throw new Error('答题服务属于已停止的插件实例');
135
+ }
136
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { createToken } from 'zhin.js';
2
+ import type { QuizService } from './quiz.js';
3
+
4
+ /** Owner-scoped quiz state consumed by discovered commands and middleware. */
5
+ export const quizServiceToken = createToken<QuizService>('zhin.answers.quiz', 'Pending quiz questions per conversation and sender');