Skill: jiajiaoy/english-daily (version 1.0.2)

--- name: english-daily description: "Daily English learning with spaced repetition — built-in A1–B2 word bank, new words daily, quiz mode (MCQ/fill-in/spelling), streak tracking, level progression." keywords: 学英语, 英语单词, 今日单词, 英语练习, 英语学习, 词汇, 测验, 打卡, 连续学习, 进度, 间隔重复, 每日推送, English learning, vocabulary, daily words, quiz, streak, spaced repetition, English practice, CEFR, word bank metadata: openclaw: runtime: node: ">=18" --- # English Daily > 私人英语学习助手 — 每日单词 · SRS复习 · 测验打卡 · 进度追踪 ## 何时使用 - 用户说"学英语""英语单词""今日单词""英语练习" - 用户想背单词、做填空、做选择题 - 用户说"测验""我的进度""连续打卡""学习报告" - 用户说"开启推送""每天推英语单词" --- ## 核心命令 ```bash # 注册（首次使用） node scripts/register.js <userId> <姓名> [等级 A1/A2/B1/B2] [每日目标 1-20] # 今日学习（每日推送内容） node scripts/daily-push.js <userId> # 测验练习 node scripts/quiz.js <userId> [vocab|sentence|mixed] # 记录测验积分（Claude 在测验完成后调用） node scripts/quiz.js <userId> --score <正确题数×10> # 查看进度 node scripts/progress.js <userId> # 推送管理 node scripts/push-toggle.js on <userId> [--morning 08:00] [--channel telegram] node scripts/push-toggle.js off <userId> node scripts/push-toggle.js status <userId> ``` --- ## 学习流程 1. **注册** → `register.js` 创建学习档案（等级、每日目标） 2. **每日学习** → `daily-push.js` 输出今日复习词 + 新词列表 3. **测验** → `quiz.js` 生成5题（词义选择或句子填空），Claude 逐题互动 4. **记分** → 测验完成后 Claude 调用 `--score` 记录积分并更新SRS进度 5. **进度** → `progress.js` 显示连续打卡、掌握词数、升级进度 --- ## 推送设置 ```bash node scripts/push-toggle.js on <userId> # 默认 08:00 node scripts/push-toggle.js on <userId> --morning 07:30 --channel feishu node scripts/push-toggle.js off <userId> ``` 支持渠道：`telegram` / `feishu` / `slack` / `discord` --- ## 等级体系 | 等级 | 词汇量 | 升级条件（掌握词数） | |------|--------|---------------------| | A1 | ~40词 | 掌握40词 → 升A2 | | A2 | ~50词 | 掌握90词 → 升B1 | | B1 | ~40词 | 掌握130词 → 升B2 | | B2 | ~30词 | 最高等级 | 掌握标准：SRS间隔 ≥ 7天（即多次正确复习） --- ## SRS算法说明 采用简化SM-2间隔重复： - 质量1（遗忘）/ 质量2（困难）→ 明天复习 - 质量3（掌握）→ 间隔 × 1.5 - 质量4（轻松）→ 间隔 × 2.0 - 最大间隔30天 --- ## 注意事项 - 数据存储在 `data/users/<userId>.json`，无外部API依赖 - 内置单词库（A1-B2共约160词），`data/wordbank.json` - 所有脚本仅使用 Node.js 内置模块（fs/path），无需 npm install - 用户ID仅允许字母、数字、连字符、下划线（防路径穿越）

path: README.md
content: # English Daily — Daily English Learning Skill

> Daily English learning with spaced repetition — built-in A1–B2 vocabulary, quizzes, streaks, and progress tracking. No API needed.

