@rookiestar/eng-lang-tutor 1.2.4 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,57 +2,6 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [1.2.4] - 2026-02-27
6
-
7
- ### Fixed
8
- - **TTS voice selection**: Audio composer now automatically selects appropriate voices based on TTS provider
9
- - Edge-TTS: `en-US-JennyNeural`, `en-US-EricNeural`
10
- - XunFei: `catherine`, `henry`
11
- - **CLI import paths**: Fixed broken imports in `state_manager.py` CLI module
12
-
13
- ### Changed
14
- - **npm package**: Removed automatic `pip install` from postinstall script
15
- - Added `eng-lang-tutor-setup` CLI tool for manual dependency installation
16
- - Supports `--venv`, `--user`, and `--check` options
17
- - **Test coverage**: Added 38 new tests covering:
18
- - `scripts/audio/utils.py` (6 tests)
19
- - `scripts/audio/converter.py` (16 tests)
20
- - `scripts/utils/helpers.py` (16 tests)
21
- - **Test fixes**: Fixed pre-existing test failures in CLI and TTS modules
22
-
23
- ## [1.2.3] - 2026-02-27
24
-
25
- ### Changed
26
- - Version bump for npm publishing
27
-
28
- ## [1.2.0] - 2026-02-27
29
-
30
- ### Changed
31
- - Initial npm package setup
32
-
33
- ## [1.0.1] - 2025-02-27
34
-
35
- ### Fixed
36
- - **TTS voice selection**: Audio composer now automatically selects appropriate voices based on TTS provider
37
- - Edge-TTS: `en-US-JennyNeural`, `en-US-EricNeural`
38
- - XunFei: `catherine`, `henry`
39
- - **CLI import paths**: Fixed broken imports in `state_manager.py` CLI module
40
-
41
- ### Changed
42
- - **npm package**: Removed automatic `pip install` from postinstall script
43
- - Added `eng-lang-tutor-setup` CLI tool for manual dependency installation
44
- - Supports `--venv`, `--user`, and `--check` options
45
- - **Test coverage**: Added 38 new tests covering:
46
- - `scripts/audio/utils.py` (6 tests)
47
- - `scripts/audio/converter.py` (16 tests)
48
- - `scripts/utils/helpers.py` (16 tests)
49
- - **Test fixes**: Fixed pre-existing test failures in CLI and TTS modules
50
-
51
- ## [1.0.1] - 2025-02-27
52
-
53
- ### Changed
54
- - Version bump for npm publishing (1.0.0 was previously unpublished)
55
-
56
5
  ## [1.0.0] - 2025-02-26
57
6
 
58
7
  Initial release.
package/README.md CHANGED
@@ -84,6 +84,21 @@ openclaw skills list
84
84
  openclaw skills info eng-lang-tutor
85
85
  ```
86
86
 
87
+ ### 卸载
88
+
89
+ ```bash
90
+ # 卸载 npm 包并清除所有数据(包括状态和媒体文件)
91
+ npm uninstall -g @rookiestar/eng-lang-tutor && rm -rf ~/.openclaw/state/eng-lang-tutor ~/.openclaw/media/eng-lang-tutor
92
+ ```
93
+
94
+ **如需保留学习数据,仅卸载软件:**
95
+
96
+ ```bash
97
+ npm uninstall -g @rookiestar/eng-lang-tutor
98
+ ```
99
+
100
+ > 注意:`~/.openclaw/state/eng-lang-tutor/` 存储学习进度、XP、连胜等数据;`~/.openclaw/media/eng-lang-tutor/` 存储生成的音频文件。
101
+
87
102
  **重启 Gateway:**
88
103
 
89
104
  ```bash
@@ -180,6 +195,59 @@ export XUNFEI_API_SECRET=xxx
180
195
  | 快速 | 1.3 | 听力挑战 |
181
196
  | 非常快 | 1.7 | 进阶训练 |
182
197
 
198
+ ## 🛠️ 飞书语音气泡魔改指南
199
+
200
+ > 通过修改 OpenClaw 飞书插件,可使 `.opus` 音频文件以语音气泡形式展示,提升用户体验。
201
+
202
+ **文件位置:** `/home/linuxbrew/.linuxbrew/lib/node_modules/openclaw/extensions/feishu/src/media.ts`
203
+
204
+ ### 📍 第一处修改:扩充类型定义 (约第 276 行)
205
+
206
+ ```typescript
207
+ // 【修改前】
208
+ msgType?: "file" | "media";
209
+
210
+ // 【修改后】
211
+ msgType?: "file" | "media" | "audio";
212
+ ```
213
+
214
+ ### 📍 第二处修改:重构路由逻辑 (约第 375 行)
215
+
216
+ ```typescript
217
+ // 【修改前】
218
+ // Feishu requires msg_type "media" for audio/video, "file" for documents
219
+ const isMedia = fileType === "mp4" || fileType === "opus";
220
+ return sendFileFeishu({
221
+ cfg,
222
+ to,
223
+ fileKey,
224
+ msgType: isMedia ? "media" : "file",
225
+ replyToMessageId,
226
+ accountId,
227
+ });
228
+
229
+ // 【修改后】
230
+ // 精细化路由:mp4 走 media (视频), opus 走 audio (语音气泡), 其余走 file
231
+ let msgType: "file" | "media" | "audio" = "file";
232
+ if (fileType === "mp4") {
233
+ msgType = "media";
234
+ } else if (fileType === "opus") {
235
+ msgType = "audio";
236
+ }
237
+ return sendFileFeishu({
238
+ cfg,
239
+ to,
240
+ fileKey,
241
+ msgType,
242
+ replyToMessageId,
243
+ accountId,
244
+ });
245
+ ```
246
+
247
+ **效果:**
248
+ - `.opus` 文件 → 语音气泡形式播放
249
+ - 其他文件 → 普通文件附件形式
250
+
183
251
  ## 命令列表
