| name | delf-exam |
| description | TEF/TCF 模拟套卷组卷 + 四维评分报告(CO/CE/EE/EO 雷达图,HTML 可截图分享)。触发:/delf-exam,或"模考"/"模拟套卷"/"出一套卷"/"测一下我现在的水平"。组卷复用 delf-vocab 出题引擎 + delf-writing/delf-oral 评分逻辑,依赖前面所有模块产出的薄弱点数据按权重组卷。 |
| allowed-tools | Read, Write, Bash |
DELF 模拟套卷 · 四维评分报告
Overview
整合四项官方能力(CO 听力 / CE 阅读 / EE 写作 / EO 口语)的"文字化模考",
产出可截图传播的 HTML 成绩单(雷达图 + 四维表现卡片)。本模块不是从零造题——
它是前面三个模块能力的"组装层":
| 套卷模块 | 复用来源 | 呈现形式 |
|---|
| CO(听力策略模拟) | delf-vocab 子能力 C 的"听力机经"题型(情境选择题,文字呈现) | 文字版:呈现对话摘要/独白文字,考查理解与选项判断策略 |
| CE(阅读) | delf-vocab 子能力 D 的阅读理解题型(T/F/NG / 主旨概括) | 正常文字呈现 |
| EE(写作) | delf-writing 的批改模式四维评分逻辑 | 用户在 HTML 中看题,回对话框提交 |
| EO(口语策略模拟) | delf-oral 的角色扮演评价逻辑(表达清晰度优先) | 文字版:用户以文字作答,考查口语组织逻辑与表达策略 |
⚠️ CO/EO 诚实定位说明:本系统无法播放音频或录音。CO 是"听力策略训练"——
通过文字呈现对话场景,考查"在真实听力中要注意什么"的判断能力;EO 是"口语表达训练"——
通过文字作答,考查"如何组织观点、论证清晰"的核心能力。与真实考试的听说形式不同,
但对策略与表达逻辑的训练是真实有效的。真实听力请配合音频材料;真实口语请配合语音工具。
依赖前面所有模块沉淀的 mistakes-db/mastery-db 数据(BLUEPRINT.md 开发顺序第 6 步)。
人设语气统一从 [[delf-soul]] 取用;批改输出统一走 [[delf-feedback-template]];
四维评分锚点规则与 [[delf-scoring-tables]] 的"待核实"红线与 delf-writing 完全一致
——本模块绝不可单独编造一套换算标准。
When to Use
- 用户输入
/delf-exam
- 用户说"模考"/"模拟套卷"/"出一套卷"/"测一下我现在的水平"
delf-main 路由表 4 号选项命中
- 用户已在其他模块练习一段时间后主动想"全面体检"
不应从模糊意图触发。
Non-Negotiables(先于步骤说明,因为风险点集中)
- 总分校验铁律:组卷时各部分分值分配必须有明确依据并在
<thinking> 中自检合计,
不允许出现"凑数"式分配
- HTML 自包含铁律:所有 HTML 文件(试卷 + 报告)由 Write 工具内联生成,
CSS/JS 全部嵌入,不依赖任何外部模板文件(包括已弃用的
templates/base_report.html)
- 四维换算必须诚实:[[delf-scoring-tables]] 中标记
[待核实] 的官方分数区间,
报告中只能呈现"相对表现 + CECRL 定性区间",不得编造精确换算数值
- 总题量与时长有上限:CO/CE 各部分题量参照
delf-vocab 出题引擎的默认规模
(单次出题不超过约 10 题/部分),整体模考时长向用户声明在 30-50 分钟区间,
超出后允许用户选择"先交这部分,剩余部分下次再约"
- CO/EO 必须带文字模拟版说明:在试卷 HTML 和报告 HTML 中都保留诚实定位脚注
Instructions
Step 0:加载上下文 + 扫描用户材料
第一步:读数据库
python3 -c "
import sys, os, runpy
for _p in ['.claude/hooks', 'hooks']:
_s = os.path.join(os.getcwd(), _p, 'read-db.py')
if os.path.isfile(_s):
sys.path.insert(0, os.path.dirname(_s)); runpy.run_path(_s, run_name='__main__'); break
"
从输出中提取 data_dir 字段,推导路径变量(后文所有 outputs/xxx / materials/xxx 均指这里的绝对路径,Read/Write 工具调用时必须使用绝对路径):
project_root = data_dir 去掉最后一级(即 delf-data 的父目录)
outputs_dir = {project_root}/outputs
materials_dir = {project_root}/materials
需要:mastery_db(四项能力当前掌握度,用于决定组卷难度配比与薄弱点加权)、
mistakes_db(高频错误模式,用于针对性安插"陷阱题")、learner_profile(目标考试类型
TEF/TCF——决定整体题型框架与后续换算口径)。
第二步:扫描 {materials_dir} 文件夹(与读库并行,不阻塞)
python3 -c "
import sys, os
sys.stdout.reconfigure(encoding='utf-8')
for _p in ['.claude/hooks', 'hooks']:
_h = os.path.join(os.getcwd(), _p)
if os.path.isdir(_h):
sys.path.insert(0, _h); break
from delf_paths import data_dir
p = os.path.join(os.path.dirname(data_dir()), 'materials')
if not os.path.isdir(p):
print('EMPTY')
else:
files = [f for f in os.listdir(p) if not f.startswith('.')]
print('\n'.join(files) if files else 'EMPTY')
"
按文件名自动分类,无需用户说明:
| 文件名信号 | 分类 | 出卷角色 |
|---|
| 含"真题"/"试卷"/"历年"/"卷" | 参考试卷 | 仿照题型结构和难度出题(权重×2) |
| 含"词汇"/"语法"/"笔记"/"资料"/"阅读"/"听力" | 知识资料 | 提取考点话题,AI 按这些话题出题 |
| materials/ 为空或无相关文件 | 无资料 | 纯 AI 出卷(下方 Step 1 说明) |
首次进入本模块时,第一条回复自然带一句「输入 888 可以直接回主菜单,0 返回上一级」的提示。
用户输入 888:立即清空断点缓存(current_module: null),直接输出 Module 0 简版菜单,无需确认。
用户输入 0:出卷前(Step 1 确认阶段)直接回主菜单;出卷后(Step 2 开始后)触发"暂停保存"——保留当前断点并显示简版菜单,下次可续接,不等同于 888(888 才是完全中断当前模考)。
Step 1:组卷规划(强制 <thinking>)
出卷来源遵循统一规则:用户 materials/ 资料是主轴,AI 自身知识是补充;没有资料时 AI 全权出卷。
这不是两个模式——是同一条管道,资料决定优先级和风格,AI 填补所有空缺。
<thinking>
=== 出卷来源判断 ===
materials/ 扫描结果:
参考试卷:[文件名 / 无]
知识资料:[文件名列表 / 无]
出卷路径:[路径A 资料驱动 / 路径B 纯AI]
--- 路径A:有 materials/ 资料时 ---
1. 读取参考试卷(若有):提取题型结构、题量分布、难度层次、话题风格
→ 这套卷的"骨架"参照参考试卷复刻,不复制原题
2. 读取知识资料(若有):提取高频话题、考点、词汇领域
→ 这些话题优先安插进 CO/CE/EE/EO 各部分
3. AI 补全策略:
- 参考试卷覆盖的题型已有骨架,AI 在该骨架内出全新题目
- 知识资料未覆盖的考点,AI 按 mistakes_db 薄弱点自行补充
- learner_profile.current_level 决定整体难度,略高半档
4. 四项题量配比(基于参考试卷结构或薄弱点加权):
CO ___题 / CE ___题 / EE ___篇 / EO ___题
--- 路径B:materials/ 为空时 ---
1. 目标考试:TEF / TCF → 决定四项呈现框架与官方量表口径
2. 四项配比:CO ___题 / CE ___题 / EE ___篇 / EO ___题
依据:mastery_db 中 mastery_level 较低的项加大权重,
并从 mistakes_db 高频 pattern_id 中挑选对应场景/考点优先安插
3. 话题与考点:AI 按 learner_profile 目标考试的标准话题库(职场/社会/时事/文化)
结合 mistakes_db 具体错误模式选取,不局限于 content/ 库,AI 知识全量可用
=== 通用规划 ===
难度分布:参照 learner_profile.current_level,整体略高半档("跳一跳够得着")
各部分预计时长:CO ___min / CE ___min / EE ___min / EO ___min,合计 ___min
(超出 50min 时与用户确认是否分两次完成)
session_id = <computed.next_session_id>(来自 read-db.py 输出,格式 session-NNN)
</thinking>
组卷规划完成后,输出一句简短告知(不展示内部计划细节,但若检测到用户资料则提一句):
(有资料时)好,我读了你放在 materials/ 的资料,参照那套题的风格给你出了一套模拟卷。
(无资料时)好,根据你的练习记录,我出了一套针对你薄弱点的模拟卷。
整套预计 {N} 分钟,按 CO → CE → EE → EO 顺序进行,每完成一部分再进入下一部分。
接下来我会生成 HTML 试卷文件,用浏览器打开作答,做完每部分把答案粘回对话框。
准备好了吗?回复任意内容开始出卷。
Step 2:生成 HTML 试卷
收到用户"开始"信号后,使用 Write 工具生成完整 HTML 试卷文件。
关键原则:
- HTML 文件完全自包含(CSS/JS 全部内联),不依赖任何外部文件
- 四部分全部写入同一个 HTML 文件,用
<section> 分隔,每部分有清晰的标题和说明
- CO/EO 带诚实的"文字模拟版"标注
- 答案区域用
<textarea> 让用户填写,底部有"复制所有答案"按钮
- 文件名格式:
outputs/delf-mock-exam-{session_id}.html
- 题目内容由 AI 当场生成,不复制 materials/ 原文;materials/ 资料决定题目的话题、风格和骨架,AI 出全新题目
HTML 试卷模板结构(Write 时内联完整 HTML,不引用外部文件):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DELF 模拟套卷 · {session_id}</title>
<style>
:root { --ink: #2d2a32; --paper: #fdfbf7; --accent: #6c63ff; --warn: #f0ad4e; --border: #e4e0f5; }
* { box-sizing: border-box; }
body { margin: 0; font-family: "PingFang SC","Microsoft YaHei",sans-serif;
background: #f3f0ff; color: var(--ink); padding: 24px; }
.page { max-width: 760px; margin: 0 auto; }
.header-card { background: var(--paper); border-radius: 16px; padding: 28px 32px;
box-shadow: 0 6px 24px rgba(108,99,255,.12); margin-bottom: 20px; }
h1 { font-size: 22px; font-weight: 800; margin: 0 0 6px; }
.meta { font-size: 13px; color: #8b8694; margin: 0 0 12px; }
.note-box { background: #fff8e1; border-left: 3px solid var(--warn);
border-radius: 6px; padding: 10px 14px; font-size: 13px;
line-height: 1.6; margin-top: 8px; }
.section { background: var(--paper); border-radius: 16px; padding: 24px 28px;
box-shadow: 0 4px 16px rgba(108,99,255,.08); margin-bottom: 20px; }
.section-header { display: flex; align-items: center; gap: 10px;
border-bottom: 2px solid var(--border); padding-bottom: 12px; margin-bottom: 18px; }
.section-tag { background: var(--accent); color: #fff; border-radius: 8px;
padding: 3px 12px; font-size: 13px; font-weight: 700; }
.section-tag.sim { background: var(--warn); }
h2 { font-size: 17px; font-weight: 700; margin: 0; }
.sim-note { font-size: 12px; color: #b3aebd; font-style: italic; margin-top: 2px; }
.passage { background: #f8f6ff; border-radius: 8px; padding: 14px 16px;
font-size: 14px; line-height: 1.7; margin: 12px 0; border-left: 3px solid var(--accent); }
.q-block { margin: 14px 0 8px; }
.q-num { font-weight: 700; color: var(--accent); font-size: 14px; }
.q-text { font-size: 15px; margin: 4px 0 8px; line-height: 1.5; }
.options label { display: block; padding: 6px 10px; border-radius: 6px;
cursor: pointer; font-size: 14px; margin: 3px 0; }
.options label:hover { background: #f3f0ff; }
.options input[type=radio] { margin-right: 8px; }
textarea { width: 100%; border: 1px solid var(--border); border-radius: 8px;
padding: 10px 12px; font-size: 14px; font-family: inherit;
resize: vertical; min-height: 100px; margin-top: 6px; }
textarea:focus { outline: none; border-color: var(--accent); }
.word-count { font-size: 12px; color: #b3aebd; text-align: right; margin-top: 4px; }
.submit-area { text-align: center; margin: 28px 0 8px; }
.btn-copy { background: var(--accent); color: #fff; border: none; border-radius: 10px;
padding: 12px 32px; font-size: 15px; font-weight: 700; cursor: pointer; }
.btn-copy:hover { background: #5a52e0; }
.footnote { font-size: 11px; color: #c0bbcc; text-align: center; margin-top: 16px; }
@media print { .submit-area { display: none; } }
</style>
</head>
<body>
<div class="page">
<div class="header-card">
<h1>📝 DELF 模拟套卷</h1>
<p class="meta">{TEF/TCF} · {date} · 预计 {N} 分钟 · 会话 {session_id}</p>
<div class="note-box">
💡 做完每个部分,把答案复制粘贴回 Claude 对话框,我来批改并生成成绩报告。<br>
CO / EO 为<strong>文字模拟版</strong>——训练理解策略与表达逻辑,非真实听说形式。
</div>
</div>
<div class="section" id="co">
<div class="section-header">
<span class="section-tag sim">CO 文字版</span>
<div>
<h2>听力理解策略模拟(文字版)</h2>
<div class="sim-note">⚠️ 文字模拟版 · 真实听力请配合音频材料</div>
</div>
</div>
{CO_QUESTIONS_HTML}
</div>
<div class="section" id="ce">
<div class="section-header">
<span class="section-tag">CE</span>
<div><h2>阅读理解</h2></div>
</div>
{CE_QUESTIONS_HTML}
</div>
<div class="section" id="ee">
<div class="section-header">
<span class="section-tag">EE</span>
<div><h2>书面表达</h2></div>
</div>
{EE_QUESTIONS_HTML}
<textarea id="ee-answer" placeholder="在这里写你的作文…" oninput="updateCount(this)"></textarea>
<div class="word-count" id="ee-count">0 字</div>
</div>
<div class="section" id="eo">
<div class="section-header">
<span class="section-tag sim">EO 文字版</span>
<div>
<h2>口语表达模拟(文字版)</h2>
<div class="sim-note">⚠️ 文字模拟版 · 真实口语请配合语音工具</div>
</div>
</div>
{EO_QUESTIONS_HTML}
<textarea id="eo-answer" placeholder="用文字写出你的口语作答…" oninput="updateCount(this)"></textarea>
<div class="word-count" id="eo-count">0 字</div>
</div>
<div class="submit-area">
<button class="btn-copy" onclick="copyAnswers()">📋 复制所有答案 → 粘贴回对话框</button>
</div>
<p class="footnote">本试卷为 Claude Code DELF 备考系统生成 · 仅供个人学习使用</p>
</div>
<script>
function updateCount(el) {
var id = el.id.replace('-answer','-count');
var words = el.value.trim() ? el.value.trim().split(/\s+/).length : 0;
document.getElementById(id).textContent = words + ' mots';
}
function copyAnswers() {
var parts = [];
['co','ce'].forEach(function(id) {
var sec = document.getElementById(id);
if (!sec) return;
var radios = sec.querySelectorAll('input[type=radio]:checked');
var ans = [];
radios.forEach(function(r) { ans.push(r.name + ': ' + r.value); });
if (ans.length) parts.push('[' + id.toUpperCase() + '] ' + ans.join(', '));
});
['ee','eo'].forEach(function(id) {
var ta = document.getElementById(id+'-answer');
if (ta && ta.value.trim()) parts.push('[' + id.toUpperCase() + ']\n' + ta.value.trim());
});
var text = parts.join('\n\n');
if (!text) { alert('请先填写答案再复制!'); return; }
navigator.clipboard.writeText(text).then(function() {
alert('已复制!请粘贴回 Claude 对话框。');
}).catch(function() {
prompt('请手动复制以下内容:', text);
});
}
</script>
</body>
</html>
生成说明:
{CO_QUESTIONS_HTML} / {CE_QUESTIONS_HTML} / {EE_QUESTIONS_HTML} / {EO_QUESTIONS_HTML}
均为 Claude 按组卷规划内联生成的真实题目 HTML 片段,不是占位符——Write 时直接嵌入完整题目
- CO 题:每题一个
.q-block,含情境描述(<div class="passage">)+ 选项(.options,单选 radio)
- CE 题:同上,含阅读篇章 + 判断/选择题
- EE 题:题目说明文字 +
<textarea> 作文区(已在模板中)
- EO 题:话题说明 + 提示要点 +
<textarea> 文字作答区(已在模板中)
同步写入参考答案文件(与试卷同步完成,Step 3 批改时优先从此文件读取,不依赖记忆):
使用 Write 工具生成 outputs/delf-mock-exam-{session_id}-key.json,按组卷规划填入各题正确答案:
{
"session_id": "<session_id>",
"co_answers": { "q1": "<A/B/C/D>", "q2": "<A/B/C/D>" },
"ce_answers": { "q1": "<Vrai/Faux/On ne sait pas>", "q2": "..." },
"ee_rubric": "<EE 评分要点:任务类型、必含信息点、预期 CECRL 区间>",
"eo_rubric": "<EO 评分要点:话题框架、论点层次、预期 CECRL 区间>"
}
Write 完成后输出:
📄 outputs/delf-mock-exam-{session_id}.html 已生成!
1. 用浏览器打开这个文件
2. 完成四个部分的作答
3. 点击底部「复制所有答案」按钮,把答案粘贴回这里
4. 我来批改并生成成绩单
---
回复:1 继续练习 0 上一级菜单 888 主菜单
Step 3:接收答案 + 统一评分(四维评分报告生成前的核心计算)
用户粘回答案后,先 Read outputs/delf-mock-exam-{session_id}-key.json 取参考答案,再在 <thinking> 中进行评分计算(不依赖上下文记忆,防止长会话评分偏差):
<thinking>
CO 评分:
- 各题答案核对(对照组卷规划中的标准答案/最优判断)
- 正确率 = M/N,归入相对表现区间
- 陷阱命中情况:mistakes_db 高频陷阱命中几个
CE 评分:
- 同上
EE 评分(CECRL 定性):
- 任务完成度:A2/B1/B2 区间 + 依据
- 连贯衔接:...
- 词汇运用:...
- 语法准确:...
- 综合 CECRL 区间估计
EO 评分(CECRL 定性):
- 观点是否清晰:...
- 论证展开:...
- 词汇与句型多样性:...
- 综合 CECRL 区间估计
四维相对表现值(0-1,用于雷达图):
CO: 正确率
CE: 正确率
EE: CECRL 区间中点估值(B1 中段 ≈ 0.5,B1 偏上到 B2 ≈ 0.65)
EO: 同 EE 映射逻辑
→ 此映射仅供可视化排布,不对外呈现为官方分数
</thinking>
评分口径统一:CO/CE 用正确率,EE/EO 用 CECRL 定性区间,最终统一折算为相对表现值——
不得四项各用一套互不可比的标准,也不得编造官方精确换算数值。
Step 4:批改与详细反馈
- CO/CE:逐题简要点评(对的一句话带过,错的标注"陷阱类型",呼应
delf-vocab 子能力 D 的反馈重点)
- EE/EO:分别走 [[delf-feedback-template]] 与
delf-writing/delf-oral 各自的详细反馈格式,
含改写对比 / 更地道说法
批改完成后告知用户:
批改完成!接下来生成你的成绩报告(HTML 雷达图)……
---
回复:1 继续练习 0 上一级菜单 888 主菜单
Step 5:生成 HTML 成绩报告
使用 Write 工具生成完整自包含 HTML 成绩报告。文件名:outputs/delf-mock-report-{session_id}.html
报告数据准备(在 <thinking> 中完成,实际写入 HTML 时内联进 <script> 块):
var reportData = {
title: "DELF 模考成绩单 · {TEF/TCF}",
subtitle: "{date} · 第 {N} 次模考 · 用时约 {duration} 分钟",
radar: [
{ axis: "CO", value: 0.00 },
{ axis: "CE", value: 0.00 },
{ axis: "EE", value: 0.00 },
{ axis: "EO", value: 0.00 }
],
skills: [
{ name: "CO 听力策略模拟", level: "warn", label: "B1 区间",
note: "文字版 · 具体陷阱描述", sim: true },
{ name: "CE 阅读理解", level: "good", label: "B1 偏上", note: "..." },
{ name: "EE 书面表达", level: "warn", label: "B1 中段", note: "..." },
{ name: "EO 口语表达模拟", level: "warn", label: "B1 区间",
note: "文字版 · 具体表现描述", sim: true }
],
takeaway: "...",
footnote: "本报告为基于练习数据的相对表现参考;官方分数换算请以最新官方标准为准。CO/EO 为文字模拟版评估,真实听说能力请配合音频/语音工具训练。"
};
level 取值:good(相对强项)/ warn(接近目标但有差距)/ bad(明显薄弱)——
依据该项相对表现值与其余三项的对比,以及与 learner_profile.target_level 的差距判断,
不是固定阈值,要结合个体情况给出有依据的相对判断。
HTML 报告完整模板(Write 时内联完整 HTML,CSS/JS 全部嵌入,不依赖外部文件):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DELF 模考成绩单</title>
<style>
:root { --ink:#2d2a32; --paper:#fdfbf7; --accent:#6c63ff;
--good:#4caf81; --warn:#f0ad4e; --bad:#e0625a; }
* { box-sizing:border-box; }
body { margin:0; font-family:"PingFang SC","Microsoft YaHei",-apple-system,sans-serif;
background:linear-gradient(180deg,#fdfbf7 0%,#f3f0ff 100%);
color:var(--ink); padding:24px; }
.card { max-width:720px; margin:0 auto 20px; background:var(--paper);
border-radius:16px; padding:24px; box-shadow:0 6px 24px rgba(108,99,255,.12); }
.title { font-size:22px; font-weight:700; margin:0 0 4px; }
.subtitle { font-size:14px; color:#8b8694; margin:0 0 18px; }
.radar-wrap { display:flex; justify-content:center; margin:12px 0 8px; }
.skill-row { display:flex; align-items:center; justify-content:space-between;
padding:10px 0; border-bottom:1px solid #ece8f5; font-size:15px; }
.skill-row:last-child { border-bottom:none; }
.skill-name { font-weight:600; }
.skill-note { font-size:13px; color:#8b8694; margin-top:2px; }
.sim-tag { font-size:11px; color:#f0ad4e; margin-left:6px; }
.badge { display:inline-block; padding:2px 10px; border-radius:999px;
font-size:12px; font-weight:600; color:#fff; }
.badge.good { background:var(--good); }
.badge.warn { background:var(--warn); }
.badge.bad { background:var(--bad); }
.takeaway { background:#f3f0ff; border-radius:12px; padding:14px 16px;
font-size:14px; line-height:1.6; margin-top:12px; }
.footnote { font-size:12px; color:#b3aebd; text-align:center; margin-top:8px; }
</style>
</head>
<body>
<div id="report-root"></div>
<script>
var reportData = {REPORT_DATA_JSON};
(function(){
var data = reportData;
var root = document.getElementById('report-root');
function el(tag,cls,html){ var n=document.createElement(tag);
if(cls) n.className=cls; if(html!==undefined) n.innerHTML=html; return n; }
function txt(tag,cls,str){ var n=document.createElement(tag);
if(cls) n.className=cls; if(str!==undefined) n.textContent=str; return n; }
function badgeClass(l){ return l==='good'?'badge good':l==='warn'?'badge warn':'badge bad'; }
var hdr=el('div','card');
hdr.appendChild(txt('p','title','📝 '+(data.title||'DELF 模考成绩单')));
hdr.appendChild(txt('p','subtitle',data.subtitle||''));
var rw=el('div','radar-wrap'); rw.appendChild(buildRadar(data.radar||[])); hdr.appendChild(rw);
root.appendChild(hdr);
var det=el('div','card');
det.appendChild(el('p','title','四维表现'));
(data.skills||[]).forEach(function(s){
var row=el('div','skill-row');
var left=el('div');
var nameEl=txt('div','skill-name',s.name);
if(s.sim) nameEl.innerHTML+=el('span','sim-tag','文字模拟版').outerHTML;
left.appendChild(nameEl);
if(s.note) left.appendChild(txt('div','skill-note',s.note));
row.appendChild(left);
row.appendChild(txt('span',badgeClass(s.level),s.label||''));
det.appendChild(row);
});
root.appendChild(det);
if(data.takeaway){
var tk=el('div','card');
tk.appendChild(el('p','title','💡 这次最值得带走的'));
tk.appendChild(txt('div','takeaway',data.takeaway));
root.appendChild(tk);
}
root.appendChild(txt('p','footnote',data.footnote||''));
function buildRadar(pts){
var size=260,cx=130,cy=130,r=92,n=pts.length||4;
var ns='http://www.w3.org/2000/svg';
var svg=document.createElementNS(ns,'svg');
svg.setAttribute('width',size); svg.setAttribute('height',size);
function pt(i,ratio){ var a=(Math.PI*2*i/n)-Math.PI/2;
return [cx+Math.cos(a)*r*ratio, cy+Math.sin(a)*r*ratio]; }
[.25,.5,.75,1].forEach(function(ratio){
var poly=document.createElementNS(ns,'polygon');
var coords=[]; for(var i=0;i<n;i++) coords.push(pt(i,ratio).join(','));
poly.setAttribute('points',coords.join(' '));
poly.setAttribute('fill','none'); poly.setAttribute('stroke','#e4e0f0');
svg.appendChild(poly);
});
pts.forEach(function(p,i){
var end=pt(i,1);
var line=document.createElementNS(ns,'line');
line.setAttribute('x1',cx); line.setAttribute('y1',cy);
line.setAttribute('x2',end[0]); line.setAttribute('y2',end[1]);
line.setAttribute('stroke','#e4e0f0'); svg.appendChild(line);
var lbl=pt(i,1.2);
var txt=document.createElementNS(ns,'text');
txt.setAttribute('x',lbl[0]); txt.setAttribute('y',lbl[1]);
txt.setAttribute('text-anchor','middle'); txt.setAttribute('font-size','13');
txt.setAttribute('fill','#6c63ff'); txt.setAttribute('font-weight','700');
txt.textContent=p.axis; svg.appendChild(txt);
});
var coords=pts.map(function(p,i){ return pt(i,Math.max(0,Math.min(1,p.value))); });
var poly=document.createElementNS(ns,'polygon');
poly.setAttribute('points',coords.map(function(c){return c.join(',');}).join(' '));
poly.setAttribute('fill','rgba(108,99,255,.28)');
poly.setAttribute('stroke','#6c63ff'); poly.setAttribute('stroke-width','2');
svg.appendChild(poly);
coords.forEach(function(c){
var dot=document.createElementNS(ns,'circle');
dot.setAttribute('cx',c[0]); dot.setAttribute('cy',c[1]);
dot.setAttribute('r',3); dot.setAttribute('fill','#6c63ff');
svg.appendChild(dot);
});
return svg;
}
})();
</script>
</body>
</html>
Write 工具使用说明:
- 上方模板中的
{REPORT_DATA_JSON} 替换为实际的 JSON 对象(直接内联,不是字符串)
sim: true 字段用于在报告中为 CO/EO 行显示"文字模拟版"小标签
- 整个 HTML 一次性 Write 到
outputs/delf-mock-report-{session_id}.html
- 不执行任何
cp 命令,不依赖 templates/ 目录
Write 完成后输出:
📊 outputs/delf-mock-report-{session_id}.html 已生成!
用浏览器打开查看四维雷达图成绩单,可以截图记录这次的状态。
---
回复:1 继续练习 0 上一级菜单 888 主菜单
数据写入
python3 -c "
import sys, os, runpy
for _p in ['.claude/hooks', 'hooks']:
_s = os.path.join(os.getcwd(), _p, 'update-db.py')
if os.path.isfile(_s):
sys.path.insert(0, os.path.dirname(_s)); runpy.run_path(_s, run_name='__main__'); break
" <<'EOF'
{
"session_id": "<computed.next_session_id>",
"date": "<today>",
"duration_minutes": <实际用时>,
"command_used": "/delf-exam",
"skills_practiced": ["comprehension_orale", "comprehension_ecrite", "expression_ecrite", "expression_orale"],
"skill_scores": {
"<co_sub_skill>": {"exercises": N, "correct": M, "time_minutes": T},
"<ce_sub_skill>": {"exercises": N, "correct": M, "time_minutes": T},
"<ee_sub_skill>": {"exercises": 1, "correct": <按四维相对表现折算>, "time_minutes": T},
"<eo_sub_skill>": {"exercises": 1, "correct": <按相对表现折算>, "time_minutes": T}
},
"errors": [ ... ],
"milestones": ["<若四项相对表现较上次模考有显著提升,记录一条里程碑>"],
"session_notes": "模考报告:<四项相对表现摘要>"
}
EOF
模考产出的薄弱点数据会反过来影响后续 delf-vocab 的选词优先级与 mastery_db,
形成"模考发现问题 → 专项模块补强 → 下次模考验证"的完整闭环。
断点缓存
模考流程较长,每完成一个部分就更新一次断点,避免用户中途离开后从头再来(完整结构见全局 Step 5 模板):
{
"current_module": "delf-exam",
"sub_state": { "stage": "已完成 CO/CE,待完成 EE/EO", "session_draft_id": "<session_id>" }
}
完成全部四部分并生成报告后清空 current_module。
Non-Negotiables(汇总)
- 组卷依据必须可追溯:四项配比与考点选择必须能说出"为什么这么配"(依据
mastery_db/mistakes_db),不是随机拼凑
- 评分口径统一:CO/CE 用正确率,EE/EO 用 CECRL 定性区间,最终统一折算为相对表现值——不得四项各用一套互不可比的标准
- 绝不编造官方分数换算:[[delf-scoring-tables]] 待核实部分一律只做相对表现描述
- HTML 完全自包含:所有 HTML 由 Write 工具内联生成,CSS/JS 嵌入,禁止
cp templates/ 命令,禁止依赖任何外部模板文件
- CO/EO 必须带文字模拟版说明:试卷 HTML 和报告 HTML 均保留诚实定位标注,不隐瞒与真实考试的差异
- 长流程必须分段保存断点,不能要求用户"一口气做完否则重来"