[![clawhub](https://img.shields.io/badge/clawhub-english--daily-blue)](https://clawhub.ai/skills/english-daily)
[![openclaw](https://img.shields.io/badge/openclaw-skill-orange)](https://openclaw.ai)

## What it does

English Daily is a complete daily English learning system — not just a word of the day, but a full spaced repetition (SRS) vocabulary program with quizzes, streaks, and level progression. Built-in A1–B2 word bank with no external API or installation required.

**Daily push** — new words matched to your current level  
**Spaced repetition (SRS)** — reviews scheduled based on your retention  
**Quiz mode** — multiple choice, fill-in-the-blank, spelling  
**Streak tracking** — daily consistency rewards  
**Level progression** — A1 → A2 → B1 → B2  
**No dependency** — everything runs locally, no API key needed

## Installation

```bash
openclaw install english-daily
```

## Usage

```bash
# Daily word push
openclaw run english-daily morning

# Quiz mode
openclaw run english-daily quiz

# Check progress and streak
openclaw run english-daily progress
```

## Keywords

English learning · daily English · vocabulary · spaced repetition · SRS · IELTS vocabulary · English quiz · word of the day · English streak · 学英语 · 每日英语 · 英语单词 · 英语练习 · 词汇 · 打卡 · 背单词 · IELTS · TOEFL

---

Built for [OpenClaw](https://openclaw.ai) · Published on [clawhub.ai](https://clawhub.ai/skills/english-daily)


path: _meta.json
content: {
  "slug": "english-daily",
  "version": "1.0.0",
  "runtime": { "node": ">=18" }
}


path: data/wordbank.json
content: [
  {"w":"hello","p":"/həˈloʊ/","t":"int.","zh":"你好；喂","lv":"A1","tp":["social","basic"],"ex":[["Hello! How are you?","你好！你怎么样？"],["She said hello to everyone.","她向每个人打招呼。"]]},
  {"w":"water","p":"/ˈwɔːtər/","t":"n.","zh":"水","lv":"A1","tp":["food","basic"],"ex":[["I drink water every day.","我每天喝水。"],["The water is cold.","这水很凉。"]]},
  {"w":"food","p":"/fuːd/","t":"n.","zh":"食物；食品","lv":"A1","tp":["food","basic"],"ex":[["I like Chinese food.","我喜欢中国食物。"],["Food is important for health.","食物对健康很重要。"]]},
  {"w":"family","p":"/ˈfæməli/","t":"n.","zh":"家庭；家人","lv":"A1","tp":["family","basic"],"ex":[["My family is very big.","我的家庭很大。"],["Family is the most important thing.","家庭是最重要的事。"]]},
  {"w":"house","p":"/haʊs/","t":"n.","zh":"房子；住宅","lv":"A1","tp":["home","basic"],"ex":[["We live in a big house.","我们住在一栋大房子里。"],["The house is near the school.","这房子在学校附近。"]]},
  {"w":"school","p":"/skuːl/","t":"n.","zh":"学校","lv":"A1","tp":["education","basic"],"ex":[["I go to school every day.","我每天去上学。"],["The school is very large.","这所学校很大。"]]},
  {"w":"work","p":"/wɜːrk/","t":"n./v.","zh":"工作；劳动","lv":"A1","tp":["work","basic"],"ex":[["She works at a hospital.","她在医院工作。"],["Work hard and you will succeed.","努力工作你就会成功。"]]},
  {"w":"friend","p":"/frend/","t":"n.","zh":"朋友；友人","lv":"A1","tp":["social","basic"],"ex":[["He is my best friend.","他是我最好的朋友。"],["Make friends with everyone.","和每个人交朋友。"]]},
  {"w":"time","p":"/taɪm/","t":"n.","zh":"时间；时刻","lv":"A1","tp":["general","basic"],"ex":[["What time is it?","现在几点了？"],["Time flies quickly.","时间飞逝。"]]},
  {"w":"day","p":"/deɪ/","t":"n.","zh":"天；白天","lv":"A1","tp":["general","basic"],"ex":[["Have a nice day!","祝你今天愉快！"],["I work five days a week.","我每周工作五天。"]]},
  {"w":"help","p":"/help/","t":"v./n.","zh":"帮助；援助","lv":"A1","tp":["social","basic"],"ex":[["Can you help me?","你能帮我吗？"],["She helped me with my homework.","她帮我做了作业。"]]},
  {"w":"love","p":"/lʌv/","t":"v./n.","zh":"爱；热爱","lv":"A1","tp":["emotion","basic"],"ex":[["I love my family.","我爱我的家人。"],["Love is a beautiful feeling.","爱是一种美好的感受。"]]},
  {"w":"money","p":"/ˈmʌni/","t":"n.","zh":"钱；金钱","lv":"A1","tp":["finance","basic"],"ex":[["I don't have much money.","我没有很多钱。"],["Money can't buy happiness.","金钱买不来幸福。"]]},
  {"w":"book","p":"/bʊk/","t":"n.","zh":"书；图书","lv":"A1","tp":["education","basic"],"ex":[["I read a book every week.","我每周读一本书。"],["This book is very interesting.","这本书很有趣。"]]},
  {"w":"phone","p":"/foʊn/","t":"n.","zh":"电话；手机","lv":"A1","tp":["technology","basic"],"ex":[["My phone is new.","我的手机是新的。"],["She called me on the phone.","她给我打了电话。"]]},
  {"w":"happy","p":"/ˈhæpi/","t":"adj.","zh":"高兴的；快乐的","lv":"A1","tp":["emotion","basic"],"ex":[["I am very happy today.","我今天非常高兴。"],["A happy life is a good life.","快乐的生活是美好的生活。"]]},
  {"w":"sad","p":"/sæd/","t":"adj.","zh":"悲伤的；难过的","lv":"A1","tp":["emotion","basic"],"ex":[["She felt sad when he left.","他离开时她感到悲伤。"],["Don't be sad, things will get better.","别难过，事情会好转的。"]]},
  {"w":"big","p":"/bɪɡ/","t":"adj.","zh":"大的；巨大的","lv":"A1","tp":["general","basic"],"ex":[["That is a very big dog.","那是一只非常大的狗。"],["He has big dreams.","他有远大的梦想。"]]},
  {"w":"small","p":"/smɔːl/","t":"adj.","zh":"小的；微小的","lv":"A1","tp":["general","basic"],"ex":[["The room is very small.","这个房间很小。"],["Small steps lead to big changes.","小步骤带来大变化。"]]},
  {"w":"good","p":"/ɡʊd/","t":"adj.","zh":"好的；优秀的","lv":"A1","tp":["general","basic"],"ex":[["She is a good student.","她是一个好学生。"],["Have a good time!","玩得开心！"]]},
  {"w":"bad","p":"/bæd/","t":"adj.","zh":"坏的；糟糕的","lv":"A1","tp":["general","basic"],"ex":[["The weather is bad today.","今天天气很糟糕。"],["It's not as bad as it seems.","没有看起来那么糟。"]]},
  {"w":"eat","p":"/iːt/","t":"v.","zh":"吃；进食","lv":"A1","tp":["food","basic"],"ex":[["I eat breakfast every morning.","我每天早上吃早餐。"],["What do you want to eat?","你想吃什么？"]]},
  {"w":"drink","p":"/drɪŋk/","t":"v./n.","zh":"喝；饮料","lv":"A1","tp":["food","basic"],"ex":[["She drinks coffee in the morning.","她早上喝咖啡。"],["Would you like a drink?","你想喝点什么吗？"]]},
  {"w":"go","p":"/ɡoʊ/","t":"v.","zh":"去；走","lv":"A1","tp":["general","basic"],"ex":[["I go to work by bus.","我乘公共汽车去上班。"],["Let's go to the park.","我们去公园吧。"]]},
  {"w":"come","p":"/kʌm/","t":"v.","zh":"来；来到","lv":"A1","tp":["general","basic"],"ex":[["Please come here.","请到这里来。"],["She came home late.","她很晚才回家。"]]},
  {"w":"see","p":"/siː/","t":"v.","zh":"看见；理解","lv":"A1","tp":["general","basic"],"ex":[["I can see the mountains.","我能看见山脉。"],["Do you see what I mean?","你明白我的意思吗？"]]},
  {"w":"know","p":"/noʊ/","t":"v.","zh":"知道；了解","lv":"A1","tp":["general","basic"],"ex":[["Do you know her name?","你知道她的名字吗？"],["I know how to cook.","我知道怎么做饭。"]]},
  {"w":"want","p":"/wɑːnt/","t":"v.","zh":"想要；希望","lv":"A1","tp":["general","basic"],"ex":[["What do you want?","你想要什么？"],["I want to learn English.","我想学英语。"]]},
  {"w":"need","p":"/niːd/","t":"v.","zh":"需要；必须","lv":"A1","tp":["general","basic"],"ex":[["I need your help.","我需要你的帮助。"],["You need to rest more.","你需要多休息。"]]},
  {"w":"like","p":"/laɪk/","t":"v./prep.","zh":"喜欢；像","lv":"A1","tp":["emotion","basic"],"ex":[["I like reading books.","我喜欢读书。"],["She sings like an angel.","她唱歌像天使一样。"]]},
  {"w":"have","p":"/hæv/","t":"v.","zh":"有；拥有","lv":"A1","tp":["general","basic"],"ex":[["I have two brothers.","我有两个兄弟。"],["Do you have a pen?","你有笔吗？"]]},
  {"w":"make","p":"/meɪk/","t":"v.","zh":"制作；使得","lv":"A1","tp":["general","basic"],"ex":[["She makes delicious cake.","她做美味的蛋糕。"],["Music makes me happy.","音乐让我快乐。"]]},
  {"w":"get","p":"/ɡet/","t":"v.","zh":"得到；变得","lv":"A1","tp":["general","basic"],"ex":[["I get up at 7 every morning.","我每天早上7点起床。"],["Get some rest tonight.","今晚好好休息。"]]},
  {"w":"give","p":"/ɡɪv/","t":"v.","zh":"给予；提供","lv":"A1","tp":["general","basic"],"ex":[["Give me a chance.","给我一次机会。"],["She gave me a gift.","她给了我一份礼物。"]]},
  {"w":"take","p":"/teɪk/","t":"v.","zh":"拿；带走","lv":"A1","tp":["general","basic"],"ex":[["Take this book with you.","把这本书带走。"],["It takes two hours to drive there.","开车去那里要两个小时。"]]},
  {"w":"find","p":"/faɪnd/","t":"v.","zh":"找到；发现","lv":"A1","tp":["general","basic"],"ex":[["I can't find my keys.","我找不到我的钥匙。"],["She found a new job.","她找到了一份新工作。"]]},
  {"w":"ask","p":"/æsk/","t":"v.","zh":"问；询问","lv":"A1","tp":["social","basic"],"ex":[["Ask your teacher for help.","向老师寻求帮助。"],["She asked me a question.","她问了我一个问题。"]]},
  {"w":"read","p":"/riːd/","t":"v.","zh":"读；阅读","lv":"A1","tp":["education","basic"],"ex":[["I read every night before bed.","我每晚睡前都读书。"],["Can you read this sign?","你能读懂这个标志吗？"]]},
  {"w":"write","p":"/raɪt/","t":"v.","zh":"写；书写","lv":"A1","tp":["education","basic"],"ex":[["Please write your name here.","请在这里写上你的名字。"],["She writes in her diary every day.","她每天写日记。"]]},
  {"w":"speak","p":"/spiːk/","t":"v.","zh":"说话；演讲","lv":"A1","tp":["communication","basic"],"ex":[["Can you speak English?","你会说英语吗？"],["She spoke clearly and slowly.","她说得清晰而缓慢。"]]},
  {"w":"hotel","p":"/hoʊˈtel/","t":"n.","zh":"酒店；旅馆","lv":"A2","tp":["travel","general"],"ex":[["We stayed at a nice hotel.","我们住在一家很好的酒店。"],["The hotel has a swimming pool.","这家酒店有游泳池。"]]},
  {"w":"travel","p":"/ˈtrævəl/","t":"v./n.","zh":"旅行；旅游","lv":"A2","tp":["travel","general"],"ex":[["I love to travel abroad.","我喜欢出国旅行。"],["Travel broadens the mind.","旅行开阔视野。"]]},
  {"w":"price","p":"/praɪs/","t":"n.","zh":"价格；价钱","lv":"A2","tp":["finance","general"],"ex":[["What is the price of this?","这个多少钱？"],["The price is too high for me.","这个价格对我来说太高了。"]]},
  {"w":"weather","p":"/ˈweðər/","t":"n.","zh":"天气；气候","lv":"A2","tp":["general"],"ex":[["The weather is nice today.","今天天气很好。"],["What is the weather like there?","那里天气怎么样？"]]},
  {"w":"health","p":"/helθ/","t":"n.","zh":"健康；卫生","lv":"A2","tp":["health","general"],"ex":[["Good health is the greatest wealth.","健康是最大的财富。"],["Exercise is important for health.","锻炼对健康很重要。"]]},
  {"w":"sick","p":"/sɪk/","t":"adj.","zh":"生病的；恶心的","lv":"A2","tp":["health","general"],"ex":[["She stayed home because she was sick.","她因为生病在家休息。"],["I feel sick after eating that.","吃了那个后我感到恶心。"]]},
  {"w":"tired","p":"/ˈtaɪərd/","t":"adj.","zh":"疲倦的；厌倦的","lv":"A2","tp":["emotion","health"],"ex":[["I'm very tired after work.","工作后我非常疲倦。"],["She is tired of waiting.","她厌倦了等待。"]]},
  {"w":"excited","p":"/ɪkˈsaɪtɪd/","t":"adj.","zh":"兴奋的；激动的","lv":"A2","tp":["emotion","general"],"ex":[["The children are excited about the trip.","孩子们对这次旅行感到兴奋。"],["She was excited to start her new job.","她对开始新工作感到兴奋。"]]},
  {"w":"worried","p":"/ˈwʌrid/","t":"adj.","zh":"担心的；忧虑的","lv":"A2","tp":["emotion","general"],"ex":[["He was worried about the exam.","他对考试感到担心。"],["Don't be worried, everything will be fine.","别担心，一切都会好的。"]]},
  {"w":"office","p":"/ˈɑːfɪs/","t":"n.","zh":"办公室；事务所","lv":"A2","tp":["work","general"],"ex":[["I work in a big office.","我在一个大办公室工作。"],["The office is on the third floor.","办公室在三楼。"]]},
  {"w":"meeting","p":"/ˈmiːtɪŋ/","t":"n.","zh":"会议；会面","lv":"A2","tp":["work","general"],"ex":[["The meeting starts at nine.","会议九点开始。"],["I have an important meeting today.","今天我有一个重要的会议。"]]},
  {"w":"important","p":"/ɪmˈpɔːrtənt/","t":"adj.","zh":"重要的；重大的","lv":"A2","tp":["general"],"ex":[["Education is very important.","教育非常重要。"],["This is an important decision.","这是一个重要的决定。"]]},
  {"w":"busy","p":"/ˈbɪzi/","t":"adj.","zh":"忙碌的；繁忙的","lv":"A2","tp":["work","general"],"ex":[["I am very busy this week.","这周我很忙。"],["The street is busy with traffic.","街道上交通繁忙。"]]},
  {"w":"cheap","p":"/tʃiːp/","t":"adj.","zh":"便宜的；廉价的","lv":"A2","tp":["finance","general"],"ex":[["This restaurant is cheap but good.","这家餐厅便宜但很好。"],["I found a cheap flight to Beijing.","我找到了一张去北京的便宜机票。"]]},
  {"w":"expensive","p":"/ɪkˈspensɪv/","t":"adj.","zh":"昂贵的；费钱的","lv":"A2","tp":["finance","general"],"ex":[["That car is too expensive for me.","那辆车对我来说太贵了。"],["Good quality is often expensive.","好质量通常是昂贵的。"]]},
  {"w":"arrive","p":"/əˈraɪv/","t":"v.","zh":"到达；抵达","lv":"A2","tp":["travel","general"],"ex":[["What time did you arrive?","你什么时候到达的？"],["She arrived late to the meeting.","她开会迟到了。"]]},
  {"w":"depart","p":"/dɪˈpɑːrt/","t":"v.","zh":"出发；离开","lv":"A2","tp":["travel","general"],"ex":[["The train departs at eight.","火车八点出发。"],["We will depart early tomorrow.","我们明天早早出发。"]]},
  {"w":"understand","p":"/ˌʌndərˈstænd/","t":"v.","zh":"理解；明白","lv":"A2","tp":["communication","education"],"ex":[["I don't understand this question.","我不理解这个问题。"],["Do you understand what I said?","你明白我说的吗？"]]},
  {"w":"explain","p":"/ɪkˈspleɪn/","t":"v.","zh":"解释；说明","lv":"A2","tp":["communication","education"],"ex":[["Can you explain that again?","你能再解释一遍吗？"],["She explained the rules clearly.","她清楚地解释了规则。"]]},
  {"w":"practice","p":"/ˈpræktɪs/","t":"v./n.","zh":"练习；实践","lv":"A2","tp":["education","general"],"ex":[["Practice makes perfect.","熟能生巧。"],["I practice English every day.","我每天练习英语。"]]},
  {"w":"improve","p":"/ɪmˈpruːv/","t":"v.","zh":"改进；提高","lv":"A2","tp":["general","education"],"ex":[["I want to improve my English.","我想提高我的英语水平。"],["Exercise can improve your health.","锻炼可以改善你的健康。"]]},
  {"w":"enjoy","p":"/ɪnˈdʒɔɪ/","t":"v.","zh":"享受；欣赏","lv":"A2","tp":["emotion","general"],"ex":[["I enjoy reading in the park.","我喜欢在公园里读书。"],["Enjoy your vacation!","好好享受你的假期！"]]},
  {"w":"forget","p":"/fərˈɡet/","t":"v.","zh":"忘记；遗忘","lv":"A2","tp":["general"],"ex":[["Don't forget to call me.","别忘了给我打电话。"],["I forgot her birthday.","我忘记了她的生日。"]]},
  {"w":"remember","p":"/rɪˈmembər/","t":"v.","zh":"记得；记住","lv":"A2","tp":["general"],"ex":[["Do you remember our first meeting?","你还记得我们第一次见面吗？"],["Remember to lock the door.","记得锁门。"]]},
  {"w":"decide","p":"/dɪˈsaɪd/","t":"v.","zh":"决定；下决心","lv":"A2","tp":["general"],"ex":[["We decided to move to a new city.","我们决定搬到一个新城市。"],["It's hard to decide which one to choose.","很难决定选哪一个。"]]},
  {"w":"agree","p":"/əˈɡriː/","t":"v.","zh":"同意；赞成","lv":"A2","tp":["social","communication"],"ex":[["I agree with your idea.","我同意你的想法。"],["They agreed to meet at noon.","他们同意在中午见面。"]]},
  {"w":"disagree","p":"/ˌdɪsəˈɡriː/","t":"v.","zh":"不同意；有异议","lv":"A2","tp":["social","communication"],"ex":[["I disagree with that decision.","我不同意那个决定。"],["They disagree about the best solution.","他们对最佳解决方案有异议。"]]},
  {"w":"suggest","p":"/səˈdʒest/","t":"v.","zh":"建议；提议","lv":"A2","tp":["communication","general"],"ex":[["I suggest you see a doctor.","我建议你去看医生。"],["She suggested a new approach.","她提出了一种新方法。"]]},
  {"w":"choose","p":"/tʃuːz/","t":"v.","zh":"选择；挑选","lv":"A2","tp":["general"],"ex":[["You can choose any color you like.","你可以选择任何你喜欢的颜色。"],["Choose your friends wisely.","明智地选择朋友。"]]},
  {"w":"prefer","p":"/prɪˈfɜːr/","t":"v.","zh":"更喜欢；宁愿","lv":"A2","tp":["emotion","general"],"ex":[["I prefer tea to coffee.","我更喜欢茶而不是咖啡。"],["She prefers working alone.","她更喜欢独自工作。"]]},
  {"w":"believe","p":"/bɪˈliːv/","t":"v.","zh":"相信；认为","lv":"A2","tp":["general"],"ex":[["I believe in you.","我相信你。"],["Believe in yourself and your abilities.","相信自己和你的能力。"]]},
  {"w":"hope","p":"/hoʊp/","t":"v./n.","zh":"希望；盼望","lv":"A2","tp":["emotion","general"],"ex":[["I hope you feel better soon.","我希望你很快好转。"],["There is always hope.","永远都有希望。"]]},
  {"w":"wish","p":"/wɪʃ/","t":"v./n.","zh":"希望；愿望","lv":"A2","tp":["emotion","general"],"ex":[["I wish I could travel more.","我希望我能多旅行。"],["Make a wish!","许个愿！"]]},
  {"w":"plan","p":"/plæn/","t":"v./n.","zh":"计划；打算","lv":"A2","tp":["work","general"],"ex":[["What is your plan for tomorrow?","你明天有什么计划？"],["We planned a trip to Japan.","我们计划了一次去日本的旅行。"]]},
  {"w":"try","p":"/traɪ/","t":"v.","zh":"尝试；努力","lv":"A2","tp":["general"],"ex":[["Always try your best.","永远尽力而为。"],["Try this new restaurant.","试试这家新餐厅。"]]},
  {"w":"learn","p":"/lɜːrn/","t":"v.","zh":"学习；学会","lv":"A2","tp":["education","general"],"ex":[["I am learning English.","我正在学英语。"],["You can learn from your mistakes.","你可以从错误中学习。"]]},
  {"w":"teach","p":"/tiːtʃ/","t":"v.","zh":"教；教导","lv":"A2","tp":["education","general"],"ex":[["She teaches math at school.","她在学校教数学。"],["He taught me how to cook.","他教我做饭。"]]},
  {"w":"study","p":"/ˈstʌdi/","t":"v./n.","zh":"学习；研究","lv":"A2","tp":["education","general"],"ex":[["I study for two hours every night.","我每晚学习两个小时。"],["Her study of history is impressive.","她对历史的研究令人印象深刻。"]]},
  {"w":"review","p":"/rɪˈvjuː/","t":"v./n.","zh":"复习；回顾","lv":"A2","tp":["education","general"],"ex":[["Review your notes before the exam.","考试前复习你的笔记。"],["He wrote a positive review.","他写了一篇正面的评论。"]]},
  {"w":"difficult","p":"/ˈdɪfɪkəlt/","t":"adj.","zh":"困难的；难以","lv":"A2","tp":["general"],"ex":[["This problem is very difficult.","这个问题非常困难。"],["It is difficult to learn a new language.","学一门新语言很难。"]]},
  {"w":"easy","p":"/ˈiːzi/","t":"adj.","zh":"容易的；简单的","lv":"A2","tp":["general"],"ex":[["The test was easy for me.","这次测试对我来说很容易。"],["Take it easy.","放轻松。"]]},
  {"w":"beautiful","p":"/ˈbjuːtɪfəl/","t":"adj.","zh":"美丽的；漂亮的","lv":"A2","tp":["general"],"ex":[["The sunset is beautiful.","日落很美丽。"],["She has a beautiful smile.","她有一个美丽的笑容。"]]},
  {"w":"interesting","p":"/ˈɪntrəstɪŋ/","t":"adj.","zh":"有趣的；令人感兴趣的","lv":"A2","tp":["general"],"ex":[["This is a very interesting book.","这是一本非常有趣的书。"],["I find history interesting.","我觉得历史很有趣。"]]},
  {"w":"boring","p":"/ˈbɔːrɪŋ/","t":"adj.","zh":"无聊的；乏味的","lv":"A2","tp":["emotion","general"],"ex":[["The lecture was very boring.","这场讲座非常无聊。"],["I don't want to do boring work.","我不想做无聊的工作。"]]},
  {"w":"funny","p":"/ˈfʌni/","t":"adj.","zh":"有趣的；可笑的","lv":"A2","tp":["emotion","general"],"ex":[["He told a very funny joke.","他讲了一个非常有趣的笑话。"],["That's not funny at all.","那一点也不好笑。"]]},
  {"w":"serious","p":"/ˈsɪriəs/","t":"adj.","zh":"严肃的；认真的","lv":"A2","tp":["emotion","general"],"ex":[["She is always serious about her work.","她对工作总是很认真。"],["This is a serious problem.","这是一个严重的问题。"]]},
  {"w":"careful","p":"/ˈkerfəl/","t":"adj.","zh":"小心的；谨慎的","lv":"A2","tp":["general"],"ex":[["Be careful when crossing the street.","过马路要小心。"],["She is a careful driver.","她是一个谨慎的司机。"]]},
  {"w":"friendly","p":"/ˈfrendli/","t":"adj.","zh":"友好的；亲切的","lv":"A2","tp":["social","general"],"ex":[["The staff here is very friendly.","这里的员工非常友好。"],["She has a friendly personality.","她性格友好。"]]},
  {"w":"achieve","p":"/əˈtʃiːv/","t":"v.","zh":"实现；完成","lv":"B1","tp":["work","general"],"ex":[["She achieved her goal at last.","她终于实现了目标。"],["Hard work helps you achieve success.","努力工作帮助你取得成功。"]]},
  {"w":"challenge","p":"/ˈtʃælɪndʒ/","t":"n./v.","zh":"挑战；挑战性","lv":"B1","tp":["general"],"ex":[["Every challenge is an opportunity.","每一个挑战都是一个机会。"],["He challenged himself to run every day.","他挑战自己每天跑步。"]]},
  {"w":"opportunity","p":"/ˌɑːpərˈtuːnəti/","t":"n.","zh":"机会；时机","lv":"B1","tp":["work","general"],"ex":[["Don't miss this great opportunity.","不要错过这个好机会。"],["She took every opportunity to practice.","她抓住每一个机会练习。"]]},
  {"w":"experience","p":"/ɪkˈspɪriəns/","t":"n./v.","zh":"经历；经验","lv":"B1","tp":["general"],"ex":[["That was a life-changing experience.","那是一次改变人生的经历。"],["Experience is the best teacher.","经验是最好的老师。"]]},
  {"w":"success","p":"/səkˈses/","t":"n.","zh":"成功；成就","lv":"B1","tp":["work","general"],"ex":[["Success requires patience and effort.","成功需要耐心和努力。"],["Her success inspired many people.","她的成功激励了很多人。"]]},
  {"w":"failure","p":"/ˈfeɪljər/","t":"n.","zh":"失败；不成功","lv":"B1","tp":["general"],"ex":[["Failure is the mother of success.","失败是成功之母。"],["Don't fear failure, learn from it.","不要害怕失败，要从中学习。"]]},
  {"w":"relationship","p":"/rɪˈleɪʃənʃɪp/","t":"n.","zh":"关系；人际关系","lv":"B1","tp":["social","general"],"ex":[["Their relationship grew stronger.","他们的关系越来越牢固。"],["Good relationships require communication.","良好的关系需要沟通。"]]},
  {"w":"communication","p":"/kəˌmjuːnɪˈkeɪʃən/","t":"n.","zh":"沟通；交流","lv":"B1","tp":["communication","general"],"ex":[["Good communication is key to success.","良好的沟通是成功的关键。"],["Communication skills are important at work.","沟通技巧在工作中很重要。"]]},
  {"w":"responsibility","p":"/rɪˌspɑːnsəˈbɪləti/","t":"n.","zh":"责任；责任感","lv":"B1","tp":["work","general"],"ex":[["Take responsibility for your actions.","为你的行为承担责任。"],["Leadership comes with great responsibility.","领导力伴随着巨大的责任。"]]},
  {"w":"environment","p":"/ɪnˈvaɪrənmənt/","t":"n.","zh":"环境；自然环境","lv":"B1","tp":["general","society"],"ex":[["We must protect the environment.","我们必须保护环境。"],["The work environment affects productivity.","工作环境影响生产效率。"]]},
  {"w":"culture","p":"/ˈkʌltʃər/","t":"n.","zh":"文化；文明","lv":"B1","tp":["society","general"],"ex":[["I am fascinated by Chinese culture.","我对中国文化着迷。"],["Different cultures have different values.","不同文化有不同的价值观。"]]},
  {"w":"tradition","p":"/trəˈdɪʃən/","t":"n.","zh":"传统；习俗","lv":"B1","tp":["society","general"],"ex":[["This is an old family tradition.","这是一个古老的家族传统。"],["They celebrate many local traditions.","他们庆祝许多当地传统。"]]},
  {"w":"society","p":"/səˈsaɪəti/","t":"n.","zh":"社会；社团","lv":"B1","tp":["society","general"],"ex":[["Education plays a big role in society.","教育在社会中扮演重要角色。"],["A fair society benefits everyone.","公平的社会对每个人都有益。"]]},
  {"w":"community","p":"/kəˈmjuːnəti/","t":"n.","zh":"社区；社群","lv":"B1","tp":["society","general"],"ex":[["She volunteers in her community.","她在社区做志愿者。"],["A strong community supports everyone.","一个强大的社区支持每个人。"]]},
  {"w":"government","p":"/ˈɡʌvərnmənt/","t":"n.","zh":"政府；政体","lv":"B1","tp":["society","general"],"ex":[["The government passed a new law.","政府通过了一项新法律。"],["People need to trust their government.","人们需要信任他们的政府。"]]},
  {"w":"education","p":"/ˌedʒuˈkeɪʃən/","t":"n.","zh":"教育；培训","lv":"B1","tp":["education","general"],"ex":[["Education opens doors to the future.","教育为未来打开大门。"],["Quality education should be available to all.","优质教育应该面向所有人。"]]},
  {"w":"career","p":"/kəˈrɪr/","t":"n.","zh":"职业；事业","lv":"B1","tp":["work","general"],"ex":[["She built a successful career in medicine.","她在医学领域建立了成功的职业。"],["Choose a career you are passionate about.","选择你热衷的职业。"]]},
  {"w":"ambition","p":"/æmˈbɪʃən/","t":"n.","zh":"雄心；抱负","lv":"B1","tp":["work","general"],"ex":[["He has a great ambition to be a doctor.","他有成为医生的远大抱负。"],["Ambition drives people to succeed.","雄心驱使人们走向成功。"]]},
  {"w":"confidence","p":"/ˈkɑːnfɪdəns/","t":"n.","zh":"自信；信心","lv":"B1","tp":["emotion","general"],"ex":[["Confidence is key to success.","自信是成功的关键。"],["She spoke with great confidence.","她充满自信地说话。"]]},
  {"w":"attitude","p":"/ˈætɪtuːd/","t":"n.","zh":"态度；立场","lv":"B1","tp":["emotion","general"],"ex":[["A positive attitude makes a big difference.","积极的态度能带来巨大的改变。"],["Change your attitude, change your life.","改变你的态度，改变你的生活。"]]},
  {"w":"behavior","p":"/bɪˈheɪvjər/","t":"n.","zh":"行为；举止","lv":"B1","tp":["general","social"],"ex":[["Good behavior is expected at school.","在学校需要有良好的行为举止。"],["Your behavior affects those around you.","你的行为影响你周围的人。"]]},
  {"w":"creativity","p":"/ˌkriːeɪˈtɪvəti/","t":"n.","zh":"创造力；创意","lv":"B1","tp":["work","general"],"ex":[["Creativity is valued in this company.","创造力在这家公司很受重视。"],["Art encourages creativity in children.","艺术培养孩子的创造力。"]]},
  {"w":"curiosity","p":"/ˌkjʊriˈɑːsəti/","t":"n.","zh":"好奇心；求知欲","lv":"B1","tp":["emotion","education"],"ex":[["Curiosity leads to discovery.","好奇心带来发现。"],["Children have natural curiosity.","孩子天生有好奇心。"]]},
  {"w":"determination","p":"/dɪˌtɜːrmɪˈneɪʃən/","t":"n.","zh":"决心；决断力","lv":"B1","tp":["emotion","general"],"ex":[["Her determination helped her succeed.","她的决心帮助她成功了。"],["Success requires determination and effort.","成功需要决心和努力。"]]},
  {"w":"discipline","p":"/ˈdɪsɪplɪn/","t":"n.","zh":"纪律；自律","lv":"B1","tp":["general","education"],"ex":[["Discipline is the key to achieving goals.","纪律是实现目标的关键。"],["Self-discipline helps you stay focused.","自律帮助你保持专注。"]]},
  {"w":"emotion","p":"/ɪˈmoʊʃən/","t":"n.","zh":"情感；感情","lv":"B1","tp":["emotion","general"],"ex":[["She expressed her emotions freely.","她自由地表达了她的情感。"],["Managing emotions is important.","管理情绪很重要。"]]},
  {"w":"focus","p":"/ˈfoʊkəs/","t":"v./n.","zh":"专注；焦点","lv":"B1","tp":["work","general"],"ex":[["Focus on what truly matters.","专注于真正重要的事。"],["I need to focus on my studies.","我需要专注于学习。"]]},
  {"w":"growth","p":"/ɡroʊθ/","t":"n.","zh":"成长；增长","lv":"B1","tp":["general"],"ex":[["Personal growth is a lifelong journey.","个人成长是一生的旅程。"],["Economic growth creates new opportunities.","经济增长创造新机会。"]]},
  {"w":"habit","p":"/ˈhæbɪt/","t":"n.","zh":"习惯；惯例","lv":"B1","tp":["general"],"ex":[["Good habits lead to a better life.","好习惯带来更好的生活。"],["Reading is a habit I want to develop.","读书是我想培养的习惯。"]]},
  {"w":"influence","p":"/ˈɪnfluəns/","t":"n./v.","zh":"影响；感化","lv":"B1","tp":["social","general"],"ex":[["Teachers have a great influence on students.","老师对学生有很大的影响。"],["Music can influence your mood.","音乐可以影响你的心情。"]]},
  {"w":"integrity","p":"/ɪnˈteɡrəti/","t":"n.","zh":"诚信；正直","lv":"B1","tp":["social","general"],"ex":[["Integrity is the foundation of trust.","诚信是信任的基础。"],["Act with integrity in all situations.","在所有情况下都要诚信行事。"]]},
  {"w":"leadership","p":"/ˈliːdərʃɪp/","t":"n.","zh":"领导力；领导地位","lv":"B1","tp":["work","general"],"ex":[["Good leadership inspires others.","良好的领导力激励他人。"],["She showed strong leadership during the crisis.","她在危机中表现出强大的领导力。"]]},
  {"w":"motivation","p":"/ˌmoʊtɪˈveɪʃən/","t":"n.","zh":"动力；动机","lv":"B1","tp":["emotion","work"],"ex":[["What is your motivation for learning English?","你学英语的动力是什么？"],["Internal motivation lasts longer.","内在动力持续更久。"]]},
  {"w":"patience","p":"/ˈpeɪʃəns/","t":"n.","zh":"耐心；忍耐","lv":"B1","tp":["emotion","general"],"ex":[["Patience is a virtue.","耐心是一种美德。"],["Learning a language requires patience.","学习一门语言需要耐心。"]]},
  {"w":"perspective","p":"/pərˈspektɪv/","t":"n.","zh":"观点；视角","lv":"B1","tp":["general"],"ex":[["Try to see things from a different perspective.","尝试从不同的角度看事情。"],["Her perspective changed my mind.","她的观点改变了我的想法。"]]},
  {"w":"priority","p":"/praɪˈɔːrəti/","t":"n.","zh":"优先事项；重点","lv":"B1","tp":["work","general"],"ex":[["Health should be your top priority.","健康应该是你的首要任务。"],["Set your priorities and stick to them.","设定你的优先事项并坚持执行。"]]},
  {"w":"progress","p":"/ˈprɑːɡres/","t":"n./v.","zh":"进步；发展","lv":"B1","tp":["general","education"],"ex":[["I am making good progress in English.","我的英语取得了很好的进步。"],["Progress requires constant effort.","进步需要持续的努力。"]]},
  {"w":"purpose","p":"/ˈpɜːrpəs/","t":"n.","zh":"目的；意义","lv":"B1","tp":["general"],"ex":[["Find your purpose in life.","找到你生命的目的。"],["Every action should have a purpose.","每个行动都应该有目的。"]]},
  {"w":"resilience","p":"/rɪˈzɪliəns/","t":"n.","zh":"韧性；复原力","lv":"B1","tp":["emotion","general"],"ex":[["Resilience helps you overcome setbacks.","韧性帮助你克服挫折。"],["Children develop resilience through challenges.","孩子们通过挑战培养韧性。"]]},
  {"w":"skill","p":"/skɪl/","t":"n.","zh":"技能；技巧","lv":"B1","tp":["work","education"],"ex":[["Learning new skills takes time.","学习新技能需要时间。"],["Communication is an important skill.","沟通是一项重要的技能。"]]},
  {"w":"analyze","p":"/ˈænəlaɪz/","t":"v.","zh":"分析；剖析","lv":"B2","tp":["work","education"],"ex":[["We need to analyze the data carefully.","我们需要仔细分析数据。"],["She analyzed the problem from all angles.","她从各个角度分析了这个问题。"]]},
  {"w":"evaluate","p":"/ɪˈvæljueɪt/","t":"v.","zh":"评估；评价","lv":"B2","tp":["work","education"],"ex":[["We need to evaluate the risks first.","我们需要先评估风险。"],["The teacher evaluates students' progress monthly.","老师每月评估学生的进步。"]]},
  {"w":"implement","p":"/ˈɪmplɪment/","t":"v.","zh":"实施；执行","lv":"B2","tp":["work","general"],"ex":[["The company will implement new policies.","公司将实施新政策。"],["It's time to implement our plan.","是时候执行我们的计划了。"]]},
  {"w":"significant","p":"/sɪɡˈnɪfɪkənt/","t":"adj.","zh":"重大的；显著的","lv":"B2","tp":["general"],"ex":[["This is a significant discovery.","这是一项重大发现。"],["The changes have been significant.","这些变化是显著的。"]]},
  {"w":"fundamental","p":"/ˌfʌndəˈmentəl/","t":"adj.","zh":"基本的；根本的","lv":"B2","tp":["general"],"ex":[["Trust is fundamental to any relationship.","信任是任何关系的基础。"],["This is a fundamental problem.","这是一个根本性的问题。"]]},
  {"w":"phenomenon","p":"/fɪˈnɑːmɪnən/","t":"n.","zh":"现象；非凡的人","lv":"B2","tp":["general","education"],"ex":[["Climate change is a global phenomenon.","气候变化是一个全球现象。"],["The social media phenomenon changed communication.","社交媒体现象改变了沟通方式。"]]},
  {"w":"consequence","p":"/ˈkɑːnsɪkwens/","t":"n.","zh":"后果；结果","lv":"B2","tp":["general"],"ex":[["Consider the consequences of your actions.","考虑你行动的后果。"],["There are serious consequences for breaking rules.","违反规定有严重后果。"]]},
  {"w":"assumption","p":"/əˈsʌmpʃən/","t":"n.","zh":"假设；设想","lv":"B2","tp":["general","education"],"ex":[["Don't make assumptions without evidence.","没有证据不要做假设。"],["His assumption was completely wrong.","他的假设完全错误。"]]},
  {"w":"innovative","p":"/ˈɪnəveɪtɪv/","t":"adj.","zh":"创新的；革新的","lv":"B2","tp":["work","general"],"ex":[["They found an innovative solution.","他们找到了一个创新的解决方案。"],["Innovative thinking drives progress.","创新思维推动进步。"]]},
  {"w":"sustainable","p":"/səˈsteɪnəbəl/","t":"adj.","zh":"可持续的；可维持的","lv":"B2","tp":["general","society"],"ex":[["We need sustainable energy solutions.","我们需要可持续的能源解决方案。"],["Sustainable development protects future generations.","可持续发展保护后代。"]]},
  {"w":"comprehensive","p":"/ˌkɑːmprɪˈhensɪv/","t":"adj.","zh":"全面的；综合的","lv":"B2","tp":["general"],"ex":[["We need a comprehensive plan.","我们需要一个全面的计划。"],["She gave a comprehensive review of the topic.","她对这个话题进行了全面的回顾。"]]},
  {"w":"elaborate","p":"/ɪˈlæbərɪt/","t":"adj./v.","zh":"详细的；精心制作","lv":"B2","tp":["general"],"ex":[["Can you elaborate on that point?","你能详细说明那一点吗？"],["She made an elaborate plan.","她制定了一个详细的计划。"]]},
  {"w":"inevitable","p":"/ɪnˈevɪtəbəl/","t":"adj.","zh":"不可避免的；必然的","lv":"B2","tp":["general"],"ex":[["Change is inevitable in life.","生活中的变化是不可避免的。"],["Conflict was inevitable under those circumstances.","在那种情况下冲突是不可避免的。"]]},
  {"w":"sophisticated","p":"/səˈfɪstɪkeɪtɪd/","t":"adj.","zh":"复杂的；有见识的","lv":"B2","tp":["general"],"ex":[["She is a sophisticated thinker.","她是一个思维复杂的人。"],["The system uses sophisticated technology.","该系统使用了复杂的技术。"]]},
  {"w":"dilemma","p":"/dɪˈlemə/","t":"n.","zh":"困境；两难局面","lv":"B2","tp":["general"],"ex":[["She faced a moral dilemma.","她面临一个道德困境。"],["There is no easy answer to this dilemma.","这个困境没有简单的答案。"]]},
  {"w":"paradox","p":"/ˈpærədɑːks/","t":"n.","zh":"悖论；矛盾","lv":"B2","tp":["general","education"],"ex":[["It's a paradox that more choices can lead to less happiness.","更多的选择会导致更少的幸福，这是一个悖论。"],["The paradox of freedom is that limits can increase it.","自由的悖论是限制可以增加它。"]]},
  {"w":"ambiguous","p":"/æmˈbɪɡjuəs/","t":"adj.","zh":"模糊的；含混不清的","lv":"B2","tp":["communication","general"],"ex":[["His answer was ambiguous.","他的回答很模糊。"],["The instructions were ambiguous and confusing.","指示模糊而令人困惑。"]]},
  {"w":"diverse","p":"/daɪˈvɜːrs/","t":"adj.","zh":"多样的；多元的","lv":"B2","tp":["society","general"],"ex":[["Our team has diverse backgrounds.","我们的团队有多元化的背景。"],["A diverse society is a rich society.","多元化的社会是丰富的社会。"]]},
  {"w":"efficient","p":"/ɪˈfɪʃənt/","t":"adj.","zh":"高效的；有效率的","lv":"B2","tp":["work","general"],"ex":[["We need a more efficient process.","我们需要更高效的流程。"],["She is a very efficient worker.","她是一个非常高效的员工。"]]},
  {"w":"impact","p":"/ˈɪmpækt/","t":"n./v.","zh":"影响；冲击","lv":"B2","tp":["general"],"ex":[["Technology has a huge impact on our lives.","技术对我们的生活有巨大影响。"],["The decision impacted the whole company.","这个决定影响了整个公司。"]]},
  {"w":"initiative","p":"/ɪˈnɪʃətɪv/","t":"n.","zh":"主动性；倡议","lv":"B2","tp":["work","general"],"ex":[["Take the initiative and make the first move.","主动出击，迈出第一步。"],["The government launched a new initiative.","政府启动了一项新倡议。"]]},
  {"w":"strategy","p":"/ˈstræt̮ədʒi/","t":"n.","zh":"策略；战略","lv":"B2","tp":["work","general"],"ex":[["We need a better strategy to compete.","我们需要更好的策略来竞争。"],["Their strategy was successful.","他们的策略成功了。"]]},
  {"w":"resource","p":"/ˈriːsɔːrs/","t":"n.","zh":"资源；资料","lv":"B2","tp":["work","general"],"ex":[["Time is our most valuable resource.","时间是我们最宝贵的资源。"],["The company has many resources available.","公司有很多可用资源。"]]},
  {"w":"potential","p":"/pəˈtenʃəl/","t":"n./adj.","zh":"潜力；潜在的","lv":"B2","tp":["general"],"ex":[["Every person has great potential.","每个人都有巨大的潜力。"],["This is a potential problem we should address.","这是我们应该解决的潜在问题。"]]},
  {"w":"collaborate","p":"/kəˈlæbəreɪt/","t":"v.","zh":"合作；协作","lv":"B2","tp":["work","social"],"ex":[["We collaborate with teams around the world.","我们与世界各地的团队合作。"],["Students collaborate on group projects.","学生们在小组项目中合作。"]]},
  {"w":"negotiate","p":"/nɪˈɡoʊʃieɪt/","t":"v.","zh":"谈判；协商","lv":"B2","tp":["work","social"],"ex":[["They negotiated a better deal.","他们谈判达成了更好的协议。"],["You need to negotiate your salary.","你需要谈判薪资。"]]},
  {"w":"advocate","p":"/ˈædvəkeɪt/","t":"v./n.","zh":"倡导；提倡者","lv":"B2","tp":["social","general"],"ex":[["She advocates for human rights.","她倡导人权。"],["He is an advocate for environmental protection.","他是环境保护的倡导者。"]]},
  {"w":"enhance","p":"/ɪnˈhæns/","t":"v.","zh":"增强；提升","lv":"B2","tp":["general","work"],"ex":[["This course will enhance your skills.","这门课将提升你的技能。"],["Technology can enhance productivity.","技术可以提高生产效率。"]]},
  {"w":"facilitate","p":"/fəˈsɪlɪteɪt/","t":"v.","zh":"促进；帮助","lv":"B2","tp":["work","general"],"ex":[["Good management facilitates team success.","良好的管理促进团队成功。"],["Technology facilitates communication.","技术促进了沟通。"]]},
  {"w":"demonstrate","p":"/ˈdemənstreɪt/","t":"v.","zh":"展示；证明","lv":"B2","tp":["communication","general"],"ex":[["Please demonstrate how to use the software.","请展示如何使用该软件。"],["She demonstrated great courage.","她展示了极大的勇气。"]]}
]


path: package.json
content: {
  "name": "english-daily",
  "version": "1.0.2",
  "description": "Daily English learning with spaced repetition — built-in A1–B2 word bank, new words daily, quiz mode (MCQ/fill-in/spelling), streak tracking, level progression.",
  "keywords": [
    "学英语",
    "英语单词",
    "每日单词",
    "英语练习",
    "词汇",
    "测验",
    "打卡",
    "间隔重复",
    "每日推送",
    "English learning",
    "vocabulary",
    "daily words",
    "quiz",
    "streak",
    "spaced repetition",
    "CEFR",
    "word bank"
  ],
  "author": "jiajiaoy",
  "license": "MIT",
  "scripts": {
    "register": "node scripts/register.js",
    "daily": "node scripts/daily-push.js",
    "quiz": "node scripts/quiz.js",
    "progress": "node scripts/progress.js",
    "push-on": "node scripts/push-toggle.js on",
    "push-off": "node scripts/push-toggle.js off",
    "push-status": "node scripts/push-toggle.js status"
  }
}

path: scripts/daily-push.js
content: #!/usr/bin/env node
/**
 * english-daily — 每日学习推送 prompt 生成器
 *
 * 用法:
 *   node daily-push.js <userId>
 *
 * 由 openclaw cron 驱动，每日早晨执行。
 * 输出结构化文本，由 Claude 格式化呈现给用户。
 */

'use strict';

const fs   = require('fs');
const path = require('path');
const {
  getDueWords,
  getNewWordsForUser,
  todayStr,
  addDays
} = require('./wordbank');

const USERS_DIR = path.join(__dirname, '../data/users');

// ── Security helpers ──────────────────────────────────────────────────────────

function sanitizeId(value) {
  if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
    console.error('❌ 无效的 userId：只允许字母、数字、- 和 _，长度 1-128');
    process.exit(1);
  }
  return value;
}

function safeUserPath(userId) {
  const resolved = path.resolve(USERS_DIR, `${userId}.json`);
  if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
    console.error('❌ 非法路径');
    process.exit(1);
  }
  return resolved;
}

function loadUser(userId) {
  const f = safeUserPath(userId);
  if (!fs.existsSync(f)) {
    console.error(`❌ 未找到用户：${userId}。请先注册：node register.js ${userId} <姓名>`);
    process.exit(1);
  }
  return JSON.parse(fs.readFileSync(f, 'utf8'));
}

function saveUser(userId, data) {
  fs.mkdirSync(USERS_DIR, { recursive: true });
  fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}

// ── Streak update ─────────────────────────────────────────────────────────────

function updateStreak(profile) {
  const today     = todayStr();
  const yesterday = addDays(today, -1);
  const last      = profile.lastStudyDate;

  if (last === today) {
    // Already counted today, no change
    return profile;
  } else if (last === yesterday) {
    profile.streak = (profile.streak || 0) + 1;
  } else {
    // Streak broken (or first study)
    profile.streak = 1;
  }

  if (profile.streak > (profile.longestStreak || 0)) {
    profile.longestStreak = profile.streak;
  }

  profile.lastStudyDate = today;
  return profile;
}

// ── Format helpers ────────────────────────────────────────────────────────────

function formatWord(entry) {
  const ex = entry.ex && entry.ex[0] ? `${entry.ex[0][0]} / ${entry.ex[0][1]}` : '';
  return `${entry.w} | ${entry.p} | ${entry.t} | ${entry.zh}${ex ? ' | ' + ex : ''}`;
}

function formatWordFull(entry) {
  const lines = [`${entry.w} | ${entry.p} | ${entry.t} | ${entry.zh}`];
  if (entry.ex) {
    entry.ex.forEach(([en, zh]) => lines.push(`  例: ${en} | ${zh}`));
  }
  return lines.join('\n');
}

// ── Core function (exportable for cron) ───────────────────────────────────────

function runDailyPush(userId) {
  userId = sanitizeId(userId);
  const profile = loadUser(userId);

  // Update streak
  updateStreak(profile);

  const today      = todayStr();
  const dailyGoal  = profile.preferences && profile.preferences.dailyGoal ? profile.preferences.dailyGoal : 5;
  const dueWords   = getDueWords(profile);
  const newWords   = getNewWordsForUser(profile, dailyGoal);

  // Save updated profile (streak + lastStudyDate)
  saveUser(userId, profile);

  // Format date nicely
  const dateObj = new Date(today + 'T12:00:00Z');
  const dateDisplay = dateObj.toLocaleDateString('zh-CN', {
    year: 'numeric', month: 'long', day: 'numeric', weekday: 'long',
    timeZone: 'Asia/Shanghai'
  });

  // ── Output ─────────────────────────────────────────────────────────────────
  console.log(`=== 今日英语学习 · ${dateDisplay} ===`);
  console.log(`用户：${profile.name} | 等级：${profile.level} | 连续学习：${profile.streak}天 | 积分：${profile.totalPoints || 0}`);
  console.log('');

  if (dueWords.length > 0) {
    console.log(`【复习】（${dueWords.length}个需要复习的单词）`);
    dueWords.forEach(w => console.log(formatWord(w)));
    console.log('');
  } else {
    console.log('【复习】今日无需复习的单词 ✅');
    console.log('');
  }

  if (newWords.length > 0) {
    console.log(`【今日新词】（目标：${dailyGoal}个）`);
    newWords.forEach(w => console.log(formatWordFull(w)));
    console.log('');
  } else {
    console.log(`【今日新词】当前等级（${profile.level}）的单词已全部学完！请尝试提升等级。`);
    console.log('');
  }

  console.log('【学习建议】');
  console.log('- 先复习旧词，再学新词');
  console.log('- 每个单词至少造一个句子');
  console.log('- 回复"测验"开始今日练习');
  console.log('');
  console.log(`📊 查看进度：node scripts/progress.js ${userId}`);
  console.log(`📝 开始测验：node scripts/quiz.js ${userId}`);
}

// ── CLI entry ─────────────────────────────────────────────────────────────────

if (require.main === module) {
  const args = process.argv.slice(2);
  if (!args[0]) {
    console.log('用法: node daily-push.js <userId>');
    process.exit(1);
  }
  runDailyPush(args[0]);
}

module.exports = { runDailyPush };


path: scripts/evening-push.js
content: #!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`英语晚间复习时间🌙（${date}）。请为用户生成今日词汇测验：node scripts/quiz.js ${userId} mixed。逐题互动，完成后调用 node scripts/quiz.js ${userId} --score <分数> 记录成绩。附今日学习打卡总结和明日预告。`);


path: scripts/morning-push.js
content: #!/usr/bin/env node
'use strict';
const fs=require('fs'),path=require('path');
const USERS_DIR=path.join(__dirname,'../data/users');
function sanitizeId(v){if(typeof v!=='string'||!/^[a-zA-Z0-9_-]{1,128}$/.test(v)){console.error('invalid userId');process.exit(1);}return v;}
function safeUserPath(u){const r=path.resolve(USERS_DIR,u+'.json');if(!r.startsWith(path.resolve(USERS_DIR)+path.sep)){console.error('illegal path');process.exit(1);}return r;}
function loadUser(u){const f=safeUserPath(u);return fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')):{};}
const userId=sanitizeId(process.argv[2]||'default');
loadUser(userId);
const now=new Date();
const WEEKDAYS=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
const wd=now.getDay();
const date=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
const weekday=WEEKDAYS[wd];
const month=now.getMonth()+1;
const day=now.getDate();
const tomorrow_weekday=WEEKDAYS[(wd+1)%7];
console.log(`今天是${weekday}（${date}）。请运行今日英语学习推送：node scripts/daily-push.js ${userId}。输出今日复习词汇+新词列表，并附早间学习激励一句话（英文）。`);


path: scripts/progress.js
content: #!/usr/bin/env node
/**
 * english-daily — 学习进度查看
 *
 * 用法:
 *   node progress.js <userId>
 */

'use strict';

const fs   = require('fs');
const path = require('path');
const { getWordStats, loadWordBank, todayStr, addDays } = require('./wordbank');

const USERS_DIR = path.join(__dirname, '../data/users');

// ── Security helpers ──────────────────────────────────────────────────────────

function sanitizeId(value) {
  if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
    console.error('❌ 无效的 userId：只允许字母、数字、- 和 _，长度 1-128');
    process.exit(1);
  }
  return value;
}

function safeUserPath(userId) {
  const resolved = path.resolve(USERS_DIR, `${userId}.json`);
  if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
    console.error('❌ 非法路径');
    process.exit(1);
  }
  return resolved;
}

function loadUser(userId) {
  const f = safeUserPath(userId);
  if (!fs.existsSync(f)) {
    console.error(`❌ 未找到用户：${userId}。请先注册：node register.js ${userId} <姓名>`);
    process.exit(1);
  }
  return JSON.parse(fs.readFileSync(f, 'utf8'));
}

function saveUser(userId, data) {
  fs.mkdirSync(USERS_DIR, { recursive: true });
  fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}

// ── Level-up thresholds ───────────────────────────────────────────────────────

const LEVEL_UP = {
  A1: { wordsNeeded: 40,  next: 'A2' },
  A2: { wordsNeeded: 90,  next: 'B1' },
  B1: { wordsNeeded: 130, next: 'B2' },
  B2: { wordsNeeded: Infinity, next: null }
};

function checkLevelUp(profile) {
  const threshold = LEVEL_UP[profile.level];
  if (!threshold || !threshold.next) return false;

  const stats = getWordStats(profile);
  if (stats.mastered >= threshold.wordsNeeded) {
    return threshold.next;
  }
  return false;
}

// ── Weekly study days ─────────────────────────────────────────────────────────

function getWeeklyDays(profile) {
  // Count study days in the last 7 calendar days
  // We only track lastStudyDate precisely; use streak as proxy
  const streak = profile.streak || 0;
  return Math.min(streak, 7);
}

// ── Words to next level ───────────────────────────────────────────────────────

function wordsToNextLevel(profile) {
  const threshold = LEVEL_UP[profile.level];
  if (!threshold || !threshold.next) return 0;
  const stats = getWordStats(profile);
  return Math.max(0, threshold.wordsNeeded - stats.mastered);
}

// ── Main ──────────────────────────────────────────────────────────────────────

const args = process.argv.slice(2);
if (!args[0]) {
  console.log('用法: node progress.js <userId>');
  process.exit(1);
}

const userId  = sanitizeId(args[0]);
const profile = loadUser(userId);

// Check for level-up
const newLevel = checkLevelUp(profile);
if (newLevel) {
  console.log(`\n🎉 恭喜！你已升级至 ${newLevel}！`);
  profile.level = newLevel;
  profile.targetLevel = LEVEL_UP[newLevel] ? LEVEL_UP[newLevel].next || newLevel : newLevel;
  saveUser(userId, profile);
  console.log(`等级已更新：${profile.level} → 目标 ${profile.targetLevel}\n`);
}

const stats      = getWordStats(profile);
const bank       = loadWordBank();
const allAtLevel = bank.filter(w => {
  const levels = ['A1','A2','B1','B2'];
  return levels.indexOf(w.lv) <= levels.indexOf(profile.level);
}).length;

const weeklyDays   = getWeeklyDays(profile);
const toNextLevel  = wordsToNextLevel(profile);
const threshold    = LEVEL_UP[profile.level];

console.log(`
📊 学习进度 — ${profile.name}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🔥 连续学习：${profile.streak || 0}天（最长：${profile.longestStreak || 0}天）
⭐ 总积分：${profile.totalPoints || 0}
📚 已学单词：${Object.keys(profile.wordProgress || {}).length} / ${allAtLevel}
🎯 当前等级：${profile.level} → 目标：${profile.targetLevel || profile.level}

词汇详情：
  已掌握（间隔≥7天）：${stats.mastered}个
  学习中（间隔<7天）：${stats.learning}个
  待复习（今日到期）：${stats.due}个

📅 本周学习：${weeklyDays}/7天
${threshold && threshold.next
  ? `💡 距离下一等级（${threshold.next}）：还需掌握 ${toNextLevel} 个单词`
  : '🏆 已达到最高等级 B2！'}

${stats.due > 0 ? `⚠️  今日有 ${stats.due} 个单词需要复习！` : '✅ 今日无待复习单词'}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
开始今日学习：node scripts/daily-push.js ${userId}
开始测验：    node scripts/quiz.js ${userId}
`);


path: scripts/push-toggle.js
content: #!/usr/bin/env node
/**
 * english-daily — 每日推送开关
 *
 * 用法:
 *   node push-toggle.js on <userId> [--morning HH:MM] [--channel telegram]
 *   node push-toggle.js off <userId>
 *   node push-toggle.js status <userId>
 *
 * 支持渠道：telegram / feishu / slack / discord
 */

'use strict';

const fs   = require('fs');
const path = require('path');

const USERS_DIR = path.join(__dirname, '../data/users');
const ALLOWED_CHANNELS = new Set(['telegram', 'feishu', 'slack', 'discord']);

// ── Security helpers ──────────────────────────────────────────────────────────

function sanitizeId(value, label) {
  if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
    console.error(`❌ 无效的 ${label}：只允许字母、数字、- 和 _，长度 1-128`);
    process.exit(1);
  }
  return value;
}

function sanitizeTime(value, label) {
  if (typeof value !== 'string' || !/^\d{1,2}:\d{2}$/.test(value)) {
    console.error(`❌ 无效的 ${label}：格式应为 HH:MM，如 08:00`);
    process.exit(1);
  }
  const [h, m] = value.split(':').map(Number);
  if (h < 0 || h > 23 || m < 0 || m > 59) {
    console.error(`❌ 无效的 ${label}：小时 0-23，分钟 0-59`);
    process.exit(1);
  }
  return { h, m };
}

function safeUserPath(userId) {
  const resolved = path.resolve(USERS_DIR, `${userId}.json`);
  if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
    console.error('❌ 非法路径');
    process.exit(1);
  }
  return resolved;
}

