Skill: smallkeyboy/mistake-assistant (version 1.0.0)

--- name: mistake-assistant description: 智能错题管理助手，支持错题记录、分类整理、复习提醒、统计分析和导出分享。使用场景：(1) 记录做错的题目（支持手动录入、OCR识别、截图上传）(2) 按科目/知识点/难度分类管理错题 (3) 智能复习提醒（艾宾浩斯遗忘曲线）(4) 分析薄弱知识点和错误趋势 (5) 导出错题本为Markdown/PDF/HTML格式。适用于学生备考、刷题复习、知识巩固等场景。 --- # 错题小助手 智能错题管理系统，帮助你高效记录、复习和攻克错题。 ## 核心功能 ### 1. 记录错题 **手动录入：** ``` 科目：数学 题目：求证 sin²x + cos²x = 1 错误答案：sin²x + cos²x = 0 正确答案：sin²x + cos²x = 1 错误原因：记错了公式 知识点：三角恒等式 难度：简单 ``` **快捷录入格式：** ``` 【科目】数学 【题目】求证 sin²x + cos²x = 1 【错答】sin²x + cos²x = 0 【正答】sin²x + cos²x = 1 【原因】记错公式 【知识点】三角恒等式 【难度】简单 ``` **支持题型：** - 选择题（单选/多选） - 填空题 - 解答题/计算题 - 简答题 - 判断题 **录入方式：** - 手动输入题目内容 - 粘贴题目文本 - 提供图片路径（自动调用OCR识别） ### 2. 分类整理 错题自动存储在 `mistake-data/` 目录下： ``` mistake-data/ ├── index.json # 错题索引（ID、科目、知识点映射） ├── math/ # 数学 │ ├── algebra.json # 代数 │ ├── geometry.json # 几何 │ └── trigonometry.json # 三角函数 ├── physics/ # 物理 │ ├── mechanics.json # 力学 │ └── electromagnetism.json # 电磁学 └── ... ``` **标签系统：** - 科目标签：数学、物理、化学、英语... - 知识点标签：二次函数、牛顿定律、氧化还原... - 难度标签：简单、中等、困难 - 状态标签：待复习、已掌握、需强化 ### 3. 复习提醒 **艾宾浩斯遗忘曲线复习计划：** | 复习次数 | 间隔时间 | 提醒方式 | |---------|---------|---------| | 第1次 | 1天后 | 消息提醒 | | 第2次 | 2天后 | 消息提醒 | | 第3次 | 4天后 | 消息提醒 | | 第4次 | 7天后 | 消息提醒 | | 第5次 | 15天后 | 消息提醒 | **复习流程：** 1. 查看今日待复习错题 2. 尝试重新作答 3. 标记掌握状态：已掌握 / 仍需强化 4. 更新复习计划 **查询今日复习任务：** ``` 今日复习 今天要复习什么 复习提醒 ``` ### 4. 统计分析 **个人学情报告：** ``` 【错题统计】 总错题数：128 待复习：45 已掌握：83 【薄弱知识点 TOP 5】 1. 二次函数（错误8次） 2. 牛顿第二定律（错误6次） 3. 定语从句（错误5次） 4. 氧化还原反应（错误4次） 5. 三角函数图像（错误4次） 【科目分布】 数学：52题（40.6%） 物理：38题（29.7%） 英语：25题（19.5%） 化学：13题（10.2%） 【近期趋势】 本周新增：12题 本周掌握：8题 掌握率：66.7% ``` **趋势分析：** - 按时间段统计（本周/本月/本学期） - 错误类型分布（概念错误/计算错误/审题错误） - 难度分布 - 进步曲线图 ### 5. 导出分享 **导出格式：** 1. **Markdown格式** - 适合笔记软件 2. **PDF格式** - 适合打印复习 3. **HTML格式** - 适合浏览器查看 4. **JSON格式** - 数据备份/迁移 **导出命令：** ``` 导出数学错题 导出所有错题为PDF 导出本周新增错题 按知识点导出：二次函数 ``` ## 使用流程 ### 添加错题 1. 提供题目信息（手动输入或粘贴） 2. 指定科目和知识点 3. 系统自动分类存储 4. 设置首次复习提醒 **示例对话：** ``` 用户：我有一道数学错题要记录 助手：好的，请提供题目内容。你可以： 1. 直接粘贴题目 2. 告诉我图片路径（我来OCR识别） 3. 按格式录入 用户：【题目】已知 f(x) = x² + 2x + 1，求 f(x) 的最小值 【错答】最小值是0 【正答】最小值是-1 【原因】忘记配方了 助手：已记录！这是一道二次函数题目，难度中等。 下次复习时间：明天 错题ID：math-2024-001 ``` ### 复习错题 ``` 用户：今日复习 助手：【今日待复习 - 5题】 1. [数学-二次函数] 求f(x)=x²+2x+1最小值 2. [物理-牛顿定律] 物体受力分析... 3. [英语-定语从句] which和that的区别... 开始复习第1题？ ``` ### 查看统计 ``` 用户：我的学情报告 助手：正在生成统计报告... [显示统计数据和图表] ``` ## 资源说明 ### scripts/ - `add_mistake.py` - 添加错题到数据库 - `review_today.py` - 获取今日待复习错题 - `stats.py` - 生成学情统计报告 - `export.py` - 导出错题本（支持多格式） - `ocr_image.py` - 图片OCR识别（需要Tesseract） ### references/ - `subjects.json` - 科目和知识点分类树 - `forgetting_curve.md` - 艾宾浩斯遗忘曲线算法说明 - `templates.md` - 错题录入模板 ### assets/ - `templates/export_template.md` - 导出Markdown模板 - `templates/report_template.html` - HTML报告模板 ## 快捷命令 | 命令 | 说明 | |-----|------| | 添加错题 | 开始录入新错题 | | 今日复习 | 查看今日待复习题目 | | 我的统计 | 查看学情报告 | | 导出错题 | 导出错题本 | | 查询[科目] | 查看某科目错题 | | 查询[知识点] | 查看某知识点相关错题 | | 标记掌握 [ID] | 标记错题为已掌握 | | 删除错题 [ID] | 删除指定错题 | ## 数据存储 所有错题数据存储在用户工作区的 `mistake-data/` 目录： - 支持跨设备同步（通过云盘） - JSON格式便于备份和迁移 - 自动创建数据索引

