Initial commit
This commit is contained in:
117
skills/quiz-html/README.md
Executable file
117
skills/quiz-html/README.md
Executable file
@@ -0,0 +1,117 @@
|
||||
# quiz-html · 网页题库生成器
|
||||
|
||||
把题目数组 → 一个可独立打开的 HTML 练习网页。
|
||||
|
||||
## ✨ 功能一览
|
||||
|
||||
- 📂 **双重筛选**:分类(学科/子模块) + 学习状态(已掌握/未做/错题)
|
||||
- 🎯 **4 种题型**:选择 / 判断 / 填空 / 简答
|
||||
- 🤖 **智能答题**:选 ≠ 提交,可反复改;答错给一次重试机会
|
||||
- ⌨️ **键盘快捷键**:A/B/C/D · Enter · ← → · Space
|
||||
- 📝 **模拟考模式**:选题量 + 限时 + 一次性提交 + 成绩页
|
||||
- 🧠 **记忆口诀**:题目带 `memory_tip` 字段会高亮显示
|
||||
- 🌓 **主题切换**:明 / 暗双主题,localStorage 记忆
|
||||
- 💾 **进度持久化**:浏览器记住每题状态,关掉再开还在
|
||||
- 📱 **移动端适配**:手机也能用
|
||||
|
||||
## 🚀 快速上手
|
||||
|
||||
```bash
|
||||
# 1. 准备题目 JSON 文件(数组)
|
||||
cat > /tmp/q.json << 'EOF'
|
||||
[
|
||||
{"type":"single_choice","prompt":"1+1=?","options":["A. 1","B. 2","C. 3"],"answer":"B",
|
||||
"explanation":"基础运算","category":"数学 / 加减法","level":1}
|
||||
]
|
||||
EOF
|
||||
|
||||
# 2. 生成 HTML
|
||||
python3 scripts/build_quiz_html.py /tmp/q.json --title "数学练习" --open
|
||||
```
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
quiz-html/
|
||||
├── SKILL.md # skill 描述(给 agent 用)
|
||||
├── README.md # 本文件(给人看)
|
||||
├── skill.yaml # skill 元数据
|
||||
├── scripts/
|
||||
│ └── build_quiz_html.py # 注入脚本
|
||||
├── templates/
|
||||
│ └── quiz_template.html # HTML 模板(含 {{占位符}})
|
||||
└── examples/
|
||||
└── demo.html # 示例:已注入的可直接打开的 demo
|
||||
```
|
||||
|
||||
## 🔗 与 quiz-mastery 联动
|
||||
|
||||
本 skill 不直接出题,专门负责"题目 → 网页"这一步。
|
||||
出题/导入请用 `quiz-mastery`。完整链路:
|
||||
|
||||
```
|
||||
quiz-mastery 出题
|
||||
↓
|
||||
题目 JSON
|
||||
↓
|
||||
询问用户:"要做成网页吗?"
|
||||
↓ 用户说要
|
||||
quiz-html ← 你在这里
|
||||
↓
|
||||
生成 .html
|
||||
↓
|
||||
浏览器打开 → 用户开始练
|
||||
```
|
||||
|
||||
## 📝 题目字段
|
||||
|
||||
| 字段 | 必填 | 类型 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `type` | ✅ | string | `single_choice` / `true_false` / `fill_blank` / `short_answer` |
|
||||
| `prompt` | ✅ | string | 题干 |
|
||||
| `options` | 选择题 | array | `["A. xxx", "B. yyy", ...]` |
|
||||
| `answer` | ✅ | string | 标准答案 |
|
||||
| `explanation` | 推荐 | string | 解析(支持 `**粗体**` `*斜体*` `\` 代码 \``) |
|
||||
| `knowledge_point` | 推荐 | string | 知识点名(侧边栏分组用) |
|
||||
| `category` | 推荐 | string | 分类路径,建议格式 `"学科 / 子模块"` |
|
||||
| `level` | 可选 | int | 难度 1-3 |
|
||||
| `memory_tip` | 可选 | string | 记忆口诀,会用橙色卡片高亮 |
|
||||
|
||||
## ⌨️ 用户使用快捷键
|
||||
|
||||
| 按键 | 作用 |
|
||||
|---|---|
|
||||
| `A` `B` `C` `D` | 选选项 |
|
||||
| `T` `F` | 判断题 |
|
||||
| `Enter` | 提交 |
|
||||
| `←` `→` | 上一题 / 下一题 |
|
||||
| `Space` | 查看答案 |
|
||||
| `Ctrl/⌘+Enter` | 模拟考一键交卷 |
|
||||
|
||||
## 🛠️ 命令行参数
|
||||
|
||||
```
|
||||
python3 build_quiz_html.py <questions.json> [options]
|
||||
|
||||
--output, -o 输出 HTML 路径(默认:题目同目录)
|
||||
--title 页面标题
|
||||
--subtitle 副标题(默认自动生成)
|
||||
--id 题库 ID(用于 localStorage 隔离)
|
||||
--open 生成后用浏览器打开
|
||||
```
|
||||
|
||||
## 🔍 退出码
|
||||
|
||||
| 码 | 含义 |
|
||||
|---|---|
|
||||
| 0 | 成功 |
|
||||
| 1 | 参数错误 / 文件不存在 / 模板缺失 |
|
||||
| 2 | JSON 解析失败 / 数据格式不合法 |
|
||||
|
||||
## 📌 边界
|
||||
|
||||
| 任务 | 用谁 |
|
||||
|---|---|
|
||||
| 出题 / 评分 / 掌握度追踪 | `quiz-mastery` |
|
||||
| 长期学习计划 | `study-buddy` |
|
||||
| **把题目做成网页让用户在浏览器练** | **`quiz-html` (本 skill)** |
|
||||
191
skills/quiz-html/SKILL.md
Executable file
191
skills/quiz-html/SKILL.md
Executable file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
name: quiz-html
|
||||
description: 把题目数组生成一个**可独立运行的网页练习页**(HTML 文件)。当用户完成 quiz-mastery 的「从资料出题」或「从文件提取题目」流程后,应主动询问是否需要"在网页里练习",确认后调用本 skill 把题目注入模板,生成 HTML 给用户。也支持用户直接说"把这些题做成网页/HTML/练习页"时触发。**不处理**:出题(→ quiz-mastery)、评分(→ quiz-mastery)、长期复习计划(→ study-buddy)。
|
||||
---
|
||||
|
||||
# 网页题库生成器 (Quiz HTML Builder)
|
||||
|
||||
把一组题目 JSON → 一个**单文件 HTML 练习网页**,包含:
|
||||
- 📂 分类筛选(学科 / 子模块)+ 学习状态筛选(已掌握 / 未做 / 错题)
|
||||
- 🎯 4 种题型支持:选择 / 判断 / 填空 / 简答
|
||||
- 🤖 答题自动标记,错题自动归入错题本(首次错误给一次重试机会)
|
||||
- ⌨️ 完整键盘快捷键(A/B/C/D · Enter · 方向键 · Space)
|
||||
- 📝 模拟考模式(限时 + 一次性提交 + 成绩页)
|
||||
- 🌓 明暗主题切换 · localStorage 持久化 · 移动端适配
|
||||
|
||||
## 核心触发场景
|
||||
|
||||
### 场景 1:quiz-mastery 出题/导入完成后主动询问 ⭐
|
||||
这是本 skill 的**主要入口**。当 `quiz-mastery` 完成以下任一流程:
|
||||
- 「从资料出题」:`generate_from_material.py` → 生成题目 JSON → `service.import_questions()` 入库
|
||||
- 「从题目文件提取」:`import_quiz.py` → 解析出题目 JSON → 入库
|
||||
|
||||
quiz-mastery 出题完成、向用户展示题目前,**主动问一句**:
|
||||
> "题目准备好啦~ 要不要我把它们生成一个网页练习页?你可以在浏览器里慢慢做,错题会自动记下来,还能切换主题、模拟考试 🎯"
|
||||
|
||||
用户说"要 / 好 / 生成网页 / 来一个 / 嗯"任一肯定意思 → 调用本 skill。
|
||||
用户说"不用 / 算了 / 直接在这里做" → 走原本的对话练习流程。
|
||||
|
||||
### 场景 2:用户直接要求生成网页
|
||||
触发关键词:
|
||||
- "把这些题做成网页"、"做个 HTML 练习页"、"生成一个题库网页"
|
||||
- "我想在浏览器里练"、"做个网页版"
|
||||
- "把题目导出成 HTML"
|
||||
|
||||
## 调用方式
|
||||
|
||||
### 一句话总结
|
||||
```bash
|
||||
python3 scripts/build_quiz_html.py <题目JSON文件> [--title "..." --open]
|
||||
```
|
||||
|
||||
### 标准流程
|
||||
|
||||
1. **拿到题目 JSON**(数组,每项是一道题)
|
||||
- 来源 A:quiz-mastery 出题后的 LLM 输出(系统已是标准格式)
|
||||
- 来源 B:用户直接粘贴的题目数组
|
||||
- 来源 C:从数据库读取的题目(quiz-mastery 的 `data/sessions/<sid>/questions.json`)
|
||||
|
||||
2. **写到临时 JSON 文件**:
|
||||
```python
|
||||
import json, tempfile
|
||||
from pathlib import Path
|
||||
tmp = Path(tempfile.mkdtemp()) / "questions.json"
|
||||
tmp.write_text(json.dumps(questions, ensure_ascii=False), encoding="utf-8")
|
||||
```
|
||||
|
||||
3. **调用脚本**:
|
||||
```bash
|
||||
python3 ~/Desktop/studybuddy_4.0/skills/quiz-html/scripts/build_quiz_html.py \
|
||||
/tmp/xxx/questions.json \
|
||||
--title "📚 物理 · 热学练习" \
|
||||
--output ~/Desktop/quiz_物理热学_20260518.html \
|
||||
--open
|
||||
```
|
||||
|
||||
4. **解析返回 JSON**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"output_path": "/Users/.../quiz_xxx.html",
|
||||
"question_count": 8,
|
||||
"title": "📚 物理 · 热学练习",
|
||||
"subtitle": "共 8 题 · 选择×5 · 判断×2 · 填空×1 · 物理",
|
||||
"id": "q_1779091201",
|
||||
"size_bytes": 63752
|
||||
}
|
||||
```
|
||||
|
||||
5. **告诉用户**:把 HTML 路径报给用户,提示「已经在浏览器打开了,可以开始练啦 ✨」
|
||||
|
||||
## 题目 JSON 字段标准
|
||||
|
||||
完全兼容 quiz-mastery 输出格式,**新增可选字段** `category` / `memory_tip`:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `type` | ✅ | `single_choice` / `true_false` / `fill_blank` / `short_answer` |
|
||||
| `prompt` | ✅ | 题干。也兼容 `question` 字段(自动转换) |
|
||||
| `options` | 选择题必填 | `["A. xxx", "B. yyy", ...]` |
|
||||
| `answer` | ✅ | 选择题填字母;判断题填 `"True"`/`"False"`;填空/简答填文本 |
|
||||
| `explanation` | 推荐 | 解析(强烈建议填,K12 学生需要) |
|
||||
| `knowledge_point` | 推荐 | 知识点名(侧边栏二级分组用) |
|
||||
| `category` | 推荐 | 分类路径,**用"学科 / 子模块"格式**:`"物理 / 电学"`、`"数学 / 分数"` |
|
||||
| `level` | 可选 | 难度 1-3 |
|
||||
| `memory_tip` | 可选 | 记忆口诀,会用橙色卡片高亮显示(K12 神器) |
|
||||
|
||||
### 示例
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "single_choice",
|
||||
"prompt": "下列关于并联电路电流规律的说法,正确的是( )。",
|
||||
"options": [
|
||||
"A. 干路电流等于各支路电流之差",
|
||||
"B. 干路电流等于各支路电流之和",
|
||||
"C. 各支路电流相等",
|
||||
"D. 干路电流大于任一支路电流的两倍"
|
||||
],
|
||||
"answer": "B",
|
||||
"explanation": "并联电路中,**干路电流等于各支路电流之和**:I = I₁ + I₂ + ...",
|
||||
"knowledge_point": "并联电路电流规律",
|
||||
"category": "物理 / 电学",
|
||||
"level": 1,
|
||||
"memory_tip": "🧠 并联看路口:进多少、出多少,电流不会消失"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 设计原则
|
||||
|
||||
### 1. 自动 category,让筛选有意义
|
||||
如果题目缺 `category` 字段,最好补上(哪怕基于学科推断)。否则所有题都堆到"通用"分类下,分类筛选就废了。
|
||||
|
||||
### 2. category 用"学科 / 子模块"
|
||||
- ✅ `"物理 / 电学"`、`"物理 / 热学"` → 顶部出 4 个细分类 chip
|
||||
- ❌ `"物理"` → 只出 1 个,子模块在侧栏体现,但筛选粒度变粗
|
||||
|
||||
### 3. 知识点和分类不是同一层
|
||||
- `category` = 横向分类(哪个学科/章节),用于**顶部 chips 筛选**
|
||||
- `knowledge_point` = 细粒度知识点,用于**左侧栏二级分组**
|
||||
|
||||
### 4. 输出文件命名
|
||||
默认输出到题目 JSON 同目录,文件名 `quiz_<title_slug>_<时间戳>.html`。
|
||||
**建议显式传 `--output`**,放到 `~/Desktop/` 或一个固定目录方便用户找。
|
||||
|
||||
## 工作示例(quiz-mastery 衔接全流程)
|
||||
|
||||
```python
|
||||
# 1. quiz-mastery 已完成出题,拿到题目数组
|
||||
questions = [
|
||||
{"type": "single_choice", "prompt": "...", "options": [...], "answer": "A",
|
||||
"explanation": "...", "knowledge_point": "...", "category": "物理 / 电学"},
|
||||
# ...
|
||||
]
|
||||
|
||||
# 2. agent 问用户:"要不要做成网页版?"
|
||||
# 3. 用户:"要"
|
||||
# 4. agent 写临时文件 + 调用 skill
|
||||
|
||||
import json, subprocess, tempfile
|
||||
from pathlib import Path
|
||||
|
||||
tmp_dir = Path(tempfile.mkdtemp(prefix="quiz_"))
|
||||
qjson = tmp_dir / "questions.json"
|
||||
qjson.write_text(json.dumps(questions, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
output = Path.home() / "Desktop" / "quiz_物理电学.html"
|
||||
|
||||
result = subprocess.run([
|
||||
"python3",
|
||||
str(Path.home() / "Desktop/studybuddy_4.0/skills/quiz-html/scripts/build_quiz_html.py"),
|
||||
str(qjson),
|
||||
"--title", "📚 物理 · 电学练习",
|
||||
"--output", str(output),
|
||||
"--open",
|
||||
], capture_output=True, text=True)
|
||||
|
||||
info = json.loads(result.stdout)
|
||||
# info["output_path"] = "/Users/.../Desktop/quiz_物理电学.html"
|
||||
```
|
||||
|
||||
然后告诉用户:
|
||||
> 「已经做好啦~ 网页已自动打开 ✨
|
||||
> 路径:`~/Desktop/quiz_物理电学.html`
|
||||
> 慢慢做,做完会自动记录错题,下次可以筛"错题"专门攻克 💪」
|
||||
|
||||
## 与其他 skill 的边界
|
||||
|
||||
| 任务 | 用谁 |
|
||||
|---|---|
|
||||
| 从资料出题 | **quiz-mastery** |
|
||||
| 从文件提取题目 | **quiz-mastery** |
|
||||
| 评分、掌握度追踪、艾宾浩斯安排 | **quiz-mastery** |
|
||||
| 把题目做成网页给用户在浏览器练 | **quiz-html**(本 skill) |
|
||||
| 学习计划、长期跟进 | **study-buddy** |
|
||||
|
||||
## 失败处理
|
||||
|
||||
- `exit 1`:参数错误 / 文件不存在 / 模板缺失 → 报错给用户,让用户检查路径
|
||||
- `exit 2`:JSON 格式问题 / 题目数据非法 → 告诉用户哪几题被跳过,提示检查字段
|
||||
- 部分题被 skip 但有合法题:仍会成功生成,但 stderr 会列出被跳过的题,需要在回复里告知用户「跳过了 N 题,原因 XXX」
|
||||
1349
skills/quiz-html/examples/demo.html
Executable file
1349
skills/quiz-html/examples/demo.html
Executable file
File diff suppressed because it is too large
Load Diff
220
skills/quiz-html/scripts/build_quiz_html.py
Executable file
220
skills/quiz-html/scripts/build_quiz_html.py
Executable file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
build_quiz_html.py - 把题目 JSON 注入到模板 HTML,生成可独立运行的练习网页。
|
||||
|
||||
用法:
|
||||
python3 build_quiz_html.py <questions_json> [options]
|
||||
|
||||
参数:
|
||||
questions_json 题目 JSON 文件路径(必填)
|
||||
格式:题目数组,每题字段见下方"题目字段"
|
||||
|
||||
选项:
|
||||
--output <path> 输出 HTML 路径(默认:./quiz_<timestamp>.html)
|
||||
--title <str> 页面标题(默认:"题库练习")
|
||||
--subtitle <str> 副标题(默认根据题量自动生成)
|
||||
--id <str> 题库标识 ID(用于 localStorage 隔离,默认时间戳)
|
||||
--open 生成后自动用浏览器打开
|
||||
|
||||
题目字段(来自 quiz-mastery 的标准 JSON 格式):
|
||||
type single_choice | true_false | fill_blank | short_answer
|
||||
prompt 题干(必填)
|
||||
options 选项数组,仅 single_choice 用,格式 ["A. xxx", "B. yyy", ...]
|
||||
answer 标准答案
|
||||
explanation 解析(推荐填,K12 学生很需要)
|
||||
knowledge_point 知识点名(用于侧边栏分组)
|
||||
category 分类路径(推荐用"学科 / 子模块",如"物理 / 电学")
|
||||
level 难度 1-3(可选)
|
||||
memory_tip 记忆口诀(可选,会用橙色卡片高亮显示)
|
||||
|
||||
退出码:
|
||||
0 成功
|
||||
1 参数错误 / 文件不存在
|
||||
2 JSON 解析失败 / 数据格式不对
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SKILL_DIR = Path(__file__).resolve().parent.parent
|
||||
TEMPLATE_PATH = SKILL_DIR / "templates" / "quiz_template.html"
|
||||
|
||||
REQUIRED_FIELDS = {"type", "prompt", "answer"}
|
||||
VALID_TYPES = {"single_choice", "true_false", "fill_blank", "short_answer"}
|
||||
|
||||
|
||||
def load_questions(path: Path) -> list[dict[str, Any]]:
|
||||
"""加载题目 JSON,做基本格式校验。"""
|
||||
try:
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
except OSError as e:
|
||||
print(f"❌ 无法读取文件:{path} - {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ JSON 解析失败:{e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
if not isinstance(data, list):
|
||||
print(f"❌ 题目必须是数组(list),得到 {type(data).__name__}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
if not data:
|
||||
print("❌ 题目数组为空", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
cleaned: list[dict[str, Any]] = []
|
||||
for i, q in enumerate(data, 1):
|
||||
if not isinstance(q, dict):
|
||||
print(f"⚠️ 第 {i} 题不是对象,已跳过", file=sys.stderr)
|
||||
continue
|
||||
miss = REQUIRED_FIELDS - set(q.keys())
|
||||
if miss:
|
||||
# 容忍 quiz-mastery 输出里 prompt 写成 question 的旧字段
|
||||
if "prompt" in miss and "question" in q:
|
||||
q["prompt"] = q["question"]
|
||||
miss.discard("prompt")
|
||||
if miss:
|
||||
print(f"⚠️ 第 {i} 题缺字段 {miss},已跳过", file=sys.stderr)
|
||||
continue
|
||||
if q.get("type") not in VALID_TYPES:
|
||||
print(f"⚠️ 第 {i} 题 type 非法:{q.get('type')}(应为 {VALID_TYPES}),已跳过", file=sys.stderr)
|
||||
continue
|
||||
# 单选题校验:options 必须存在
|
||||
if q["type"] == "single_choice" and not q.get("options"):
|
||||
print(f"⚠️ 第 {i} 题(选择题)缺 options,已跳过", file=sys.stderr)
|
||||
continue
|
||||
cleaned.append(q)
|
||||
|
||||
if not cleaned:
|
||||
print("❌ 没有任何合法题目", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
def auto_subtitle(questions: list[dict[str, Any]]) -> str:
|
||||
"""根据题目自动生成副标题:题量 + 涉及分类。"""
|
||||
cats = sorted({(q.get("category") or "").strip() for q in questions if q.get("category")})
|
||||
type_count: dict[str, int] = {}
|
||||
for q in questions:
|
||||
t = q.get("type", "?")
|
||||
type_count[t] = type_count.get(t, 0) + 1
|
||||
type_names = {
|
||||
"single_choice": "选择",
|
||||
"true_false": "判断",
|
||||
"fill_blank": "填空",
|
||||
"short_answer": "简答",
|
||||
}
|
||||
breakdown = " · ".join(f"{type_names.get(t, t)}×{n}" for t, n in type_count.items())
|
||||
parts = [f"共 {len(questions)} 题"]
|
||||
if breakdown:
|
||||
parts.append(breakdown)
|
||||
if cats:
|
||||
# 提取顶级学科(按 "/" 拆)
|
||||
top_cats = sorted({c.split("/")[0].strip() for c in cats if c})
|
||||
if top_cats:
|
||||
parts.append("、".join(top_cats))
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
def render(questions: list[dict[str, Any]], title: str, subtitle: str, qid: str) -> str:
|
||||
"""把题目数据注入模板。"""
|
||||
if not TEMPLATE_PATH.exists():
|
||||
print(f"❌ 模板文件不存在:{TEMPLATE_PATH}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
html = TEMPLATE_PATH.read_text(encoding="utf-8")
|
||||
|
||||
meta = {"id": qid, "title": title, "subtitle": subtitle}
|
||||
quiz_json = json.dumps(questions, ensure_ascii=False)
|
||||
meta_json = json.dumps(meta, ensure_ascii=False)
|
||||
|
||||
# 顺序替换,避免 title/subtitle 内含 {{...}} 时被二次解释
|
||||
html = html.replace("{{TITLE}}", _safe(title))
|
||||
html = html.replace("{{SUBTITLE}}", _safe(subtitle))
|
||||
html = html.replace("{{QUIZ_DATA}}", quiz_json)
|
||||
html = html.replace("{{META}}", meta_json)
|
||||
return html
|
||||
|
||||
|
||||
def _safe(s: str) -> str:
|
||||
"""HTML 安全转义(仅用于 title/subtitle 这种纯文本占位符)。"""
|
||||
return (
|
||||
s.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace('"', """)
|
||||
)
|
||||
|
||||
|
||||
def _slug(s: str) -> str:
|
||||
"""生成文件名安全的 slug。"""
|
||||
s = re.sub(r"[^\w\u4e00-\u9fa5-]+", "_", s).strip("_")
|
||||
return s[:40] or "quiz"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
description="把题目 JSON 注入模板,生成可独立运行的练习网页",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
ap.add_argument("questions", type=Path, help="题目 JSON 文件路径")
|
||||
ap.add_argument("--output", "-o", type=Path, help="输出 HTML 路径")
|
||||
ap.add_argument("--title", default="📚 题库练习", help="页面标题")
|
||||
ap.add_argument("--subtitle", default=None, help="副标题(默认自动生成)")
|
||||
ap.add_argument("--id", dest="qid", default=None, help="题库 ID(用于 localStorage 隔离)")
|
||||
ap.add_argument("--open", action="store_true", help="生成后自动用浏览器打开")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not args.questions.exists():
|
||||
print(f"❌ 文件不存在:{args.questions}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
questions = load_questions(args.questions)
|
||||
title = args.title
|
||||
subtitle = args.subtitle or auto_subtitle(questions)
|
||||
qid = args.qid or f"q_{int(time.time())}"
|
||||
|
||||
html = render(questions, title, subtitle, qid)
|
||||
|
||||
# 输出路径:默认到题目文件同目录
|
||||
if args.output:
|
||||
output = args.output
|
||||
else:
|
||||
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||
output = args.questions.parent / f"quiz_{_slug(title)}_{ts}.html"
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(html, encoding="utf-8")
|
||||
|
||||
# 输出统一 JSON,方便 agent 解析
|
||||
result = {
|
||||
"success": True,
|
||||
"output_path": str(output.resolve()),
|
||||
"question_count": len(questions),
|
||||
"title": title,
|
||||
"subtitle": subtitle,
|
||||
"id": qid,
|
||||
"size_bytes": output.stat().st_size,
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
if args.open:
|
||||
webbrowser.open(f"file://{output.resolve()}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
57
skills/quiz-html/skill.yaml
Executable file
57
skills/quiz-html/skill.yaml
Executable file
@@ -0,0 +1,57 @@
|
||||
name: quiz_html
|
||||
version: 0.1.0
|
||||
description: 把题目 JSON 注入模板生成单文件 HTML 练习网页
|
||||
|
||||
entrypoints:
|
||||
build: scripts/build_quiz_html.py
|
||||
|
||||
features:
|
||||
- html_quiz_generation
|
||||
- multi_question_type
|
||||
- category_filter
|
||||
- state_filter
|
||||
- exam_mode
|
||||
- keyboard_shortcuts
|
||||
- dark_mode
|
||||
- localstorage_persistence
|
||||
- mobile_responsive
|
||||
|
||||
depends_on:
|
||||
- quiz-mastery # 上游:题目数据来源
|
||||
|
||||
triggers:
|
||||
# 直接关键词
|
||||
- "生成网页"
|
||||
- "做成网页"
|
||||
- "做个HTML"
|
||||
- "网页版"
|
||||
- "在浏览器练"
|
||||
- "导出HTML"
|
||||
- "在网页做练习"
|
||||
|
||||
# quiz-mastery 完成后的链式触发
|
||||
- quiz_generated # 出题完成事件
|
||||
- quiz_imported # 题目导入完成事件
|
||||
|
||||
io:
|
||||
input:
|
||||
type: file
|
||||
format: json
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
required: [type, prompt, answer]
|
||||
properties:
|
||||
type: {enum: [single_choice, true_false, fill_blank, short_answer]}
|
||||
prompt: {type: string}
|
||||
options: {type: array, items: {type: string}}
|
||||
answer: {type: string}
|
||||
explanation: {type: string}
|
||||
knowledge_point: {type: string}
|
||||
category: {type: string}
|
||||
level: {type: integer, minimum: 1, maximum: 3}
|
||||
memory_tip: {type: string}
|
||||
output:
|
||||
type: file
|
||||
format: html
|
||||
self_contained: true # 单文件,无外部依赖
|
||||
1418
skills/quiz-html/templates/quiz_template.html
Executable file
1418
skills/quiz-html/templates/quiz_template.html
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user