function loadUser(userId) {
  const f = safeUserPath(userId);
  if (!fs.existsSync(f)) return null;
  return JSON.parse(fs.readFileSync(f, 'utf8'));
}

function saveUser(userId, data) {
  fs.mkdirSync(USERS_DIR, { recursive: true });
  fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}

// ── Commands ──────────────────────────────────────────────────────────────────

function enablePush(userId, opts = {}) {
  userId = sanitizeId(userId, 'userId');

  const { h: mh, m: mm } = sanitizeTime(opts.morning || '08:00', 'morning');
  const morningDisplay   = `${String(mh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`;
  const morningCron      = `${mm} ${mh} * * *`;

  const rawChannel = opts.channel || 'telegram';
  if (!ALLOWED_CHANNELS.has(rawChannel)) {
    console.error(`❌ 不支持的渠道：${rawChannel}。支持：${[...ALLOWED_CHANNELS].join(', ')}`);
    process.exit(1);
  }
  const channel = rawChannel;

  const sessionKey = `agent:main:${channel}:direct:${userId}`;

  const cronConfig = {
    name: `english-daily-morning-${userId}`,
    cronExpr: morningCron,
    tz: 'Asia/Shanghai',
    session: 'isolated',
    sessionKey,
    channel,
    to: userId,
    announce: true,
    timeoutSeconds: 120,
    message: `node ${path.join(__dirname, 'daily-push.js')} ${userId}`
  };
  console.log(`__OPENCLAW_CRON_ADD__:${JSON.stringify(cronConfig)}`);

  // Update user preferences
  const user = loadUser(userId) || {};
  if (user.preferences) {
    user.preferences.pushEnabled  = true;
    user.preferences.morningTime  = morningDisplay;
    user.preferences.channel      = channel;
  }
  user.pushEnabled   = true;
  user.morningTime   = morningDisplay;
  user.channel       = channel;
  user.pushEnabledAt = new Date().toISOString();
  saveUser(userId, user);

  console.log(`
✅ 每日英语推送已开启

⏰ 推送时间：每天 ${morningDisplay}（今日单词 + 复习）
📡 推送渠道：${channel}

关闭推送：node push-toggle.js off ${userId}
查看状态：node push-toggle.js status ${userId}`);
}