path: assets/templates/export_template.md
content: # 错题本导出模板

## Markdown 模板

```markdown
# 我的错题本

导出时间: 2024-01-15 20:30
总数: 128题

---

## 数学

### 1. [二次函数] 解答题

**难度**: 中等
**题目**: 已知 f(x) = x² + 2x + 1，求 f(x) 的最小值

**错误答案**: 最小值是 0
**正确答案**: 最小值是 -1，f(x) = (x+1)² - 1
**错误原因**: 忘记配方，直接代入顶点公式错误

**备注**: 记住配方方法：f(x) = a(x-h)² + k

**知识点**: 二次函数
**来源**: 课本第23页例题
**创建时间**: 2024-01-10

---

### 2. [牛顿定律] 选择题

**难度**: 简单
**题目**: 质量为2kg的物体，在水平面上受到10N的拉力，摩擦力为4N，求加速度

**选项**:
- A. 5 m/s²
- B. 3 m/s²
- C. 2 m/s²
- D. 7 m/s²

**错误答案**: A
**正确答案**: B
**错误原因**: 忘记考虑摩擦力

---

## 物理

...
```

## 复习卡片模板

```markdown
## 📝 复习卡片

**ID**: math-20240115-001
**科目**: 数学
**知识点**: 二次函数

### 题目
已知 f(x) = x² + 2x + 1，求 f(x) 的最小值

---

### 我的答案
（答题区域）

---

### 正确答案
最小值是 -1，f(x) = (x+1)² - 1

---

### 解析
配方：f(x) = x² + 2x + 1 = (x+1)²

### 错误反思
忘记配方方法

### 下次注意
记住配方步骤：
1. 提取二次项系数
2. 配完全平方
3. 化简为顶点式

---

**复习记录**:
- 2024-01-16: 仍需强化
- 2024-01-18: 已掌握 ✅
```

## 统计报告模板

```markdown
# 📊 学情报告

**统计周期**: 2024-01-01 ~ 2024-01-15
**生成时间**: 2024-01-15 20:30

---

## 总体情况

| 指标 | 数值 |
|-----|------|
| 总错题数 | 128 |
| 已掌握 | 45 (35.2%) |
| 待复习 | 83 (64.8%) |
| 本周新增 | 12 |
| 本周掌握 | 8 |

---

## 薄弱知识点 TOP 5

| 排名 | 知识点 | 错误次数 | 掌握率 |
|-----|--------|---------|-------|
| 1 | 二次函数 | 8 | 25% |
| 2 | 牛顿第二定律 | 6 | 33% |
| 3 | 定语从句 | 5 | 40% |
| 4 | 氧化还原反应 | 4 | 50% |
| 5 | 三角函数图像 | 4 | 25% |

---

## 科目分布

| 科目 | 错题数 | 占比 |
|-----|--------|------|
| 数学 | 52 | 40.6% |
| 物理 | 38 | 29.7% |
| 英语 | 25 | 19.5% |
| 化学 | 13 | 10.2% |

---

## 难度分布

- 困难: 15题 (11.7%)
- 中等: 78题 (60.9%)
- 简单: 35题 (27.3%)

---

## 错误原因分析

1. 概念不清: 45题 (35.2%)
2. 计算错误: 32题 (25.0%)
3. 审题错误: 28题 (21.9%)
4. 方法错误: 18题 (14.1%)
5. 粗心大意: 5题 (3.9%)

---

## 学习建议

### 重点攻克
- 🔴 **二次函数**: 错误8次，建议重新学习基础概念
- 🔴 **牛顿第二定律**: 错误6次，需加强受力分析训练

### 保持复习
- 🟡 **定语从句**: 掌握率40%，继续巩固
- 🟡 **氧化还原反应**: 掌握率50%，稳定提升

### 已有进步
- 🟢 **函数单调性**: 从错误5次降至1次
- 🟢 **直线运动**: 掌握率从30%提升至70%

---

## 下周计划

1. 每日复习5道错题
2. 重点攻克二次函数
3. 整理错题知识点思维导图
4. 完成错题专项练习
```

