已开启
[共创大赛] feat: add math-worksheet plugin - math worksheet generator #2
[共创大赛] feat: add math-worksheet plugin - math worksheet generator #2
已开启
gcw_pQGZdeQf创建于 7月3日
共 5 个文件变更+382-0
@@ -10189,6 +10189,33 @@
10189 "code"10189 "code"
10190 ],10190 ],
10191 "version": "0.1.0"10191 "version": "0.1.0"
10192+ },
10193+ {
10194+ "author": {
10195+ "email": "36114870@qq.com",
10196+ "name": "心栖智学"
10197+ },
10198+ "category": "education",
10199+ "description": "Math worksheet generator for Chinese elementary school (grade 1-6). Auto-generates printable HTML worksheets with answer keys.",
10200+ "description_zh": "小学数学智能出题器 - 一键生成 1~6 年级数学练习题,支持加减乘除、分数、小数、百分数、比例等题型。输出带参考答案的 HTML 格式练习题,可打印可导出 PDF。教师家长必备。",
10201+ "gitcode_project_id": "9981130",
10202+ "homepage": "https://gitcode.com/atomgit_atomcode/atomcode-plugins-official/tree/main/plugins/math-worksheet",
10203+ "keywords": [
10204+ "Education"
10205+ ],
10206+ "name": "math-worksheet",
10207+ "source": {
10208+ "path": "plugins/math-worksheet",
10209+ "ref": "main",
10210+ "source": "git-subdir",
10211+ "url": "https://gitcode.com/atomgit_atomcode/atomcode-plugins-official.git"
10212+ },
10213+ "tier": "verified",
10214+ "strict": true,
10215+ "tags": [
10216+ "Education"
10217+ ],
10218+ "version": "1.0.0"
10192 }10219 }
10193 ],10220 ],
10194 "version": "1.0.0"10221 "version": "1.0.0"
Aplugins/math-worksheet/.atomcode-plugin/plugin.json+6-0文件内容审核中,请稍后刷新重试
@@ -0,0 +1,52 @@
1+# 小学数学智能出题器 (Math Worksheet Generator)
2+ 
3+一键生成高质量小学数学练习题,支持 1~6 年级全学段。
4+由 AI 自动生成不重复题目,带参考答案,可打印可导出 PDF。
5+ 
6+## 使用方法
7+ 
8+在 AtomCode 中运行:
9+ 
10+```
11+/math-worksheet:math-worksheet -g 3 -c 30 -o math.html
12+```
13+ 
14+或在终端直接运行:
15+ 
16+```bash
17+python3 scripts/math_worksheet.py -g 3 -c 30 -o worksheet.html
18+```
19+ 
20+### 参数说明
21+ 
22+| 参数 | 简写 | 说明 | 默认值 |
23+|------|------|------|--------|
24+| `--grade` | `-g` | 年级 (1-6) | 3 |
25+| `--count` | `-c` | 题目数量 | 20 |
26+| `--output` | `-o` | 输出 HTML 文件 | worksheet.html |
27+| `--title` | `-t` | 自定义标题 | 自动生成 |
28+ 
29+### 各年级题型
30+ 
31+| 年级 | 题型 |
32+|------|------|
33+| 一年级 | 20以内加减法 |
34+| 二年级 | 乘法口诀 + 两位数加减 |
35+| 三年级 | 多位数乘除 + 分数基础 |
36+| 四年级 | 三位数乘两位数 + 四则混合 |
37+| 五年级 | 小数运算 + 分数运算 |
38+| 六年级 | 百分数 + 圆面积 + 比例 |
39+ 
40+## 适用场景
41+ 
42+- 教师快速出题、打印练习题
43+- 家长辅导孩子课后练习
44+- 考前专项训练
45+ 
46+## 许可
47+ 
48+MIT License
49+ 
50+## 作者
51+ 
52+- 邮箱:36114870@qq.com
@@ -0,0 +1,249 @@
1+#!/usr/bin/env python3
2+"""
3+小学数学智能出题器 v1.0
4+Auto Math Worksheet Generator for Chinese Elementary School
5+Usage: python3 math_worksheet.py --grade 3 --count 20 --output worksheet.html
6+"""
7+import argparse, random, os, datetime
8+from html import escape as escape_html
9+from fractions import Fraction
10+from dataclasses import dataclass
11+from typing import List
12+ 
13+VERSION = "1.0.0"
14+AUTHOR = "心栖智学 AI 工具组"
15+ 
16+@dataclass
17+class Question:
18+ content: str
19+ answer: str
20+ difficulty: int # 1-6
21+ 
22+class MathGenerator:
23+ """Generate elementary school math questions"""
24+ 
25+ def __init__(self, grade: int):
26+ self.grade = grade
27+ random.seed()
28+ 
29+ def generate(self, count: int) -> List[Question]:
30+ if self.grade == 1:
31+ return self._grade1(count)
32+ elif self.grade == 2:
33+ return self._grade2(count)
34+ elif self.grade == 3:
35+ return self._grade3(count)
36+ elif self.grade == 4:
37+ return self._grade4(count)
38+ elif self.grade == 5:
39+ return self._grade5(count)
40+ elif self.grade == 6:
41+ return self._grade6(count)
42+ else:
43+ raise ValueError(f"Unsupported grade: {self.grade}")
44+ 
45+ def _grade1(self, n):
46+ qs = []
47+ for _ in range(n):
48+ a, b = random.randint(1, 9), random.randint(1, 9)
49+ op = random.choice(['+', '-'])
50+ if op == '-' and a < b:
51+ a, b = b, a
52+ result = a + b if op == '+' else a - b
53+ qs.append(Question(f"{a} {op} {b} = ___", str(result), 1))
54+ return qs
55+ 
56+ def _grade2(self, n):
57+ qs = []
58+ for _ in range(n // 2):
59+ a = random.randint(1, 9)
60+ b = random.randint(1, 9)
61+ qs.append(Question(f"{a} x {b} = ___", str(a * b), 2))
62+ for _ in range(n - n // 2):
63+ a = random.randint(10, 99)
64+ b = random.randint(10, 99)
65+ op = random.choice(['+', '-'])
66+ if op == '-' and a < b:
67+ a, b = b, a
68+ r = a + b if op == '+' else a - b
69+ qs.append(Question(f"{a} {op} {b} = ___", str(r), 2))
70+ return qs
71+ 
72+ def _grade3(self, n):
73+ qs = []
74+ for i in range(n):
75+ t = i % 3
76+ if t == 0:
77+ a = random.randint(10, 99); b = random.randint(2, 9)
78+ qs.append(Question(f"{a} x {b} = ___", str(a * b), 3))
79+ elif t == 1:
80+ a = random.randint(2, 9); b = random.randint(10, 99); r = a * b
81+ qs.append(Question(f"{r} / {a} = ___", str(b), 3))
82+ else:
83+ d = random.randint(2, 9); n1 = random.randint(1, d-1); n2 = random.randint(1, d-1)
84+ op = random.choice(['+', '-'])
85+ if op == '-' and n1 < n2: n1, n2 = n2, n1
86+ r = n1 + n2 if op == '+' else n1 - n2
87+ qs.append(Question(f"{n1}/{d} {op} {n2}/{d} = ___/{d}", str(r), 3))
88+ return qs
89+ 
90+ def _grade4(self, n):
91+ qs = []
92+ for i in range(n):
93+ t = i % 3
94+ if t == 0:
95+ a = random.randint(100, 999); b = random.randint(10, 99)
96+ qs.append(Question(f"{a} x {b} = ___", str(a * b), 4))
97+ elif t == 1:
98+ a = random.randint(10, 99); b = random.randint(2, 20); r = a * b
99+ qs.append(Question(f"{r} / {a} = ___", str(b), 4))
100+ else:
101+ a, b, c = random.randint(1,9), random.randint(1,9), random.randint(1,9)
102+ qs.append(Question(f"{a} x {b} + {c} = ___", str(a*b+c), 4))
103+ return qs
104+ 
105+ def _grade5(self, n):
106+ qs = []
107+ for i in range(n):
108+ t = i % 3
109+ if t == 0:
110+ # 用整数运算确保乘积精确(1位小数 × 1位小数 = 2位小数)
111+ a_int = random.randint(10, 999) # 1位小数: 1.0 ~ 99.9
112+ b_int = random.randint(1, 99) # 1位小数: 0.1 ~ 9.9
113+ a_str = f"{a_int//10}.{a_int%10}"
114+ b_str = f"{b_int//10}.{b_int%10}"
115+ product = a_int * b_int
116+ pi, pf = divmod(product, 100)
117+ r = f"{pi}.{pf:02d}".rstrip('0').rstrip('.')
118+ qs.append(Question(f"{a_str} x {b_str} = ___", r, 5))
119+ elif t == 1:
120+ d = random.randint(2, 12); n1, n2 = random.randint(1,d-1), random.randint(1,d-1)
121+ r = Fraction(n1, d) + Fraction(n2, d)
122+ qs.append(Question(f"{n1}/{d} + {n2}/{d} = ___", str(r), 5))
123+ else:
124+ a, b = random.randint(10,99), random.randint(10,99)
125+ m = random.randint(2, 5)
126+ qs.append(Question(f"({a} + {b}) x {m} = ___", str((a+b)*m), 5))
127+ return qs
128+ 
129+ def _grade6(self, n):
130+ qs = []
131+ for i in range(n):
132+ t = i % 4
133+ if t == 0:
134+ # 用整数运算确保结果精确(1位小数运算)
135+ a_int = random.randint(1, 99) # 0.1 ~ 9.9
136+ b_int = random.randint(1, 99)
137+ a_str = f"{a_int//10}.{a_int%10}" if a_int >= 10 else f"0.{a_int}"
138+ b_str = f"{b_int//10}.{b_int%10}" if b_int >= 10 else f"0.{b_int}"
139+ op = random.choice(['x', '/'])
140+ if op == 'x':
141+ product = a_int * b_int
142+ pi, pf = divmod(product, 100)
143+ r = f"{pi}.{pf:02d}".rstrip('0').rstrip('.')
144+ else:
145+ # 约束整除,结果精确
146+ factor = random.randint(1, 20)
147+ a_int = b_int * factor
148+ a_str = f"{a_int//10}.{a_int%10}" if a_int >= 10 else f"0.{a_int}"
149+ b_str = f"{b_int//10}.{b_int%10}" if b_int >= 10 else f"0.{b_int}"
150+ r = str(factor)
151+ qs.append(Question(f"{a_str} {op} {b_str} = ___", r, 6))
152+ elif t == 1:
153+ # 用 100 的约数确保答案精确为整数
154+ p = random.choice([1, 2, 4, 5, 10, 20, 25, 50])
155+ q = 100 // p
156+ k = random.randint(1, 50) * q # 结果(要求的数)
157+ value = p * k // 100 # 已知部分
158+ qs.append(Question(f"一个数的 {p}% 是 {value},这个数是____", str(k), 6))
159+ elif t == 2:
160+ r = random.randint(2, 10)
161+ qs.append(Question(f"半径为 {r}cm 的圆面积 = ____ cm2(pi取3.14)", str(round(3.14*r*r, 2)), 6))
162+ else:
163+ a, b = random.randint(2, 8), random.randint(2, 8)
164+ qs.append(Question(f"{a}:{b} = ___:{b*3}", str(a*3), 6))
165+ return qs
166+ 
167+ 
168+def to_html(questions: List[Question], grade: int, title: str = "") -> str:
169+ if not title:
170+ title = f"小学{grade}年级数学练习题"
171+ title = escape_html(title)
172+ html = f"""<!DOCTYPE html>
173+<html lang="zh-CN">
174+<head>
175+<meta charset="UTF-8"><title>{title}</title>
176+<style>
177+ @page {{ margin: 2cm; }}
178+ body {{ font-family: 'SimSun', 'Noto Sans SC', sans-serif; font-size: 14pt; line-height: 2; }}
179+ h1 {{ text-align: center; font-size: 18pt; }}
180+ .sub {{ text-align: center; color: #666; font-size: 11pt; margin-bottom: 20px; }}
181+ .info {{ text-align: right; font-size: 10pt; color: #999; margin-bottom: 20px; }}
182+ table {{ width: 100%; border-collapse: collapse; }}
183+ td {{ padding: 8px 12px; border: 1px solid #ddd; font-size: 13pt; width: 50%; }}
184+ .answer-section {{ page-break-before: always; }}
185+ .answer-section td {{ color: #c00; font-weight: bold; }}
186+ .footer {{ text-align: center; color: #999; font-size: 9pt; margin-top: 30px; }}
187+</style>
188+</head>
189+<body>
190+<h1>{title}</h1>
atomgit-bot
atomgit-botatomgit-bot7月13日

🟡 Medium Priority

to_html() 函数在第 153 行(<title>)和第 168 行(<h1>)直接将用户通过 --title 传入的字符串拼接到 HTML 模板中,未做任何 HTML 转义(未使用 html.escape())。

失败模式:若用户传入含 HTML 特殊字符的标题,例如:

虽然该工具为本地 CLI 出题器、HTML 仅供用户自己使用,实际安全风险较低,但会导致生成的 HTML 结构异常或渲染错误,属于正确性问题。

默认 title(f"小学{grade}年级数学练习题")是安全的,问题仅影响 --title 自定义标题场景。

建议:在 to_html() 函数中对 title 做 HTML 转义:在文件头部 import html,然后在第 148-149 行之后、拼接 HTML 之前添加 title = html.escape(title)。

likedislike
不准确?
191+<p class="sub">姓名:________ 日期:________ 得分:____/_{len(questions)}</p>
192+<p class="info">共 {len(questions)} 题 | 建议用时 {max(10, len(questions)//2)} 分钟</p>
193+<table>
194+"""
195+ for i in range(0, len(questions), 2):
196+ html += f"<tr><td>{i+1}. {questions[i].content}</td>"
197+ if i+1 < len(questions):
198+ html += f"<td>{i+2}. {questions[i+1].content}</td>"
199+ else:
200+ html += "<td></td>"
201+ html += "</tr>"
202+ 
203+ html += '</table><div class="answer-section"><h2>参考答案</h2><table>'
204+ for i in range(0, len(questions), 2):
205+ html += f"<tr><td>{i+1}. {questions[i].answer}</td>"
206+ if i+1 < len(questions):
207+ html += f"<td>{i+2}. {questions[i+1].answer}</td>"
208+ else:
209+ html += "<td></td>"
210+ html += "</tr>"
211+ 
212+ html += f"""</table></div>
213+<p class="footer">由 {AUTHOR} 自动生成 | {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
214+</body>
215+</html>"""
216+ return html
217+ 
218+ 
219+def main():
220+ parser = argparse.ArgumentParser(description="小学数学智能出题器")
221+ parser.add_argument("--grade", "-g", type=int, default=3, choices=range(1, 7),
222+ help="年级 (1-6)")
223+ parser.add_argument("--count", "-c", type=int, default=20, choices=range(1, 201), help="题目数量 (1-200)")
224+ parser.add_argument("--output", "-o", default="worksheet.html", help="输出文件 (HTML)")
225+ parser.add_argument("--title", "-t", default="", help="自定义标题")
226+ parser.add_argument("--version", "-v", action="store_true", help="显示版本信息")
227+ args = parser.parse_args()
228+ args.output = os.path.abspath(os.path.normpath(args.output))
atomgit-botatomgit-bot
atomgit-botatomgit-bot7月13日

🟠 High Priority

第 209 行 args.output = os.path.abspath(os.path.normpath(args.output)) 相比初版增加了 os.path.abspath,但仅将路径解析为绝对路径,并未限制输出目录范围。

触发条件:攻击者(或恶意输入)传入任意路径,例如:

  • -o /etc/cron.d/malicious → 直接写入绝对路径
  • -o ../../.bashrc → 解析为 /home/user/.bashrc(或其他父目录下的敏感文件)

第 218 行 open(args.output, 'w') 以写入模式打开,会截断覆盖已存在的任意文件。

放大因素:SKILL.md 第 5 行 allowed-tools: "Bash(python3 math_worksheet.py:*)" 允许传入任意参数,AI Agent 可能被诱导传入恶意路径。

失败后果:覆盖系统配置、用户脚本或其他关键文件,造成数据丢失或系统异常。

与上一轮审查的关系:上一轮报告了同样的问题(仅 os.path.normpath),本轮增加了 os.path.abspath 但缺少目录范围校验,修复不完整。

建议:在 os.path.abspath 之后增加安全校验:将解析后的路径与允许的输出目录(如当前工作目录或指定的安全目录)比较,拒绝越界路径。建议方案:使用 os.path.commonpath 或解析后检查目录前缀。如果输出路径指向已存在的文件,还应给出警告或要求 --force 确认。

likedislike
不准确?
atomgit-botatomgit-bot7月29日

🟠 High Priority

第 209 行 args.output = os.path.abspath(os.path.normpath(args.output)) 将用户输入的路径解析为绝对路径,但未限制输出目录范围。

触发条件:攻击者(或恶意输入)传入越界路径,例如:

  • -o /etc/cron.d/malicious → 写入系统定时任务
  • -o ~/.bashrc → 覆盖用户 shell 配置
  • -o ../../../etc/passwd → 路径穿越(虽被 normpath 规范化,但绝对路径仍可指向任意位置)

失败模式:脚本以当前进程权限覆盖任意可写文件,可能导致系统后门植入或数据破坏。

建议:在 os.path.abspath 之后增加输出目录安全校验:用 os.path.commonpath([allowed_dir, resolved_path]) 检查,或限制输出必须在当前工作目录或其子目录内。如果路径越界,拒绝并报错退出。

likedislike
不准确?
gcw_pQGZdeQf
7月30日 评论:
229+ cwd = os.path.abspath(os.getcwd())
230+ try:
231+ if os.path.commonpath([cwd]) != os.path.commonpath([cwd, args.output]):
232+ parser.error(f"输出路径 {args.output} 不在当前工作目录下,已拒绝")
233+ except ValueError:
234+ parser.error(f"输出路径 {args.output} 与当前工作目录不在同一盘符,已拒绝")
235+ 
236+ if args.version:
237+ print(f"小学数学智能出题器 v{VERSION}")
238+ return
239+ 
240+ gen = MathGenerator(args.grade)
241+ questions = gen.generate(args.count)
242+ html = to_html(questions, args.grade, args.title)
243+ with open(args.output, 'w', encoding='utf-8') as f:
244+ f.write(html)
245+ print(f"OK - {len(questions)} 道题已生成到 {os.path.abspath(args.output)}")
246+ 
247+ 
248+if __name__ == "__main__":
249+ main()
@@ -0,0 +1,48 @@
1+---
2+name: math-worksheet
3+description: 小学数学智能出题器 — 当用户需要生成小学数学练习题时使用。支持 1~6 年级,生成带答案的 HTML 练习题。Trigger when user says: 出题、练习题、数学试卷、math worksheet、数学练习。
4+argument-hint: "-g <年级> -c <题数> [-o <输出文件>] [-t <自定义标题>]"
5+allowed-tools: "Bash(python3 math_worksheet.py:*)"
6+---
7+ 
8+# 小学数学智能出题器
9+ 
10+你需要生成数学练习题时使用此工具。该工具位于 `${CLAUDE_SKILL_DIR}/../../scripts/math_worksheet.py`。
11+ 
12+## 使用方法
13+ 
14+1. 根据用户需求确定参数:
15+ - `-g` 或 `--grade`:年级 (1-6),默认 3
16+ - `-c` 或 `--count`:题目数量,默认 20
17+ - `-o` 或 `--output`:输出文件名,默认 worksheet.html
18+ - `-t` 或 `--title`:自定义试卷标题,可选
19+ 
20+2. 运行脚本生成练习题:
21+ 
22+```bash
23+cd "${CLAUDE_SKILL_DIR}/../../scripts"
24+python3 math_worksheet.py -g 3 -c 30 -o 练习题.html
25+```
26+ 
27+3. 输出结果为 HTML 文件,可直接用浏览器打开、打印或导出 PDF。
28+ 
29+## 各年级题型参考
30+ 
31+| 年级 | 重点题型 |
32+|------|---------|
33+| 1 | 20以内加减法(难度★) |
34+| 2 | 乘法口诀、两位数加减(难度★) |
35+| 3 | 多位数乘除、分数基础(难度★★) |
36+| 4 | 三位数乘两位数、四则混合(难度★★) |
37+| 5 | 小数、分数运算(难度★★★) |
38+| 6 | 百分数、圆面积、比例(难度★★★) |
39+ 
40+## 注意事项
41+ 
42+- 每次运行题目随机生成,不重复
43+- 输出文件包含题目和参考答案两部分
44+- 建议题量:低年级 20-30 题,高年级 30-50 题
45+ 
46+---
47+ 
48+Author: 36114870@qq.com | MIT License