function disablePush(userId) {
  userId = sanitizeId(userId, 'userId');
  const user = loadUser(userId);
  if (!user) {
    console.log(`❌ 未找到用户 ${userId} 的推送记录`);
    return;
  }

  console.log(`__OPENCLAW_CRON_RM__:english-daily-morning-${userId}`);

  if (user.preferences) user.preferences.pushEnabled = false;
  user.pushEnabled    = false;
  user.pushDisabledAt = new Date().toISOString();
  saveUser(userId, user);

  console.log(`✅ 每日英语推送已关闭`);
}

function showStatus(userId) {
  userId = sanitizeId(userId, 'userId');
  const user = loadUser(userId);
  if (!user) {
    console.log(`❌ 未找到用户 ${userId} 的推送记录（请先注册）`);
    return;
  }

  const enabled     = user.pushEnabled || (user.preferences && user.preferences.pushEnabled) || false;
  const morningTime = user.morningTime || (user.preferences && user.preferences.morningTime) || '08:00';
  const channel     = user.channel || (user.preferences && user.preferences.channel) || 'telegram';
  const enabledAt   = user.pushEnabledAt ? user.pushEnabledAt.split('T')[0] : '未知';

  console.log(`
📡 推送状态 — ${userId}（${user.name || ''}）
━━━━━━━━━━━━━━━━━━━━━━━
状态：    ${enabled ? '✅ 开启中' : '❌ 已关闭'}
推送时间：${morningTime}
渠道：    ${channel}
${enabled ? '开启于：  ' + enabledAt : ''}
━━━━━━━━━━━━━━━━━━━━━━━`);
}