## 打印版模板

适合打印在A4纸上：

```markdown
---
layout: print
paper: A4
margin: 2cm
---

# 错题本

姓名: ________  日期: ________

---

## 第1题

**科目**: 数学  
**知识点**: 二次函数

**题目**:
已知 f(x) = x² + 2x + 1，求 f(x) 的最小值

**我的解答**:
_____________________________________
_____________________________________
_____________________________________

**正确答案**:
最小值是 -1

**错因分析**:
忘记配方方法

---

## 第2题

...
```


path: references/forgetting_curve.md
content: # 艾宾浩斯遗忘曲线复习算法

## 原理

艾宾浩斯遗忘曲线描述了人类记忆的遗忘规律：学习后遗忘立即开始，且遗忘速度先快后慢。

## 标准复习间隔

基于艾宾浩斯研究，推荐复习间隔：

```
第1次复习：学习后 20分钟
第2次复习：学习后 1小时
第3次复习：学习后 9小时
第4次复习：学习后 1天
第5次复习：学习后 2天
第6次复习：学习后 6天
第7次复习：学习后 1个月
第8次复习：学习后 3个月
```

## 实用简化版（每日提醒）

考虑到实际学习场景，采用简化版本：

| 复习次数 | 间隔天数 | 记忆保持率 |
|---------|---------|-----------|
| 第1次   | 1天     | ~80%      |
| 第2次   | 2天     | ~85%      |
| 第3次   | 4天     | ~90%      |
| 第4次   | 7天     | ~93%      |
| 第5次   | 15天    | ~95%      |
| 第6次   | 30天    | ~98%      |

## 算法实现

### 计算下次复习日期

```python
from datetime import datetime, timedelta

REVIEW_INTERVALS = [1, 2, 4, 7, 15, 30]

def get_next_review_date(created_date: datetime, review_count: int) -> datetime:
    """
    计算下次复习日期
    
    Args:
        created_date: 错题创建日期
        review_count: 已复习次数
    
    Returns:
        下次复习日期
    """
    if review_count >= len(REVIEW_INTERVALS):
        # 超过预设次数后，每30天复习一次
        interval = 30
    else:
        interval = REVIEW_INTERVALS[review_count]
    
    return datetime.now() + timedelta(days=interval)
```

### 动态调整

根据掌握状态调整间隔：

- **已掌握**：间隔延长50%（下次复习时间推后）
- **仍需强化**：间隔缩短50%（加快复习频率）
- **忘记**：重置复习周期，从头开始

```python
def adjust_review_interval(next_review: datetime, status: str) -> datetime:
    """
    根据掌握状态调整复习间隔
    """
    if status == "已掌握":
        # 延长间隔
        delta = next_review - datetime.now()
        return datetime.now() + delta * 1.5
    elif status == "仍需强化":
        # 缩短间隔
        delta = next_review - datetime.now()
        return datetime.now() + delta * 0.5
    elif status == "忘记":
        # 重置到第1次复习
        return datetime.now() + timedelta(days=1)
    return next_review
```

## 复习优先级

当有多道错题需要复习时，按以下优先级排序：

1. **超期未复习**：当前日期 > 应复习日期
2. **今日到期**：当前日期 = 应复习日期
3. **错误次数多**：按错误次数降序
4. **难度高**：困难 > 中等 > 简单

```python
def sort_mistakes_for_review(mistakes: list) -> list:
    """
    为错题排序，确定复习优先级
    """
    def priority_score(m):
        score = 0
        # 超期加分
        if m['next_review'] < datetime.now():
            score += 1000
        # 错误次数加分
        score += m.get('error_count', 0) * 10
        # 难度加分
        difficulty_scores = {'困难': 5, '中等': 3, '简单': 1}
        score += difficulty_scores.get(m.get('difficulty'), 3)
        return score
    
    return sorted(mistakes, key=priority_score, reverse=True)
```

## 学习建议

基于统计数据，提供个性化学习建议：