184
252
 
185
253
  | 命令 | 别名 | 描述 |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_meta": {
3
- "prompt_version": "quiz_gen_v1.2"
3
+ "prompt_version": "quiz_gen_v1.3"
4
4
  },
5
5
  "quiz_date": "2026-02-25",
6
6
  "cefr_level": "A1",
@@ -31,7 +31,7 @@
31
31
  "⬜ C. Goodbye",
32
32
  "⬜ D. Sorry"
33
33
  ],
34
- "hint": "💡 Think: what do you say when you get something?",
34
+ "hint": "💡 What's the polite thing to say?",
35
35
  "correct_feedback": "✅ Correct! **'Thank you'** = 谢谢!",
36
36
  "wrong_feedback": "❌ Not quite. When someone helps you, say **'Thank you'**!",
37
37
  "xp_display": "💎 +10 XP"
@@ -50,7 +50,7 @@
50
50
  "type_name": "Chinglish 修正 | Fix the Chinglish",
51
51
  "question_formatted": "🔧 What's wrong with this sentence?",
52
52
  "sentence_formatted": "📝 \"Thank you very **many** for your help!\"",
53
- "hint": "💡 'Many' or 'Much' - which one for degree?",
53
+ "hint": "💡 Think about which word describes amount or degree",
54
54
  "correct_feedback": "✅ Fixed! **'Thank you very much!'** sounds natural! 🎉",
55
55
  "wrong_feedback": "❌ Use **'much'** not 'many' here. **'Thank you very much!'**",
56
56
  "xp_display": "💎 +15 XP"
@@ -71,7 +71,7 @@
71
71
  "question_formatted": "✏️ What do you say?",
72
72
  "context_formatted": "🚪 Your friend opens the door for you. You say: '**___**!'",
73
73
  "word_bank_formatted": "📦 Options: [ **Thanks** | Hello | Goodbye ]",
74
- "hint": "💡 Which one means '谢谢'?",
74
+ "hint": "💡 Which one is for showing appreciation?",
75
75
  "correct_feedback": "✅ Perfect! **'Thanks!'** is casual and friendly! 🎯",
76
76
  "wrong_feedback": "❌ The answer was **'Thanks!'** - it means 谢谢!",
77
77
  "xp_display": "💎 +12 XP"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_meta": {
3
- "prompt_version": "quiz_gen_v1.2"
3
+ "prompt_version": "quiz_gen_v1.3"
4
4
  },
5
5
  "quiz_date": "2026-02-25",
6
6
  "cefr_level": "A2",
@@ -31,7 +31,7 @@
31
31
  "⬜ C. When you say goodbye",
32
32
  "⬜ D. When you ask for help"
33
33
  ],
34
- "hint": "💡 'Meet' = first time introduction",
34
+ "hint": "💡 Think about when you use this phrase in introductions",
35
35
  "correct_feedback": "✅ Correct! **'Nice to meet you'** = first time only!",
36
36
  "wrong_feedback": "❌ **'Nice to meet you'** is for first-time introductions only!",
37
37
  "xp_display": "💎 +10 XP"
@@ -50,7 +50,7 @@
50
50
  "type_name": "Chinglish 修正 | Fix the Chinglish",
51
51
  "question_formatted": "🔧 Find and fix the error:",
52
52
  "sentence_formatted": "📝 \"Hi John! **Nice to see you.** This is our first meeting.\"",
53
- "hint": "💡 If it's the FIRST meeting, 'see' or 'meet'?",
53
+ "hint": "💡 Think about the difference between first-time and repeat meetings",
54
54
  "correct_feedback": "✅ Fixed! **'Nice to meet you!'** for first introductions! 🎉",
55
55
  "wrong_feedback": "❌ First time = **'Nice to meet you'**, not 'see you'!",
56
56
  "xp_display": "💎 +15 XP"
@@ -71,7 +71,7 @@
71
71
  "question_formatted": "✏️ Complete the response:",
72
72
  "context_formatted": "🎉 Friend: \"This is Sarah.\" You: \"**___!**\"",
73
73
  "word_bank_formatted": "📦 Options: [ **Nice to meet you** | Nice to see you | Good to know you ]",
74
- "hint": "💡 First introduction = 'meet' or 'see'?",
74
+ "hint": "💡 Consider the context - is this person new to you?",
75
75
  "correct_feedback": "✅ Perfect! **'Nice to meet you!'** is the standard introduction! 🎯",
76
76
  "wrong_feedback": "❌ For first introductions, say **'Nice to meet you!'**",