module.exports = { enablePush, disablePush, showStatus };

// ── CLI entry ─────────────────────────────────────────────────────────────────

if (require.main !== module) return;

const args    = process.argv.slice(2);
const command = args[0];
const userId  = args[1];

if (!command || !userId) {
  console.log(`用法:
  node push-toggle.js on <userId> [--morning 08:00] [--channel telegram]
  node push-toggle.js off <userId>
  node push-toggle.js status <userId>`);
  process.exit(1);
}

const opts = {};
const mi = args.indexOf('--morning');
if (mi !== -1) opts.morning = args[mi + 1];
const ci = args.indexOf('--channel');
if (ci !== -1) opts.channel = args[ci + 1];

switch (command) {
  case 'on':     enablePush(userId, opts); break;
  case 'off':    disablePush(userId);      break;
  case 'status': showStatus(userId);       break;
  default:
    console.error(`❌ 未知命令：${command}（支持 on/off/status）`);
    process.exit(1);
}


path: scripts/quiz.js
content: #!/usr/bin/env node
/**
 * english-daily — 测验/练习生成器
 *
 * 用法:
 *   node quiz.js <userId> [type]
 *   node quiz.js <userId> --score <points>
 *
 * type: vocab | sentence | mixed（默认 mixed）
 *
 * 生成5道题（含答案），由 Claude 逐题互动呈现。
 * 答题完成后 Claude 调用：node quiz.js <userId> --score <points>
 */