1. **遗忘曲线陡峭**：科目/知识点需要增加复习频率
2. **错误集中**：重点攻克薄弱知识点
3. **长期未掌握**：考虑重新学习基础概念
4. **掌握率过低**：建议寻求老师/同学帮助

## 数据结构

```json
{
  "id": "math-2024-001",
  "subject": "数学",
  "topic": "二次函数",
  "created_at": "2024-01-15T10:30:00",
  "review_history": [
    {
      "date": "2024-01-16T09:00:00",
      "status": "仍需强化",
      "note": "又忘记配方了"
    },
    {
      "date": "2024-01-17T15:30:00",
      "status": "已掌握",
      "note": "这次做对了"
    }
  ],
  "review_count": 2,
  "next_review": "2024-01-21T00:00:00",
  "mastered": false
}
```


path: references/subjects.json
content: {
  "subjects": {
    "数学": {
      "code": "math",
      "topics": {
        "代数": ["二次函数", "方程与不等式", "数列", "集合", "函数概念"],
        "几何": ["平面几何", "立体几何", "解析几何", "向量"],
        "三角函数": ["三角恒等式", "三角函数图像", "解三角形"],
        "概率统计": ["概率", "统计", "排列组合"],
        "导数": ["导数概念", "导数应用", "积分"]
      }
    },
    "物理": {
      "code": "physics",
      "topics": {
        "力学": ["运动学", "牛顿定律", "功和能", "动量", "圆周运动"],
        "电磁学": ["电场", "磁场", "电磁感应", "电路"],
        "热学": ["热力学", "气体状态方程"],
        "光学": ["几何光学", "物理光学"],
        "原子物理": ["原子结构", "核反应"]
      }
    },
    "化学": {
      "code": "chemistry",
      "topics": {
        "基本概念": ["氧化还原反应", "离子反应", "物质的量"],
        "元素化合物": ["金属", "非金属", "有机化合物"],
        "化学反应原理": ["化学平衡", "电化学", "反应速率"],
        "物质结构": ["原子结构", "化学键", "晶体结构"]
      }
    },
    "英语": {
      "code": "english",
      "topics": {
        "语法": ["时态语态", "从句", "非谓语动词", "虚拟语气"],
        "词汇": ["词义辨析", "固定搭配", "词根词缀"],
        "阅读": ["主旨大意", "推理判断", "细节理解"],
        "写作": ["书信", "议论文", "记叙文"]
      }
    },
    "语文": {
      "code": "chinese",
      "topics": {
        "文言文": ["实词虚词", "句式", "翻译"],
        "现代文": ["记叙文", "议论文", "说明文"],
        "古诗词": ["诗歌鉴赏", "名句默写"],
        "作文": ["立意", "素材", "结构"]
      }
    },
    "生物": {
      "code": "biology",
      "topics": {
        "细胞": ["细胞结构", "细胞代谢", "细胞分裂"],
        "遗传": ["孟德尔定律", "DNA复制", "基因表达"],
        "生态": ["种群", "群落", "生态系统"],
        "生理": ["内环境", "神经调节", "体液调节"]
      }
    },
    "历史": {
      "code": "history",
      "topics": {
        "中国史": ["古代史", "近代史", "现代史"],
        "世界史": ["古代史", "近代史", "现代史"]
      }
    },
    "地理": {
      "code": "geography",
      "topics": {
        "自然地理": ["地球运动", "大气", "水文", "地貌"],
        "人文地理": ["人口", "城市", "产业", "交通"],
        "区域地理": ["中国地理", "世界地理"]
      }
    },
    "政治": {
      "code": "politics",
      "topics": {
        "经济生活": ["市场经济", "宏观调控", "国际经济"],
        "政治生活": ["人民当家作主", "依法治国", "国际社会"],
        "文化生活": ["文化传承", "文化创新"],
        "生活与哲学": ["唯物论", "辩证法", "认识论"]
      }
    }
  },
  "difficulty": ["简单", "中等", "困难"],
  "errorTypes": ["概念错误", "计算错误", "审题错误", "方法错误", "粗心大意"],
  "statusTypes": ["待复习", "已掌握", "需强化"]
}


path: references/templates.md
content: # 错题录入模板

## 基础模板（必填项）

```
【科目】______
【题目】______
【错答】______
【正答】______
【原因】______
```

## 完整模板（推荐）

```
【科目】______
【题目】______
【错答】______
【正答】______
【原因】______
【知识点】______
【难度】简单/中等/困难
【来源】______
【日期】______
【备注】______
```

## 选择题模板

```
【科目】______
【题型】选择题
【题目】______
【选项】
A. ______
B. ______
C. ______
D. ______
【错选】______
【正解】______
【原因】______
【知识点】______
```

## 填空题模板

```
【科目】______
【题型】填空题
【题目】______
【错答】______
【正答】______
【原因】______
【知识点】______
```