77
77
  "xp_display": "💎 +12 XP"
@@ -28,6 +28,7 @@
28
28
  "⬜ C. They want to cancel the project",
29
29
  "⬜ D. They want you to write the report"
30
30
  ],
31
+ "hint": "💡 Think about what kind of conversation this implies",
31
32
  "correct_feedback": "✅ Correct! **'Touch base'** = quick check-in ⚡",
32
33
  "wrong_feedback": "❌ Not quite. **'Touch base'** means a brief, informal chat!",
33
34
  "key_phrase": "**touch base**",
@@ -70,6 +71,7 @@
70
71
  "question_formatted": "✏️ Complete the sentence:",
71
72
  "context_formatted": "💼 To your manager: \"I wanted to **___** on the project timeline.\"",
72
73
  "word_bank_formatted": "📦 Word Bank: [ **touch base** | 'discuss together' ❌ | 'communicate' ❌ ]",
74
+ "hint": "💡 Which sounds natural for a quick check-in with your manager?",
73
75
  "correct_feedback": "✅ Perfect! **'Touch base'** is the way to go! 🎯",
74
76
  "wrong_feedback": "❌ 'Discuss together' sounds awkward. Try **'touch base'**!",
75
77
  "key_phrase": "**touch base**",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_meta": {
3
- "prompt_version": "quiz_gen_v1.2"
3
+ "prompt_version": "quiz_gen_v1.3"
4
4
  },
5
5
  "quiz_date": "2026-02-25",
6
6
  "cefr_level": "B2",
@@ -50,7 +50,7 @@
50
50
  "type_name": "Chinglish 修正 | Fix the Chinglish",
51
51
  "question_formatted": "🔧 Make this sound more corporate:",
52
52
  "sentence_formatted": "📝 \"We need to **discuss this topic again** after the meeting.\"",
53
- "hint": "💡 Use a spatial metaphor that implies returning",
53
+ "hint": "💡 Which sounds most natural in American corporate culture?",
54
54
  "correct_feedback": "✅ Much better! **'Let's circle back on this'** sounds professional! 🎉",
55
55
  "wrong_feedback": "❌ Try **'Let's circle back on this'** instead of 'discuss again'",
56
56
  "xp_display": "💎 +15 XP"
@@ -71,7 +71,7 @@
71
71
  "question_formatted": "✏️ Complete the sentence:",
72
72
  "context_formatted": "📧 \"I'll **___** on the project updates.\"",
73
73
  "word_bank_formatted": "📦 Options: [ **loop you in** | circle back | touch base | reach out ]",
74
- "hint": "💡 Include someone = ?",
74
+ "hint": "💡 Think about the relationship - you're keeping them informed",
75
75
  "correct_feedback": "✅ Perfect! **'Loop you in'** means include in communication! 🎯",
76
76
  "wrong_feedback": "❌ **'Loop you in'** is for including someone in future communication!",
77
77
  "xp_display": "💎 +12 XP"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_meta": {
3
- "prompt_version": "quiz_gen_v1.2"
3
+ "prompt_version": "quiz_gen_v1.3"
4
4
  },
5
5
  "quiz_date": "2026-02-25",
6
6
  "cefr_level": "C1",
@@ -71,7 +71,7 @@
71
71
  "question_formatted": "✏️ Complete with the right idiom:",
72
72
  "context_formatted": "🔄 30 minutes, no progress. \"We're **___** here.\"",
73
73
  "word_bank_formatted": "📦 Options: [ **spinning our wheels** | boiling the ocean | touching base | circling back ]",
74
- "hint": "💡 Car metaphor = going nowhere",
74
+ "hint": "💡 Think about which idiom describes lack of progress",
75
75
  "correct_feedback": "✅ Perfect! **'Spinning our wheels'** = unproductive! 🎯",
76
76
  "wrong_feedback": "❌ **'Spinning our wheels'** describes unproductive discussion!",
77
77
  "xp_display": "💎 +12 XP"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_meta": {
3
- "prompt_version": "quiz_gen_v1.2"
3
+ "prompt_version": "quiz_gen_v1.3"
4
4
  },
5
5
  "quiz_date": "2026-02-25",
6
6
  "cefr_level": "C2",
@@ -50,7 +50,7 @@
50
50
  "type_name": "Chinglish 修正 | Fix the Chinglish",
51
51
  "question_formatted": "🔧 Make this sound more C-suite:",
52
52
  "sentence_formatted": "📝 \"That's **too ambitious**. We need to **reduce the scope**.\"",
53
- "hint": "💡 Use the ocean metaphor and 'phased'",
53
+ "hint": "💡 What's the C-suite way to say 'too ambitious'?",
54
54
  "correct_feedback": "✅ Perfect! **'Boiling the ocean' + 'phased approach'** = executive language! 🎉",
55
55
  "wrong_feedback": "❌ Try **'That's boiling the ocean. We need a phased approach.'**",
56
56
  "xp_display": "💎 +15 XP"
@@ -81,7 +81,7 @@
81
81
  "⬜ C. Let's keep it confidential",
82
82
  "⬜ D. We should decide now"
83
83
  ],