'use strict';

const fs   = require('fs');
const path = require('path');
const {
  getDueWords,
  getNewWordsForUser,
  updateWordProgress,
  loadWordBank,
  todayStr
} = require('./wordbank');

const USERS_DIR = path.join(__dirname, '../data/users');
const QUESTIONS_PER_QUIZ = 5;

// ── Security helpers ──────────────────────────────────────────────────────────

function sanitizeId(value) {
  if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
    console.error('❌ 无效的 userId：只允许字母、数字、- 和 _，长度 1-128');
    process.exit(1);
  }
  return value;
}

function safeUserPath(userId) {
  const resolved = path.resolve(USERS_DIR, `${userId}.json`);
  if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
    console.error('❌ 非法路径');
    process.exit(1);
  }
  return resolved;
}

function loadUser(userId) {
  const f = safeUserPath(userId);
  if (!fs.existsSync(f)) {
    console.error(`❌ 未找到用户：${userId}。请先注册：node register.js ${userId} <姓名>`);
    process.exit(1);
  }
  return JSON.parse(fs.readFileSync(f, 'utf8'));
}

function saveUser(userId, data) {
  fs.mkdirSync(USERS_DIR, { recursive: true });
  fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}

// ── Helpers ───────────────────────────────────────────────────────────────────