## 解答题/计算题模板

```
【科目】______
【题型】解答题
【题目】______
【错误解答】
______

【正确解答】
______

【错误分析】
______

【知识点】______
【难度】______
```

## 简答题模板

```
【科目】______
【题型】简答题
【问题】______
【错答】______
【正答】______
【评分要点】
1. ______
2. ______
3. ______
【失分原因】______
```

## 快捷录入示例

### 示例1：数学选择題

```
数學 选择题
题目：已知 f(x) = x² - 4x + 3，求 f(x) 的最小值
选项：A.3 B.1 C.-1 D.0
错选：A
正解：C
原因：配方错误，f(x) = (x-2)² - 1
知识点：二次函数
```

### 示例2：物理计算题

```
物理
题目：质量为2kg的物体，在水平面上受到10N的拉力，摩擦力为4N，求加速度
错答：a = F/m = 10/2 = 5 m/s²
正答：a = (F-f)/m = (10-4)/2 = 3 m/s²
原因：忘记考虑摩擦力
知识点：牛顿第二定律
难度：简单
```

### 示例3：英语语法

```
英语
题目：He suggested that we ____ a meeting.
错答：will have
正答：(should) have
原因：suggest后的从句用虚拟语气，(should)+动词原形
知识点：虚拟语气
```

## 批量录入格式

适合一次性录入多道错题：

```
===== 错题1 =====
科目：数学
题目：...
正答：...

===== 错题2 =====
科目：物理
题目：...
正答：...
```

## 图片录入

提供图片路径时，系统会自动OCR识别：

```
录入错题，图片路径：/path/to/image.png
科目：数学
知识点：二次函数
```

## 语音录入（未来支持）

```
语音转文字后按模板解析...
```

## 录入后确认

录入完成后，助手会显示：

```
✅ 错题已记录

【ID】math-2024-0128
【科目】数学 - 二次函数
【难度】中等
【复习】明天首次复习

记录正确？需要修改请告诉我。
```


path: scripts/add_mistake.py
content: #!/usr/bin/env python3
"""
添加错题到数据库
"""

import json
import os
from datetime import datetime, timedelta
from pathlib import Path
import uuid

# 数据目录
DATA_DIR = Path(os.environ.get('WORKSPACE', '.')) / 'mistake-data'
INDEX_FILE = DATA_DIR / 'index.json'


def init_data_dir():
    """初始化数据目录"""
    DATA_DIR.mkdir(exist_ok=True, parents=True)
    
    if not INDEX_FILE.exists():
        index_data = {
            'mistakes': [],
            'subjects': {},
            'created_at': datetime.now().isoformat(),
            'updated_at': datetime.now().isoformat()
        }
        save_json(INDEX_FILE, index_data)


def load_json(filepath):
    """加载JSON文件"""
    if not filepath.exists():
        return None
    with open(filepath, 'r', encoding='utf-8') as f:
        return json.load(f)