84
- "hint": "💡 Get stakeholder buy-in before deciding = ?",
84
+ "hint": "💡 What's the corporate process for building consensus?",
85
85
  "correct_feedback": "✅ Perfect! **'Socialize this'** = systematic stakeholder buy-in! 🎯",
86
86
  "wrong_feedback": "❌ **'Socialize this with stakeholders'** is the C-suite process!",
87
87
  "xp_display": "💎 +15 XP"
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@rookiestar/eng-lang-tutor",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "地道美式英语导师 - OpenClaw Skill for learning authentic American English",
5
5
  "main": "scripts/cli/cli.py",
6
- "bin": {
7
- "eng-lang-tutor-setup": "scripts/setup.py"
6
+ "scripts": {
7
+ "postinstall": "python3 -m pip install -r requirements.txt --quiet"
8
8
  },
9
9
  "keywords": [
10
10
  "english",
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Release Script - Manages file visibility across local repo, remote repo, and npm package.
4
+
5
+ This script handles three layers:
6
+ 1. Local repo - All tracked files (managed by .gitignore)
7
+ 2. Remote repo - Public files only
8
+ 3. npm package - Minimal user-facing files
9
+
10
+ Usage:
11
+ python3 scripts/release.py --check # Show file distribution
12
+ python3 scripts/release.py --sync-remote # Sync to remote (dry-run)
13
+ python3 scripts/release.py --npm-pack # Create npm tarball
14
+ """
15
+
16
+ import argparse
17
+ import subprocess
18
+ import json
19
+ from pathlib import Path
20
+ from typing import Set
21
+
22
+ # Files/directories to EXCLUDE from remote repo (not publicly visible)
23
+ REMOTE_EXCLUDE = {
24
+ "tests/",
25
+ "package.json",
26
+ "package-lock.json",
27
+ "bin/",
28
+ "scripts/setup.py",
29
+ ".gitignore",
30
+ ".gitattributes",
31
+ ".npmignore",
32
+ "*.egg-info/",
33
+ ".pytest_cache/",
34
+ "__pycache__/",
35
+ ".coverage",
36
+ "htmlcov/",
37
+ ".tox/",
38
+ ".nox/",
39
+ ".claude/",
40
+ "*.pyc",
41
+ "*.pyo",
42
+ "*.tgz",
43
+ }
44
+
45
+ # Files/directories to EXCLUDE from npm package (in addition to REMOTE_EXCLUDE)
46
+ # npm package should be minimal - only what users need to run
47
+ NPM_EXCLUDE = REMOTE_EXCLUDE | {
48
+ "CLAUDE.md", # Development guide for Claude AI
49
+ "docs/", # Internal documentation
50
+ "examples/", # Sample files (not needed for runtime)
51
+ "references/", # Reference materials
52
+ "*.md", # All markdown except README.md
53
+ }
54
+
55
+ # Files that MUST be included in npm package
56
+ NPM_INCLUDE = {
57
+ "README.md", # Keep README for npm display
58
+ "package.json", # Required for npm
59
+ "requirements.txt", # Python dependencies
60
+ "scripts/", # All scripts (excluding setup.py)
61
+ "templates/", # Prompt templates
62
+ "SKILL.md", # Skill documentation
63
+ }
64
+
65
+
66
+ def get_tracked_files() -> Set[str]:
67
+ """Get all files tracked by git."""
68
+ result = subprocess.run(
69
+ ["git", "ls-files"],
70
+ capture_output=True,
71
+ text=True,
72
+ cwd=Path(__file__).parent.parent
73
+ )
74
+ return set(result.stdout.strip().split("\n"))
75
+
76
+
77
+ def should_exclude(path: str, exclude_patterns: Set[str]) -> bool:
78
+ """Check if a path should be excluded based on patterns."""
79
+ for pattern in exclude_patterns:
80
+ if pattern.endswith("/"):
81
+ if path.startswith(pattern) or "/" + pattern in path:
82
+ return True
83
+ elif pattern.startswith("*."):
84
+ if path.endswith(pattern[1:]):
85
+ return True
86
+ else:
87
+ if path == pattern or path.startswith(pattern + "/"):
88
+ return True
89
+ return False
90
+
91
+
92
+ def categorize_files():
93
+ """Categorize files into local, remote, and npm groups."""
94
+ tracked = get_tracked_files()
95
+
96
+ local_only = set()
97
+ remote_files = set()
98
+ npm_files = set()
99
+
100
+ for file in tracked:
101
+ # Skip empty strings
102
+ if not file:
103
+ continue
104
+
105
+ # Check if in remote exclude list
106
+ if should_exclude(file, REMOTE_EXCLUDE):
107
+ local_only.add(file)
108
+ else:
109
+ remote_files.add(file)
110
+
111
+ # Check if should be in npm package
112
+ if should_exclude(file, NPM_EXCLUDE):
113
+ # Check if explicitly included
114
+ included = False
115
+ for inc_pattern in NPM_INCLUDE:
116
+ if inc_pattern.endswith("/"):
117
+ if file.startswith(inc_pattern):
118
+ included = True
119
+ break
120
+ elif file == inc_pattern:
121
+ included = True
122
+ break
123
+
124
+ if included:
125
+ npm_files.add(file)
126
+ else:
127
+ npm_files.add(file)
128
+
129
+ return local_only, remote_files, npm_files
130
+
131
+
132
+ def show_distribution():
133
+ """Show file distribution across three layers."""
134
+ local_only, remote_files, npm_files = categorize_files()
135
+
136
+ print("=" * 60)
137
+ print("FILE DISTRIBUTION ACROSS THREE LAYERS")
138
+ print("=" * 60)
139
+
140
+ print(f"\n📦 LOCAL REPO ONLY ({len(local_only)} files)")
141
+ print("-" * 40)
142
+ for f in sorted(local_only):
143
+ print(f" {f}")
144
+
145
+ print(f"\n🌐 REMOTE REPO (public, {len(remote_files)} files)")
146
+ print("-" * 40)
147
+ for f in sorted(remote_files):
148
+ print(f" {f}")
149
+
150
+ print(f"\n📦 NPM PACKAGE ({len(npm_files)} files)")
151
+ print("-" * 40)
152
+ for f in sorted(npm_files):
153
+ print(f" {f}")
154
+
155
+ print("\n" + "=" * 60)
156
+ print(f"SUMMARY: Local={len(local_only) + len(remote_files)} | Remote={len(remote_files)} | npm={len(npm_files)}")
157
+ print("=" * 60)
158
+
159
+
160
+ def create_gitattributes():
161
+ """Create .gitattributes with export-ignore for local-only files."""
162
+ eng_lang_tutor_dir = Path(__file__).parent.parent
163
+ gitattributes_path = eng_lang_tutor_dir / ".gitattributes"
164
+
165
+ # Patterns to exclude from git archive (remote export)
166
+ export_ignore_patterns = [
167
+ "tests/",
168
+ ".pytest_cache/",
169
+ "*.pyc",
170
+ "*.pyo",
171
+ ".coverage",
172
+ "htmlcov/",
173
+ ".tox/",
174
+ ".nox/",
175
+ ".claude/",
176
+ "*.egg-info/",
177
+ "*.tgz",
178
+ ]
179
+
180
+ content = "# Files to exclude from git archive export\n"
181
+ content += "# These files are tracked locally but not exported to remote archives\n\n"
182
+
183
+ for pattern in export_ignore_patterns:
184
+ content += f"{pattern} export-ignore\n"
185
+
186
+ gitattributes_path.write_text(content)
187
+ print(f"✓ Created {gitattributes_path}")
188
+ print("\nNote: This affects 'git archive' only.")
189
+ print("For selective push to remote, consider using a separate public branch.")
190
+
191
+
192
+ def npm_pack():
193
+ """Create npm tarball with proper file filtering."""
194
+ eng_lang_tutor_dir = Path(__file__).parent.parent
195
+
196
+ # Update package.json files field for npm
197
+ package_json_path = eng_lang_tutor_dir / "package.json"
198
+
199
+ with open(package_json_path) as f:
200
+ package = json.load(f)
201
+
202
+ # Set files field to include only what we want
203
+ package["files"] = [
204
+ "scripts/",
205
+ "templates/",
206
+ "SKILL.md",
207
+ "README.md",
208
+ "requirements.txt",
209
+ ]
210
+
211
+ # Remove tests from npm package
212
+ if "!tests/**" in package.get("files", []):
213
+ pass # Already excluded
214
+
215
+ print("npm pack would include:")
216
+ print(json.dumps(package.get("files", []), indent=2))
217
+
218
+ print("\nRun the following to create npm package:")
219
+ print(f" cd {eng_lang_tutor_dir} && npm pack")
220
+
221
+
222
+ def main():
223
+ parser = argparse.ArgumentParser(
224
+ description="Manage file visibility across local repo, remote repo, and npm package"
225
+ )
226
+ parser.add_argument(
227
+ "--check",
228
+ action="store_true",
229
+ help="Show file distribution across three layers"
230
+ )
231
+ parser.add_argument(
232
+ "--create-gitattributes",
233
+ action="store_true",
234
+ help="Create .gitattributes with export-ignore rules"
235
+ )
236
+ parser.add_argument(
237
+ "--npm-pack",
238
+ action="store_true",
239
+ help="Show npm pack configuration"
240
+ )
241
+
242
+ args = parser.parse_args()
243
+
244
+ if args.check:
245
+ show_distribution()
246
+ elif args.create_gitattributes:
247
+ create_gitattributes()
248
+ elif args.npm_pack:
249
+ npm_pack()
250
+ else:
251
+ parser.print_help()
252
+
253
+
254
+ if __name__ == "__main__":
255
+ main()
@@ -19,18 +19,147 @@ Based on today's knowledge point, generate a 3-question quiz calibrated to user'
19
19
  ## KNOWLEDGE POINT
20
20
  {keypoint_json}
21
21
 
22
- ## CEFR DIFFICULTY CALIBRATION
22
+ ## CEFR DIFFICULTY CALIBRATION - 6 LEVELS
23
23
 
24
- | Level | Question Characteristics |
25
- |-------|-------------------------|
26
- | A1-A2 | Simple recognition, obvious distractors, direct context matching |
27
- | B1-B2 | Nuanced usage, subtle distractors, requires understanding context |
28
- | C1-C2 | Complex scenarios, idiomatic variations, cultural nuance testing |
24
+ ### A1 - Beginner 入门级
29
25
 
30
- **For B1-B2 (most users):**
31
- - Multiple choice: Distractors should be grammatically correct but contextually wrong
32
- - Chinglish fix: Use subtle errors (preposition, article, word choice) not obvious mistakes
33
- - Create NEW scenarios that differ from the keypoint examples
26
+ **Multiple Choice:**
27
+ - 4 选项,1 个明显错误(语法/拼写)
28
+ - 其他 3 个语法正确但语义不符
29
+ - 场景简单日常:购物、问路、问候
30
+
31
+ **Chinglish Fix:**
32
+ - 错误类型:基本语法、词序、明显直译
33
+ - 示例错误:"I very like it" → "I really like it"
34
+ - 示例错误:"She go to school yesterday" → "She went"
35
+
36
+ **Fill Blank:**
37
+ - 3 选 1 词库
38
+ - 直接匹配,无需理解语境
39
+
40
+ ---
41
+
42
+ ### A2 - Elementary 初级
43
+
44
+ **Multiple Choice:**
45
+ - 4 选项全部语法正确
46
+ - 干扰项语义差异明显
47
+ - 场景:日常生活、简单工作
48
+
49
+ **Chinglish Fix:**
50
+ - 错误类型:常见直译、介词错误
51
+ - 示例错误:"I agree your opinion" → "I agree with your opinion"
52
+ - 示例错误:"Discuss about this" → "Discuss this"
53
+
54
+ **Fill Blank:**
55
+ - 3 选 1 词库
56
+ - 需要简单语境理解
57
+
58
+ ---
59
+
60
+ ### B1 - Intermediate 中级
61
+
62
+ **Multiple Choice:**
63
+ - 4 选项全部语法正确
64
+ - 干扰项在**其他语境**下可用
65
+ - 包含 1 个常见学习者错误作为陷阱
66
+ - 场景:工作沟通、旅行、社交
67
+
68
+ **Example - B1 MC:**
69
+ Context: "You want to end a phone call politely."
70
+ Options:
71
+ A. "I'll let you go now" ← Correct
72
+ B. "I'll hang up now" ← 太直接
73
+ C. "Let's stop talking" ← 不礼貌
74
+ D. "I'm going now" ← 语境不符
75
+
76
+ **Chinglish Fix:**
77
+ - 错误类型:介词搭配、冠词、固定搭配
78
+ - 示例错误:"Make a decision for" → "Make a decision about"
79
+
80
+ **Fill Blank:**
81
+ - 3-4 选 1 词库
82
+ - 需要理解表达用法
83
+
84
+ ---
85
+
86
+ ### B2 - Upper-Intermediate 中高级 ⚠️ 常见级别
87
+
88
+ **Multiple Choice:**
89
+ - 4 选项全部语法正确且听起来自然
90
+ - 干扰项必须是 **plausible in OTHER contexts**
91
+ - 至少 1 个"高级陷阱":常见学习者错误
92
+ - 场景:商务会议、专业沟通
93
+
94
+ **Example - B2 MC:**
95
+ Context: "In a team meeting, you want to return to a topic later."
96
+ Options:
97
+ A. "Let's circle back on this" ← Correct
98
+ B. "Let's follow up on this" ← Plausible (different meaning - action vs discussion)
99
+ C. "We should revisit this later" ← Plausible (more formal, less idiomatic)
100
+ D. "Let's discuss this again" ← Trap (grammatically correct but not idiomatic)
101
+
102
+ **Chinglish Fix:**
103
+ - 错误类型:细微介词错误、语体不当、搭配错误
104
+ - 示例错误:"We need to discuss about the project"
105
+ - 示例错误:"I am interesting in this topic" (interested)
106
+
107
+ **Fill Blank:**
108
+ - 4 选 1 词库
109
+ - 需要理解细微语义差别
110
+
111
+ ---
112
+
113
+ ### C1 - Advanced 高级
114
+
115
+ **Multiple Choice:**
116
+ - 所有选项对非母语者都听起来自然
117
+ - 需要文化知识或习语理解
118
+ - 可包含 1-2 个英式/美式差异陷阱
119
+ - 场景:复杂商务、学术讨论
120
+
121
+ **Example - C1 MC:**
122
+ Context: "You want to politely decline a request without saying no directly."
123
+ Options:
124
+ A. "That might be challenging to fit in" ← Correct (indirect)
125
+ B. "I'm afraid I can't" ← Too direct
126
+ C. "Let me think about it" ← Implies maybe, not decline
127
+ D. "I'll have to pass on this" ← Correct but more casual
128
+
129
+ **Chinglish Fix:**
130
+ - 错误类型:文化得体性、语域错误、隐含意义
131
+ - 示例错误:"Please kindly check" (过度礼貌)
132
+ - 示例错误:在非正式场合使用过于正式的表达
133
+
134
+ **Fill Blank / Dialogue Completion:**
135
+ - 4 选 1,需要文化理解
136
+ - 多个答案可能都"可以",但只有一个"最地道"
137
+
138
+ ---
139
+
140
+ ### C2 - Proficiency 精通级
141
+
142
+ **Multiple Choice:**
143
+ - 所有选项都符合语法且自然
144
+ - 区别在于**细微语气差异**或**文化隐含意义**
145
+ - 场景:专业演讲、文化敏感话题
146
+
147
+ **Example - C2 MC:**
148
+ Context: "Giving feedback to a senior colleague on their presentation."
149
+ Options:
150
+ A. "I had a thought on slide 3" ← Correct (hedges, collaborative)
151
+ B. "You should change slide 3" ← Too direct
152
+ C. "Slide 3 could be better" ← Vague
153
+ D. "I think slide 3 is wrong" ← Too confrontational
154
+
155
+ **Chinglish Fix:**
156
+ - 错误类型:深层文化差异、语用失误
157
+ - 示例错误:不理解美式间接沟通文化
158
+ - 示例错误:在需要 hedge 的情况下过于直接
159
+
160
+ **Dialogue Completion:**
161
+ - 开放式,需要综合理解
162
+ - 可能没有"唯一正确答案",而是"最符合语境"
34
163
 
35
164
  ## QUESTION TYPE REQUIREMENTS (3 questions total)
36
165
  1. **multiple_choice** (required): Test expression recognition - 10 XP
@@ -73,6 +202,36 @@ Based on today's knowledge point, generate a 3-question quiz calibrated to user'
73
202
  - Hint format: "💡 Think about what Americans say in this situation"
74
203
  - FORBIDDEN: "💡 The answer is 'touch base'" or showing the phrase directly
75
204
 
205
+ ## 💡 HINT DESIGN PRINCIPLES
206
+
207
+ ### Core Rule: Hints guide thinking direction, NEVER reveal answers
208
+
209
+ **SAFE Hint Patterns** (通用模板,可复用):
210
+ - "💡 Think about the formality level of this situation"
211
+ - "💡 Consider who you're talking to - friend or boss?"
212
+ - "💡 What would sound natural in American English?"
213
+ - "💡 Is this formal or casual context?"
214
+ - "💡 Think about the relationship between the speakers"
215
+ - "💡 Consider the tone - professional or friendly?"
216
+ - "💡 What's appropriate for a workplace setting?"
217
+
218
+ **FORBIDDEN Hint Patterns**:
219
+ - ❌ "💡 Use a spatial metaphor" (太具体,直接指向特定答案)
220
+ - ❌ "💡 The phrase starts with 't'" (字母提示)
221
+ - ❌ "💡 Include someone = ?" (等同告诉答案)
222
+ - ❌ "💡 'A' or 'B' - which one?" (二选一)
223
+ - ❌ "💡 Think about [specific word/concept from answer]" (指向答案)
224
+ - ❌ 直接用中文翻译作为提示
225
+
226
+ **Validation Test**:
227
+ 如果 hint 只适用于 ONE possible answer,则太具体。
228
+ Good hint 应该对 2-3 个选项都"听起来合理"。
229
+
230
+ **Bad Hint Example:**
231
+ - Question: "Let's ___ on this later." (Answer: circle back)
232
+ - Bad hint: "💡 Think about a shape" ← 太具体,直接指向 circle
233
+ - Good hint: "💡 What's the idiomatic way to say 'return to a topic'?"
234
+
76
235
  3. **NEVER make distractors obviously wrong**
77
236
  - All 4 options should sound plausible
78
237
  - For B2+: At least one distractor should be a common learner error
@@ -85,6 +244,28 @@ Based on today's knowledge point, generate a 3-question quiz calibrated to user'
85
244
  3. Include encouraging feedback in display fields
86
245
  4. Total XP should be around 35-40
87
246
  5. **Use NEW contexts** - never copy-paste from keypoint examples
247
+
248
+ ## CONTEXT VARIATION RULES
249
+
250
+ ### Multiple Choice - 场景类型(轮换使用)
251
+ 1. **Email**: "You're writing to a client..."
252
+ 2. **Meeting**: "In a team meeting..."
253
+ 3. **Casual chat**: "Talking to a coworker at lunch..."
254
+ 4. **Phone/Video call**: "On a call with your manager..."
255
+ 5. **Presentation**: "During your presentation..."
256
+ 6. **Networking**: "At a professional event..."
257
+
258
+ ### Chinglish Fix - 错误类型分布(按级别)
259
+ | Level | 主导错误类型 | 占比 |
260
+ |-------|-------------|------|
261
+ | A1-A2 | 基本语法、词序、直译 | 80% |
262
+ | B1-B2 | 介词、冠词、搭配 | 60% |
263
+ | C1-C2 | 语域、文化、语用 | 50% |
264
+
265
+ ### Fill Blank - 语境深度
266
+ - A1-A2: 单句,明确场景
267
+ - B1-B2: 对话语境,需理解关系
268
+ - C1-C2: 复杂语境,可能有多重解读
88
269
  ```
89
270
 
90
271
  ---
@@ -96,7 +277,7 @@ Based on today's knowledge point, generate a 3-question quiz calibrated to user'
96
277
  ```json
97
278
  {
98
279
  "_meta": {
99
- "prompt_version": "quiz_gen_v1.2"
280
+ "prompt_version": "quiz_gen_v1.3"
100
281
  },
101
282
  "quiz_date": "{today_date}",
102
283
  "keypoint_fingerprint": "{fingerprint}",
@@ -1,22 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(python3 -m pytest:*)",
5
- "Bash(pytest:*)",
6
- "Bash(find:*)",
7
- "Bash(git add:*)",
8
- "Bash(git commit:*)",
9
- "Bash(git push)",
10
- "mcp__brave-search__brave_web_search",
11
- "mcp__web-search-prime__webSearchPrime",
12
- "mcp__zread__get_repo_structure",
13
- "mcp__zread__read_file",
14
- "mcp__zread__search_doc",
15
- "mcp__web-reader__webReader",
16
- "WebSearch",
17
- "mcp__fetch__fetch",
18
- "Bash(wc:*)",
19
- "Bash(python3 -m py_compile:*)"
20
- ]
21
- }
22
- }
@@ -1,47 +0,0 @@
1
- {
2
- "version": 2,
3
- "initialized": false,
4
- "onboarding_step": 0,
5
- "completion_status": {
6
- "quiz_completed_date": null,
7
- "keypoint_view_history": []
8
- },
9
- "schedule": {
10
- "keypoint_time": "06:45",
11
- "quiz_time": "22:45",
12
- "timezone": "Asia/Shanghai"
13
- },
14
- "user": {
15
- "xp": 0,
16
- "level": 1,
17
- "streak": 0,
18
- "streak_freeze": 0,
19
- "gems": 0,
20
- "badges": []
21
- },
22
- "preferences": {
23
- "cefr_level": "B1",
24
- "oral_written_ratio": 0.7,
25
- "topics": {
26
- "movies": 0.2,
27
- "news": 0.15,
28
- "gaming": 0.15,
29
- "sports": 0.1,
30
- "workplace": 0.2,
31
- "social": 0.1,
32
- "daily_life": 0.1
33
- },
34
- "tutor_style": "humorous",
35
- "dedup_days": 14
36
- },
37
- "progress": {
38
- "total_quizzes": 0,
39
- "correct_rate": 0.0,
40
- "last_study_date": null,
41
- "perfect_quizzes": 0,
42
- "expressions_learned": 0
43
- },
44
- "recent_topics": [],
45
- "error_notebook": [],
46
- "error_archive": []
47
- }
@@ -1,47 +0,0 @@
1
- {
2
- "version": 2,
3
- "initialized": false,
4
- "onboarding_step": 0,
5
- "completion_status": {
6
- "quiz_completed_date": null,
7
- "keypoint_view_history": []
8
- },
9
- "schedule": {
10
- "keypoint_time": "06:45",
11
- "quiz_time": "22:45",
12
- "timezone": "Asia/Shanghai"
13
- },
14
- "user": {
15
- "xp": 0,
16
- "level": 1,
17
- "streak": 0,
18
- "streak_freeze": 0,
19
- "gems": 0,
20
- "badges": []
21
- },
22
- "preferences": {
23
- "cefr_level": "B1",
24
- "oral_written_ratio": 0.7,
25
- "topics": {
26
- "movies": 0.2,
27
- "news": 0.15,
28
- "gaming": 0.15,
29
- "sports": 0.1,
30
- "workplace": 0.2,
31
- "social": 0.1,
32
- "daily_life": 0.1
33
- },
34
- "tutor_style": "humorous",
35
- "dedup_days": 14
36
- },
37
- "progress": {
38
- "total_quizzes": 0,
39
- "correct_rate": 0.0,
40
- "last_study_date": null,
41
- "perfect_quizzes": 0,
42
- "expressions_learned": 0
43
- },
44
- "recent_topics": [],
45
- "error_notebook": [],
46
- "error_archive": []
47
- }
@@ -1,47 +0,0 @@
1
- {
2
- "version": 2,
3
- "initialized": false,
4
- "onboarding_step": 0,
5
- "completion_status": {
6
- "quiz_completed_date": null,
7
- "keypoint_view_history": []
8
- },
9
- "schedule": {
10
- "keypoint_time": "06:45",
11
- "quiz_time": "22:45",
12
- "timezone": "Asia/Shanghai"
13
- },
14
- "user": {
15
- "xp": 0,
16
- "level": 1,
17
- "streak": 0,
18
- "streak_freeze": 0,
19
- "gems": 0,
20
- "badges": []
21
- },
22
- "preferences": {
23
- "cefr_level": "B1",
24
- "oral_written_ratio": 0.7,
25
- "topics": {
26
- "movies": 0.2,
27
- "news": 0.15,
28
- "gaming": 0.15,
29
- "sports": 0.1,
30
- "workplace": 0.2,
31
- "social": 0.1,
32
- "daily_life": 0.1
33
- },
34
- "tutor_style": "humorous",
35
- "dedup_days": 14
36
- },
37
- "progress": {
38
- "total_quizzes": 0,
39
- "correct_rate": 0.0,
40
- "last_study_date": null,
41
- "perfect_quizzes": 0,
42
- "expressions_learned": 0
43
- },
44
- "recent_topics": [],
45
- "error_notebook": [],
46
- "error_archive": []
47
- }