/** Simple deterministic pseudo-shuffle based on date + userId seed */
function shuffle(arr, seed) {
  const a = arr.slice();
  let s = seed;
  for (let i = a.length - 1; i > 0; i--) {
    s = ((s * 1664525) + 1013904223) & 0xffffffff;
    const j = Math.abs(s) % (i + 1);
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

function dateSeed(userId) {
  const today = todayStr().replace(/-/g, '');
  let hash = 0;
  const str = userId + today;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0;
  }
  return Math.abs(hash);
}

/** Get the pool of words a user has studied + due words + new words */
function getQuizPool(profile) {
  const bank = loadWordBank();
  const progress = profile.wordProgress || {};

  // Include: due words + recently studied words + new words (padded)
  const studied = bank.filter(w => !!progress[w.w]);
  const due      = studied.filter(w => {
    const p = progress[w.w];
    return p && p.nextReview <= todayStr();
  });

  // If user has less than 5 studied words, also grab new ones to pad
  let pool = studied.length >= QUESTIONS_PER_QUIZ
    ? studied
    : [...studied, ...getNewWordsForUser(profile, QUESTIONS_PER_QUIZ - studied.length)];

  // Ensure uniqueness
  const seen = new Set();
  pool = pool.filter(w => {
    if (seen.has(w.w)) return false;
    seen.add(w.w);
    return true;
  });

  return { pool, due };
}

/** Pick wrong answers for multiple choice (3 distractors) */
function getDistractors(correctEntry, bank, seed) {
  const others = bank.filter(w => w.w !== correctEntry.w && w.zh !== correctEntry.zh);
  const shuffled = shuffle(others, seed + 1);
  return shuffled.slice(0, 3).map(w => w.zh);
}

/** Build a vocab question (word → Chinese, multiple choice) */
function buildVocabQuestion(entry, bank, qNum, seed) {
  const distractors = getDistractors(entry, bank, seed + qNum);
  const options = shuffle([entry.zh, ...distractors], seed + qNum * 13);
  const correctIdx = options.indexOf(entry.zh);
  const labels = ['A', 'B', 'C', 'D'];

  const optionStr = options.map((o, i) => `${labels[i]}. ${o}`).join('  ');
  const answer    = labels[correctIdx];
  const example   = entry.ex && entry.ex[0] ? entry.ex[0][0] : '';

  return {
    type: 'vocab',
    word: entry.w,
    question: `Q${qNum}. What does "${entry.w}" mean?\n${entry.p}  ${entry.t}\n${optionStr}`,
    answer: `Answer: ${answer} | 解析: ${entry.w} = ${entry.zh}${example ? '，例：' + example : ''}`
  };
}

/** Build a sentence (fill-in-the-blank) question */
function buildSentenceQuestion(entry, qNum) {
  // Pick a sentence example if available
  const exPair = entry.ex && entry.ex.length > 0 ? entry.ex[0] : null;
  if (!exPair) {
    // Fallback to simple pattern
    return {
      type: 'sentence',
      word: entry.w,
      question: `Q${qNum}. 用 "${entry.zh}" 填空：_____ is important in life.\n（提示：${entry.p}  ${entry.t}）`,
      answer: `Answer: ${entry.w} | 全句: ${entry.w} is important in life.`
    };
  }

  const [enSentence, zhSentence] = exPair;
  // Find a good word to blank out — use the target word if present in example
  const wordLower = entry.w.toLowerCase();
  const regex     = new RegExp(`\\b${wordLower}(?:ed|ing|s|d|es)?\\b`, 'i');
  const match     = enSentence.match(regex);

  if (!match) {
    return {
      type: 'sentence',
      word: entry.w,
      question: `Q${qNum}. 翻译成英文（${zhSentence}）\n（提示：核心词 = ${entry.zh}，${entry.p}）`,
      answer: `Answer: ${entry.w} | 参考译文: ${enSentence}`
    };
  }

  const blanked = enSentence.replace(match[0], '_____');
  return {
    type: 'sentence',
    word: entry.w,
    question: `Q${qNum}. 填空 (${entry.zh})：\n${blanked}\n中文提示：${zhSentence}`,
    answer: `Answer: ${match[0]} | 全句: ${enSentence}`
  };
}

/** Generate quiz questions */
function generateQuiz(profile, type, seed) {
  const bank = loadWordBank();
  const { pool } = getQuizPool(profile);

  if (pool.length < QUESTIONS_PER_QUIZ) {
    return null; // Not enough words
  }

  const words   = shuffle(pool, seed).slice(0, QUESTIONS_PER_QUIZ);
  const questions = [];

  words.forEach((entry, idx) => {
    const qNum = idx + 1;
    let q;
    if (type === 'vocab') {
      q = buildVocabQuestion(entry, bank, qNum, seed);
    } else if (type === 'sentence') {
      q = buildSentenceQuestion(entry, qNum);
    } else {
      // mixed: alternate
      q = idx % 2 === 0
        ? buildVocabQuestion(entry, bank, qNum, seed)
        : buildSentenceQuestion(entry, qNum);
    }
    questions.push(q);
  });

  return { words: words.map(w => w.w), questions };
}

// ── Score recording ───────────────────────────────────────────────────────────

function recordScore(userId, rawPoints) {
  userId = sanitizeId(userId);
  const points = parseInt(rawPoints, 10);
  if (isNaN(points) || points < 0 || points > 500) {
    console.error('❌ 无效积分值（0-500）');
    process.exit(1);
  }

  const profile = loadUser(userId);
  profile.totalPoints = (profile.totalPoints || 0) + points;

  const correct = Math.round(points / 10);

  // Update word quality for the quiz words (simplified: use quality=3 for correct)
  const bank = loadWordBank();
  const { pool } = getQuizPool(profile);
  const seed = dateSeed(userId);
  const quizWords = shuffle(pool, seed).slice(0, QUESTIONS_PER_QUIZ).map(w => w.w);

  quizWords.forEach((word, idx) => {
    const quality = idx < correct ? 3 : 1;
    updateWordProgress(profile, word, quality);
  });

  // Update wordsLearned count
  const newlyLearned = quizWords.filter(w => {
    const p = profile.wordProgress[w];
    return p && p.repetitions === 0;
  });
  profile.wordsLearned = Object.keys(profile.wordProgress).length;

  saveUser(userId, profile);

  console.log(`
✅ 积分已记录！

本次得分：+${points}分
累计积分：${profile.totalPoints}分
已掌握单词：${profile.wordsLearned}个

继续加油！🎉`);
}

// ── CLI entry ─────────────────────────────────────────────────────────────────

if (require.main === module) {
  const args = process.argv.slice(2);

  if (!args[0]) {
    console.log(`用法:
  node quiz.js <userId> [type]          生成测验（type: vocab/sentence/mixed）
  node quiz.js <userId> --score <pts>   记录本次得分（10分/题）`);
    process.exit(1);
  }

  // --score mode
  const scoreIdx = args.indexOf('--score');
  if (scoreIdx !== -1) {
    const pts = args[scoreIdx + 1];
    if (!pts) {
      console.error('❌ --score 后需要跟积分数值');
      process.exit(1);
    }
    recordScore(args[0], pts);
    process.exit(0);
  }

  // Generate quiz
  const userId = sanitizeId(args[0]);
  const rawType = (args[1] || 'mixed').toLowerCase();
  const validTypes = ['vocab', 'sentence', 'mixed'];
  if (!validTypes.includes(rawType)) {
    console.error(`❌ 无效的测验类型：${rawType}。支持：${validTypes.join('/')}`);
    process.exit(1);
  }

  const profile = loadUser(userId);
  const seed    = dateSeed(userId);
  const quiz    = generateQuiz(profile, rawType, seed);

  if (!quiz) {
    console.log(`
❌ 单词量不足（需要至少 ${QUESTIONS_PER_QUIZ} 个已学单词或新单词）。
请先完成今日学习：node daily-push.js ${userId}
`);
    process.exit(0);
  }

  const typeNames = { vocab: '词义选择', sentence: '句子填空', mixed: '综合练习' };
  console.log(`
📝 英语测验 — ${profile.name}（${profile.level}）
类型：${typeNames[rawType]}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
`);

  quiz.questions.forEach(q => {
    console.log(q.question);
    console.log(q.answer);
    console.log('');
  });

  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
  console.log('【Claude 操作指南】');
  console.log('请将以上题目逐题呈现给用户，等待回答后再显示答案。');
  console.log('所有题目完成后，根据正确题数运行以下命令记录积分：');
  console.log(`  node ${path.join(__dirname, 'quiz.js')} ${userId} --score <正确题数×10>`);
  console.log('例如答对3题：--score 30');
}

module.exports = { generateQuiz, recordScore };


path: scripts/register.js
content: #!/usr/bin/env node
/**
 * english-daily — 用户注册脚本
 *
 * 用法:
 *   node register.js <userId> <name> [level] [dailyGoal]
 *
 * 参数:
 *   userId     - 用户ID（字母/数字/连字符/下划线，1-128字符）
 *   name       - 用户名称
 *   level      - 起始等级 A1/A2/B1/B2（默认 B1）
 *   dailyGoal  - 每日新单词目标 1-20（默认 5）
 *
 * 示例:
 *   node register.js 123456 张三
 *   node register.js 123456 张三 A2 8
 */

'use strict';

const fs   = require('fs');
const path = require('path');
const { getWordsByLevel, getNewWordsForUser } = require('./wordbank');

const USERS_DIR = path.join(__dirname, '../data/users');

const VALID_LEVELS = ['A1', 'A2', 'B1', 'B2'];

// ── Security helpers ──────────────────────────────────────────────────────────

function sanitizeId(value) {
  if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(value)) {
    console.error('❌ 无效的 userId：只允许字母、数字、- 和 _，长度 1-128');
    process.exit(1);
  }
  return value;
}