def save_json(filepath, data):
    """保存JSON文件"""
    filepath.parent.mkdir(parents=True, exist_ok=True)
    with open(filepath, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def generate_mistake_id(subject: str) -> str:
    """生成错题ID"""
    subject_codes = {
        '数学': 'math', '物理': 'physics', '化学': 'chemistry',
        '英语': 'english', '语文': 'chinese', '生物': 'biology',
        '历史': 'history', '地理': 'geography', '政治': 'politics'
    }
    code = subject_codes.get(subject, 'other')
    date_str = datetime.now().strftime('%Y%m%d')
    short_id = str(uuid.uuid4())[:4]
    return f"{code}-{date_str}-{short_id}"


def calculate_next_review(review_count: int = 0) -> str:
    """计算下次复习日期（艾宾浩斯曲线）"""
    intervals = [1, 2, 4, 7, 15, 30]
    
    if review_count >= len(intervals):
        days = 30
    else:
        days = intervals[review_count]
    
    next_date = datetime.now() + timedelta(days=days)
    return next_date.strftime('%Y-%m-%d')


def add_mistake(
    subject: str,
    question: str,
    wrong_answer: str,
    correct_answer: str,
    reason: str,
    topic: str = None,
    difficulty: str = '中等',
    source: str = None,
    question_type: str = '解答题',
    options: dict = None,
    notes: str = None
) -> dict:
    """
    添加错题
    
    Args:
        subject: 科目
        question: 题目内容
        wrong_answer: 错误答案
        correct_answer: 正确答案
        reason: 错误原因
        topic: 知识点
        difficulty: 难度（简单/中等/困难）
        source: 来源
        question_type: 题型
        options: 选择题选项
        notes: 备注
    
    Returns:
        创建的错题记录
    """
    init_data_dir()
    
    # 生成ID
    mistake_id = generate_mistake_id(subject)
    
    # 创建错题记录
    mistake = {
        'id': mistake_id,
        'subject': subject,
        'topic': topic,
        'question_type': question_type,
        'question': question,
        'wrong_answer': wrong_answer,
        'correct_answer': correct_answer,
        'reason': reason,
        'difficulty': difficulty,
        'source': source,
        'options': options,
        'notes': notes,
        'created_at': datetime.now().isoformat(),
        'review_count': 0,
        'next_review': calculate_next_review(0),
        'mastered': False,
        'error_count': 1,
        'review_history': []
    }
    
    # 保存到科目文件
    subject_codes = {
        '数学': 'math', '物理': 'physics', '化学': 'chemistry',
        '英语': 'english', '语文': 'chinese', '生物': 'biology',
        '历史': 'history', '地理': 'geography', '政治': 'politics'
    }
    code = subject_codes.get(subject, 'other')
    subject_file = DATA_DIR / code / f"{topic or 'general'}.json"
    
    # 加载或创建科目数据
    if subject_file.exists():
        subject_data = load_json(subject_file)
    else:
        subject_data = {
            'subject': subject,
            'code': code,
            'topic': topic,
            'mistakes': [],
            'created_at': datetime.now().isoformat()
        }
    
    subject_data['mistakes'].append(mistake)
    subject_data['updated_at'] = datetime.now().isoformat()
    save_json(subject_file, subject_data)
    
    # 更新索引
    index_data = load_json(INDEX_FILE)
    if index_data is None:
        index_data = {'mistakes': [], 'subjects': {}}
    
    index_data['mistakes'].append({
        'id': mistake_id,
        'subject': subject,
        'topic': topic,
        'difficulty': difficulty,
        'created_at': mistake['created_at'],
        'next_review': mistake['next_review'],
        'mastered': False
    })
    
    # 更新科目统计
    if subject not in index_data['subjects']:
        index_data['subjects'][subject] = {'count': 0, 'topics': {}}
    index_data['subjects'][subject]['count'] += 1
    
    if topic:
        if topic not in index_data['subjects'][subject]['topics']:
            index_data['subjects'][subject]['topics'][topic] = 0
        index_data['subjects'][subject]['topics'][topic] += 1
    
    index_data['updated_at'] = datetime.now().isoformat()
    save_json(INDEX_FILE, index_data)
    
    return mistake


def main():
    """测试示例"""
    mistake = add_mistake(
        subject='数学',
        question='已知 f(x) = x² + 2x + 1，求 f(x) 的最小值',
        wrong_answer='最小值是 0',
        correct_answer='最小值是 -1，f(x) = (x+1)² - 1',
        reason='忘记配方，直接代入顶点公式错误',
        topic='二次函数',
        difficulty='中等',
        source='课本第23页例题',
        notes='记住配方方法：f(x) = a(x-h)² + k'
    )
    
    print(f"✅ 错题已记录")
    print(f"ID: {mistake['id']}")
    print(f"科目: {mistake['subject']} - {mistake['topic']}")
    print(f"下次复习: {mistake['next_review']}")


if __name__ == '__main__':
    main()


path: scripts/export.py
content: #!/usr/bin/env python3
"""
导出错题本
支持格式: Markdown, HTML, JSON, PDF(需安装weasyprint)
"""

import json
from datetime import datetime
from pathlib import Path
import os

DATA_DIR = Path(os.environ.get('WORKSPACE', '.')) / 'mistake-data'
OUTPUT_DIR = Path(os.environ.get('WORKSPACE', '.')) / 'mistake-exports'


def load_json(filepath):
    """加载JSON文件"""
    if not filepath.exists():
        return None
    with open(filepath, 'r', encoding='utf-8') as f:
        return json.load(f)


def get_all_mistakes() -> list:
    """获取所有错题"""
    mistakes = []
    
    if not DATA_DIR.exists():
        return mistakes
    
    for subject_dir in DATA_DIR.iterdir():
        if subject_dir.is_dir() and subject_dir.name not in ['.', '..']:
            for topic_file in subject_dir.glob('*.json'):
                data = load_json(topic_file)
                if data:
                    mistakes.extend(data.get('mistakes', []))
    
    return mistakes


def filter_mistakes(mistakes: list, subject: str = None, 
                    topic: str = None, days: int = None) -> list:
    """筛选错题"""
    filtered = mistakes.copy()
    
    if subject:
        filtered = [m for m in filtered if m.get('subject') == subject]
    
    if topic:
        filtered = [m for m in filtered if m.get('topic') == topic]
    
    if days:
        cutoff = datetime.now() - __import__('datetime').timedelta(days=days)
        filtered = [
            m for m in filtered 
            if datetime.fromisoformat(m.get('created_at', '')) >= cutoff
        ]
    
    return filtered


def export_markdown(mistakes: list, title: str = "我的错题本") -> str:
    """导出为Markdown格式"""
    lines = [
        f"# {title}\n",
        f"导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n",
        f"总数: {len(mistakes)}题\n",
        "---\n"
    ]
    
    # 按科目分组
    by_subject = {}
    for m in mistakes:
        subject = m.get('subject', '未分类')
        if subject not in by_subject:
            by_subject[subject] = []
        by_subject[subject].append(m)
    
    for subject, items in sorted(by_subject.items()):
        lines.append(f"\n## {subject}\n")
        
        for i, m in enumerate(items, 1):
            topic = m.get('topic', '未分类')
            difficulty = m.get('difficulty', '中等')
            
            lines.append(f"### {i}. [{topic}] {m.get('question_type', '题目')}\n")
            lines.append(f"**难度**: {difficulty}\n")
            lines.append(f"**题目**: {m.get('question')}\n")
            
            if m.get('options'):
                lines.append("**选项**:")
                for key, val in m['options'].items():
                    lines.append(f"- {key}. {val}")
                lines.append("")
            
            lines.append(f"**错误答案**: {m.get('wrong_answer')}\n")
            lines.append(f"**正确答案**: {m.get('correct_answer')}\n")
            lines.append(f"**错误原因**: {m.get('reason')}\n")
            
            if m.get('notes'):
                lines.append(f"**备注**: {m['notes']}\n")
            
            lines.append("---\n")
    
    return '\n'.join(lines)


def export_html(mistakes: list, title: str = "我的错题本") -> str:
    """导出为HTML格式"""
    html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{title}</title>
    <style>
        body {{
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
        }}
        .header {{
            text-align: center;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 10px;
            margin-bottom: 20px;
        }}
        .subject {{
            background: white;
            padding: 20px;
            margin-bottom: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }}
        .subject h2 {{
            color: #667eea;
            border-bottom: 2px solid #667eea;
            padding-bottom: 10px;
        }}
        .mistake {{
            background: #fafafa;
            padding: 15px;
            margin: 10px 0;
            border-left: 4px solid #764ba2;
            border-radius: 5px;
        }}
        .question {{
            font-size: 1.1em;
            font-weight: bold;
            color: #333;
        }}
        .wrong {{
            color: #e74c3c;
            background: #ffe6e6;
            padding: 5px 10px;
            border-radius: 3px;
        }}
        .correct {{
            color: #27ae60;
            background: #e6ffe6;
            padding: 5px 10px;
            border-radius: 3px;
        }}
        .meta {{
            font-size: 0.9em;
            color: #888;
            margin-top: 10px;
        }}
        .tag {{
            display: inline-block;
            background: #667eea;
            color: white;
            padding: 2px 8px;
            border-radius: 3px;
            font-size: 0.8em;
            margin-right: 5px;
        }}
    </style>
</head>
<body>
    <div class="header">
        <h1>{title}</h1>
        <p>导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M')} | 共 {len(mistakes)} 题</p>
    </div>
"""
    
    # 按科目分组
    by_subject = {}
    for m in mistakes:
        subject = m.get('subject', '未分类')
        if subject not in by_subject:
            by_subject[subject] = []
        by_subject[subject].append(m)
    
    for subject, items in sorted(by_subject.items()):
        html += f'    <div class="subject">\n        <h2>{subject}</h2>\n'
        
        for m in items:
            html += f"""
        <div class="mistake">
            <div class="question">{m.get('question', '')}</div>
            <p><strong>错误答案:</strong> <span class="wrong">{m.get('wrong_answer', '')}</span></p>
            <p><strong>正确答案:</strong> <span class="correct">{m.get('correct_answer', '')}</span></p>
            <p><strong>错误原因:</strong> {m.get('reason', '')}</p>
            <div class="meta">
                <span class="tag">{m.get('topic', '未分类')}</span>
                <span class="tag">{m.get('difficulty', '中等')}</span>
            </div>
        </div>
"""
        
        html += '    </div>\n'
    
    html += '</body>\n</html>'
    
    return html


def export_json(mistakes: list) -> str:
    """导出为JSON格式（备份用）"""
    return json.dumps({
        'exported_at': datetime.now().isoformat(),
        'total': len(mistakes),
        'mistakes': mistakes
    }, ensure_ascii=False, indent=2)


def export_mistakes(
    format: str = 'markdown',
    subject: str = None,
    topic: str = None,
    days: int = None,
    output_file: str = None
) -> str:
    """
    导出错题
    
    Args:
        format: 导出格式 (markdown/html/json)
        subject: 筛选科目
        topic: 筛选知识点
        days: 筛选最近N天
        output_file: 输出文件路径
    
    Returns:
        导出内容或文件路径
    """
    mistakes = get_all_mistakes()
    mistakes = filter_mistakes(mistakes, subject, topic, days)
    
    if not mistakes:
        return "没有找到符合条件的错题"
    
    # 根据格式导出
    format = format.lower()
    
    if format == 'markdown' or format == 'md':
        content = export_markdown(mistakes)
        ext = '.md'
    elif format == 'html':
        content = export_html(mistakes)
        ext = '.html'
    elif format == 'json':
        content = export_json(mistakes)
        ext = '.json'
    else:
        return f"不支持的格式: {format}"
    
    # 保存到文件
    if output_file:
        output_path = Path(output_file)
    else:
        OUTPUT_DIR.mkdir(exist_ok=True, parents=True)
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        output_path = OUTPUT_DIR / f"mistakes_{timestamp}{ext}"
    
    output_path.parent.mkdir(parents=True, exist_ok=True)
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(content)
    
    return f"✅ 已导出到: {output_path}"


def main():
    """测试"""
    # 测试各种导出格式
    print(export_mistakes(format='markdown'))
    print()
    print(export_mistakes(format='html'))
    print()
    print(export_mistakes(format='json'))


if __name__ == '__main__':
    main()


path: scripts/review_today.py
content: #!/usr/bin/env python3
"""
获取今日待复习错题
"""

import json
from datetime import datetime
from pathlib import Path
import os

DATA_DIR = Path(os.environ.get('WORKSPACE', '.')) / 'mistake-data'
INDEX_FILE = DATA_DIR / 'index.json'


def load_json(filepath):
    """加载JSON文件"""
    if not filepath.exists():
        return None
    with open(filepath, 'r', encoding='utf-8') as f:
        return json.load(f)


def get_today_reviews() -> list:
    """
    获取今日待复习错题
    
    Returns:
        今日到期和超期的错题列表
    """
    if not INDEX_FILE.exists():
        return []
    
    index_data = load_json(INDEX_FILE)
    if not index_data:
        return []
    
    today = datetime.now().strftime('%Y-%m-%d')
    
    # 筛选今日需复习的错题
    to_review = []
    for item in index_data.get('mistakes', []):
        # 跳过已掌握的
        if item.get('mastered', False):
            continue
        
        next_review = item.get('next_review', '')
        # 今日到期或已超期
        if next_review <= today:
            to_review.append(item)
    
    # 按优先级排序：超期 > 今日到期 > 错误次数 > 难度
    def sort_key(m):
        is_overdue = 0 if m.get('next_review', '') < today else 1
        error_count = m.get('error_count', 0)
        difficulty_order = {'困难': 0, '中等': 1, '简单': 2}
        difficulty = difficulty_order.get(m.get('difficulty', '中等'), 1)
        return (is_overdue, -error_count, difficulty)
    
    to_review.sort(key=sort_key)
    
    return to_review


def get_mistake_detail(mistake_id: str) -> dict:
    """获取错题详情"""
    if not INDEX_FILE.exists():
        return None
    
    # 从ID推断文件路径
    parts = mistake_id.split('-')
    if len(parts) < 2:
        return None
    
    code = parts[0]
    
    # 搜索该科目下的所有文件
    subject_dir = DATA_DIR / code
    if not subject_dir.exists():
        return None
    
    for topic_file in subject_dir.glob('*.json'):
        data = load_json(topic_file)
        if data:
            for mistake in data.get('mistakes', []):
                if mistake.get('id') == mistake_id:
                    return mistake
    
    return None


def format_review_list(mistakes: list) -> str:
    """格式化复习列表"""
    if not mistakes:
        return "📭 今日无待复习错题"
    
    output = [f"📝【今日待复习 - {len(mistakes)}题】\n"]
    
    for i, m in enumerate(mistakes, 1):
        subject = m.get('subject', '未知')
        topic = m.get('topic', '未分类')
        difficulty = m.get('difficulty', '中等')
        mistake_id = m.get('id', '')
        
        # 状态标记
        next_review = m.get('next_review', '')
        today = datetime.now().strftime('%Y-%m-%d')
        status = "⚠️ 已超期" if next_review < today else "📅 今日到期"
        
        output.append(f"{i}. {status}")
        output.append(f"   [{subject}-{topic}]")
        output.append(f"   难度: {difficulty}")
        output.append(f"   ID: {mistake_id}")
        output.append("")
    
    output.append("💡 回复「开始复习」或「查看题目 [ID]」")
    
    return '\n'.join(output)


def main():
    """测试"""
    mistakes = get_today_reviews()
    print(format_review_list(mistakes))
    
    if mistakes:
        print("\n" + "="*50)
        detail = get_mistake_detail(mistakes[0]['id'])
        if detail:
            print(f"\n【题目详情】")
            print(f"题目: {detail.get('question')}")
            print(f"错误答案: {detail.get('wrong_answer')}")
            print(f"正确答案: {detail.get('correct_answer')}")


if __name__ == '__main__':
    main()


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