function safeUserPath(userId) {
  const resolved = path.resolve(USERS_DIR, `${userId}.json`);
  if (!resolved.startsWith(path.resolve(USERS_DIR) + path.sep)) {
    console.error('❌ 非法路径');
    process.exit(1);
  }
  return resolved;
}

// ── I/O helpers ───────────────────────────────────────────────────────────────

function saveUser(userId, data) {
  fs.mkdirSync(USERS_DIR, { recursive: true });
  fs.writeFileSync(safeUserPath(userId), JSON.stringify(data, null, 2), 'utf8');
}

function loadUser(userId) {
  const f = safeUserPath(userId);
  if (!fs.existsSync(f)) return null;
  return JSON.parse(fs.readFileSync(f, 'utf8'));
}

// ── Target level map ──────────────────────────────────────────────────────────

function nextLevel(level) {
  const idx = VALID_LEVELS.indexOf(level);
  return idx < VALID_LEVELS.length - 1 ? VALID_LEVELS[idx + 1] : level;
}

// ── Main ──────────────────────────────────────────────────────────────────────

const args = process.argv.slice(2);

if (args.length < 2) {
  console.log(`
用法:
  node register.js <userId> <name> [level] [dailyGoal]

参数:
  userId     用户ID（字母/数字/连字符/下划线，1-128字符）
  name       用户名称
  level      起始等级 A1/A2/B1/B2（默认 B1）
  dailyGoal  每日新单词目标 1-20（默认 5）

示例:
  node register.js telegram_123 张三
  node register.js telegram_123 张三 A2 8
`);
  process.exit(1);
}

const rawId      = args[0];
const rawName    = args[1];
const rawLevel   = (args[2] || 'B1').toUpperCase();
const rawGoal    = args[3];

// Validate
const userId = sanitizeId(rawId);

if (!rawName || rawName.trim().length === 0) {
  console.error('❌ 用户名称不能为空');
  process.exit(1);
}
const name = rawName.trim().slice(0, 64);

if (!VALID_LEVELS.includes(rawLevel)) {
  console.error(`❌ 无效的等级：${rawLevel}。支持：${VALID_LEVELS.join('/')}`);
  process.exit(1);
}
const level = rawLevel;

let dailyGoal = 5;
if (rawGoal !== undefined) {
  dailyGoal = parseInt(rawGoal, 10);
  if (isNaN(dailyGoal) || dailyGoal < 1 || dailyGoal > 20) {
    console.error('❌ dailyGoal 必须是 1-20 的整数');
    process.exit(1);
  }
}

// Check existing
const existing = loadUser(userId);
if (existing) {
  console.log(`⚠️  用户 ${userId} 已存在（${existing.name}，等级 ${existing.level}）`);
  console.log('如需更新，请直接修改 data/users/<userId>.json 或重新注册（将覆盖原有进度）。');
  process.exit(0);
}

const now = new Date().toISOString();

const profile = {
  userId,
  name,
  level,
  targetLevel: nextLevel(level),
  nativeLanguage: 'zh',
  streak: 0,
  longestStreak: 0,
  lastStudyDate: null,
  totalPoints: 0,
  wordsLearned: 0,
  preferences: {
    dailyGoal,
    pushEnabled: false,
    morningTime: '08:00',
    channel: 'telegram'
  },
  wordProgress: {},
  createdAt: now
};

saveUser(userId, profile);

// Count available words at their level
const availableWords = getNewWordsForUser(profile, 9999).length;

console.log(`
✅ 注册成功！

用户ID：${userId}
姓名：  ${name}
等级：  ${level}（目标：${profile.targetLevel}）
每日目标：${dailyGoal} 个新单词

📚 当前等级可学单词：${availableWords} 个

💡 下一步：
   查看今日学习  → node scripts/daily-push.js ${userId}
   开始测验      → node scripts/quiz.js ${userId}
   查看进度      → node scripts/progress.js ${userId}
   开启每日推送  → node scripts/push-toggle.js on ${userId}
`);

module.exports = { saveUser, loadUser, sanitizeId, safeUserPath };


path: scripts/wordbank.js
content: #!/usr/bin/env node
/**
 * english-daily — Word Bank Loader + SRS Utilities
 * Utility module (not a CLI entry point). Exports helpers for all scripts.
 */

'use strict';

const fs = require('fs');
const path = require('path');

const WORDBANK_PATH = path.join(__dirname, '../data/wordbank.json');

/** Load and return the full word bank array */
function loadWordBank() {
  const raw = fs.readFileSync(WORDBANK_PATH, 'utf8');
  return JSON.parse(raw);
}

/** Filter words by CEFR level (A1/A2/B1/B2) */
function getWordsByLevel(level) {
  return loadWordBank().filter(w => w.lv === level);
}

/** Find a word entry by exact word string */
function getWordByText(word) {
  return loadWordBank().find(w => w.w === word) || null;
}

/**
 * Return words the user hasn't studied yet, matching their level.
 * Falls back to next level up if not enough words remain.
 * @param {Object} profile  user profile
 * @param {number} count    how many new words to return
 * @returns {Array}
 */
function getNewWordsForUser(profile, count) {
  const bank = loadWordBank();
  const studied = new Set(Object.keys(profile.wordProgress || {}));

  // Levels to draw from, starting at user's level and going up
  const levelOrder = ['A1', 'A2', 'B1', 'B2'];
  const startIdx = levelOrder.indexOf(profile.level || 'B1');
  const levelsToTry = levelOrder.slice(startIdx);

  const candidates = [];
  for (const lv of levelsToTry) {
    const words = bank.filter(w => w.lv === lv && !studied.has(w.w));
    candidates.push(...words);
    if (candidates.length >= count) break;
  }

  return candidates.slice(0, count);
}

/**
 * Return all words due for SRS review today (nextReview <= today).
 * @param {Object} profile
 * @returns {Array} array of word bank entries
 */
function getDueWords(profile) {
  const bank = loadWordBank();
  const today = todayStr();
  const progress = profile.wordProgress || {};

  return bank.filter(entry => {
    const p = progress[entry.w];
    if (!p) return false;
    return p.nextReview <= today;
  });
}

/**
 * SM-2 simplified SRS update.
 * quality: 1=forgot, 2=hard, 3=ok, 4=easy
 * @param {Object} profile  (mutated in place)
 * @param {string} word
 * @param {number} quality  1-4
 */
function updateWordProgress(profile, word, quality) {
  if (!profile.wordProgress) profile.wordProgress = {};

  const prev = profile.wordProgress[word] || { interval: 1, repetitions: 0, ease: 2.5 };
  let { interval, repetitions, ease } = prev;

  if (quality <= 2) {
    interval = 1;
    repetitions = 0;
  } else {
    const multiplier = quality === 3 ? 1.5 : 2;
    interval = Math.min(Math.round(interval * multiplier), 30);
    repetitions += 1;
  }

  // Ease factor adjustment (SM-2 inspired)
  ease = Math.max(1.3, ease + 0.1 - (4 - quality) * 0.08);

  const nextReview = addDays(todayStr(), interval);

  profile.wordProgress[word] = {
    interval,
    repetitions,
    ease: Math.round(ease * 100) / 100,
    nextReview,
    lastQuality: quality
  };
}

/**
 * Summary stats for a user's word progress.
 * @param {Object} profile
 * @returns {{ total: number, due: number, mastered: number, learning: number }}
 */
function getWordStats(profile) {
  const bank = loadWordBank();
  const total = bank.filter(w => w.lv === profile.level ||
    ['A1','A2','B1','B2'].indexOf(w.lv) <= ['A1','A2','B1','B2'].indexOf(profile.level)).length;
  const today = todayStr();
  const progress = profile.wordProgress || {};

  let due = 0, mastered = 0, learning = 0;
  for (const [word, p] of Object.entries(progress)) {
    if (p.nextReview <= today) due++;
    if (p.interval >= 7) mastered++;
    else learning++;
  }

  return { total, due, mastered, learning };
}

// ── Helpers ───────────────────────────────────────────────────────────────────

/** Returns today's date as YYYY-MM-DD string (Asia/Shanghai local time) */
function todayStr() {
  const now = new Date();
  const cst = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
  const y = cst.getFullYear();
  const m = String(cst.getMonth() + 1).padStart(2, '0');
  const d = String(cst.getDate()).padStart(2, '0');
  return `${y}-${m}-${d}`;
}

/** Add `days` to a YYYY-MM-DD string, return new YYYY-MM-DD string */
function addDays(dateStr, days) {
  const d = new Date(dateStr + 'T12:00:00Z');
  d.setUTCDate(d.getUTCDate() + days);
  return d.toISOString().slice(0, 10);
}

module.exports = {
  loadWordBank,
  getWordsByLevel,
  getWordByText,
  getNewWordsForUser,
  getDueWords,
  updateWordProgress,
  getWordStats,
  todayStr,
  addDays
};


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)