ZJUI-Learn 从 0 到 1 部署与教师使用手册(Windows 11 + WSL + Docker)¶
这是一份单文件总手册,目标是让你下次从零开始也能快速完成:
- 本地环境搭建
- 代码开发与测试
- 教师端题目录入与发布
- 题目三核心文件(
info.json、question.html、server.py)实操
1. 一次性准备(推荐方案)¶
推荐环境:
- 视窗11
- Docker Desktop(启用WSL2引擎)
- Ubuntu(WSL发行版)
- 代码放在WSL文件系统中(如
~/dev/ZJUI-Learn)
为什么:
- 比
/mnt/c/...更快,尤其是文件监听与测试阶段 - 路径/权限问题更少
- 开发稳定性更高
2. 从 0 开始部署(可直接照做)¶
2.1 安装与检查 Docker Desktop¶
- 安装 Docker 桌面
- 设置中确认:
Use the WSL 2 based engine已开启- Ubuntu 集成已开启
- PowerShell验证:
docker --version
docker info
crsr_570b323a1c63acf973655c8785e97ca5931f571c8d655531ad36d3e007abfeea
如果拉超时(Clash代理):
- Docker 桌面 ->
Settings->Resources->Proxies - 手动代理填写:
- HTTP:
http://host.docker.internal:7890 - HTTPS:
http://host.docker.internal:7890 - 无代理:
localhost,127.0.0.1,::1,host.docker.internal,.local
- HTTP:
- 应用并重启后执行:
docker pull prairielearn/prairielearn:latest
2.2 安装并进入Ubuntu(WSL)¶
wsl --install -d Ubuntu
wsl -l -v
进入:
wsl -d Ubuntu
2.3 克隆项目(直接在WSL推荐)¶
mkdir -p ~/dev
cd ~/dev
git clone https://gitee.com/hoshimiya/ZJUI-Learn.git
cd ZJUI-Learn
3. 启动本地开发环境¶
在 WSL 的仓库根目录执行:
docker run -it --rm -p 3000:3000 \
-w /PrairieLearn \
-v "$PWD:/PrairieLearn" \
prairielearn/prairielearn /bin/bash
进入容器后首次执行:
make deps
make dev
后续用:
pnpm dev
重启用:
rs
浏览器访问:
http://localhost:3000- 或
http://localhost:3000/pl
4. 日常开发最快流程¶
首次依赖安装后,后续推荐:
pnpm dev
如果改了代码页面不刷新:
- 终端输入
rs(重启nodemon) - 浏览器硬刷新
Ctrl+F5 - 检查是否编辑了当前页面真实使用的文件
5. 测试流程(开发态)¶
5.1 快速跑单测(示例)¶
pnpm test apps/prairielearn/src/tests/homepage.test.ts
5.2 跑主应用测试¶
cd /PrairieLearn
make test-prairielearn
说明:
make test-prairielearn会准备Postgres/Redis/s3rver等依赖后再跑测试- 本地慢机出现hook超时较常见,优先排查环境速度和I/O
5.3 将本地分支同步到 Gitee(以 ai-question 为例)¶
在仓库根目录执行:
# 1) 切换到目标分支
git checkout ai-question
# 2) 查看变更并提交
git status
git add .
git commit -m "feat: update ai-question branch"
# 3) 检查远程仓库
git remote -v
如果origin不是你的Gitee地址,则先设置:
# 方式 A:直接修改 origin
git remote set-url origin https://gitee.com/ < 你的用户名 > / < 你的仓库名 > .git
或:
# 方式 B:新增 gitee 远程(保留原 origin)
git remote add gitee https://gitee.com/ < 你的用户名 > / < 你的仓库名 > .git
首次推送:
# 使用 origin
git push -u origin ai-question
# 或使用 gitee 远程
git push -u gitee ai-question
后续同一分支推送可简化为:
git push
常见问题:
Authentication failed:改用Gitee个人访问令牌(PAT)或SSH密钥,不要用账号密码。src refspec ai-question does not match any:本地分支不存在,先git checkout -b ai-question。- 远程拒绝主动(非快进):先
git pull --rebase解决冲突后再git push。
6. 教师操作主线(课程使用)¶
建议按这个顺序:
- 进入课程管理(课程/课程实例)
- 在
Questions里建题与维护题库 - 在
Assessments中组卷和配置规则 - 预览(教师视角 + 学生视角)
- 发布后观察结果并迭代
7. 重点:题目三核心文件详细教程¶
这是教师和题目开发者最关键的部分。
7.1 文件关系图¶
questions/<QID>/
info.json # 题目元信息与策略开关
question.html # 题面与输入组件
server.py # 参数生成、解析、评分
7.2 info.json:题目“配置中心”¶
最小可用模板:
{
"uuid": "11111111-2222-3333-4444-555555555555",
"type": "v3",
"title": "欧姆定律计算",
"topic": "电路基础",
"tags": ["ohm", "intro"]
}
高频字段解释:
uuid:唯一ID,必须唯一type:新题固定写"v3"title:题目标题topic:主题主题分类tags:筛选标签singleVariant:true表示单变体(不随机)partialCredit:是否允许部分得分showCorrectAnswer:面板是否显示正确答案gradingMethod:Internal/External/Manual
常见坑:
- 改了
info.json后界面没变:本地开发模式下通常注明Load from disk - 误把
type写成旧值,导致行为不一致
7.3 question.html:题面和交互输入¶
模板示例:
<pl-question-panel>
已知电压 $V={{params.v}}\\,V$,电阻 $R={{params.r}}\\,\\Omega$,求电流 $I=V/R$。
</pl-question-panel>
<pl-number-input answers-name="i" label="$I=$" suffix="A"></pl-number-input>
<pl-submission-panel> {{feedback.i}} </pl-submission-panel>
必须遵守:
{{params.xxx}}必须在server.py的generate()赋值中- 所有输入组件的
answers-name必须唯一 - 优先使用官方元素(如
pl-number-input、pl-multiple-choice)
常见坑:
answers-name重复导致评分异常- 使用了
{{params.xxx}}但没有对应参数 - 只改文案不改评分逻辑,导致题面与评分不一致
7.4 server.py:问题行为核心¶
完整入门模板(生成 + 评分反馈):
import random
import math
import prairielearn as pl
def generate(data):
v = random.randint(5, 20)
r = random.choice([1, 2, 4, 5, 10])
data["params"]["v"] = v
data["params"]["r"] = r
data["correct_answers"]["i"] = v / r
def grade(data):
i_is_correct = math.isclose(data["partial_scores"]["i"]["score"], 1.0)
if not i_is_correct:
submitted = float(data["submitted_answers"]["i"])
if submitted > 0:
data["feedback"]["i"] = "方向正确,但请再检查除法计算。"
常用函数职责:
generate(data):生成参数与标准答案parse(data):做输入清洗/格式错误处理(任选)grade(data):卡通评分与反馈(任选)test(data):复杂题可补自动测试逻辑(任选)
评分建议:
- 浮点数不要用
==严格比较 - 若自定义
grade(),覆盖避免组件已判对的结果 - 反馈要可执行,告诉学生“错在哪里”
8. 题目实例库(可直接复用)¶
下面给你一组可直接改造的实例模板。每个实例都按三个文件组织,复制后修改uuid、题面和参数即可使用。
8.1 填空题(数字填空,自动判分)¶
info.json
{
"uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a001",
"type": "v3",
"title": "一次函数求值",
"topic": "代数",
"tags": ["fill-blank", "number"]
}
question.html
<pl-question-panel>
已知 $y=2x+3$,当 $x={{params.x}}$ 时,$y=$
<pl-integer-input answers-name="y" label=""></pl-integer-input>
</pl-question-panel>
server.py
import random
def generate(data):
x = random.randint(1, 10)
data["params"]["x"] = x
data["correct_answers"]["y"] = 2 * x + 3
8.2 填空题(文本填空,忽略大小写)¶
info.json
{
"uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a002",
"type": "v3",
"title": "术语拼写",
"topic": "计算机基础",
"tags": ["fill-blank", "string"]
}
question.html
<pl-question-panel>
请补全短语:Prairie
<pl-string-input
answers-name="term"
ignore-case="true"
remove-leading-trailing="true"
></pl-string-input>
</pl-question-panel>
server.py
def generate(data):
data["correct_answers"]["term"] = "Learn"
8.3 选择题(单选题)¶
info.json
{
"uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a003",
"type": "v3",
"title": "复杂度判断",
"topic": "算法",
"tags": ["single-choice"]
}
question.html
<pl-question-panel> 对于归并排序,其平均时间复杂度是? </pl-question-panel>
<pl-multiple-choice answers-name="mc" order="fixed">
<pl-answer correct="false">$O(n)$</pl-answer>
<pl-answer correct="true">$O(n\log n)$</pl-answer>
<pl-answer correct="false">$O(n^2)$</pl-answer>
<pl-answer correct="false">$O(\log n)$</pl-answer>
</pl-multiple-choice>
server.py
def generate(data):
# 单选题无需动态参数时可留空
pass
8.4 选择题(多选题,支持部分得分)¶
info.json
{
"uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a004",
"type": "v3",
"title": "Python 语法判断",
"topic": "编程基础",
"tags": ["multi-choice", "checkbox"],
"partialCredit": true
}
question.html
<pl-question-panel> 下面哪些是 Python 的合法数据类型?(可多选) </pl-question-panel>
<pl-checkbox answers-name="types" partial-credit="each-answer" order="fixed">
<pl-answer correct="true">int</pl-answer>
<pl-answer correct="true">dict</pl-answer>
<pl-answer correct="false">char</pl-answer>
<pl-answer correct="false">pointer</pl-answer>
</pl-checkbox>
server.py
def generate(data):
pass
8.5 文字题(简答题,关键词自动评分)¶
适合“短文本回答 + 自动反馈”的场景。
info.json
{
"uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a005",
"type": "v3",
"title": "HTTP 状态码解释",
"topic": "网络基础",
"tags": ["short-answer", "text"]
}
question.html
<pl-question-panel> 请用一句话解释 HTTP 404 的含义。 </pl-question-panel>
<pl-string-input
answers-name="desc"
multiline="true"
remove-leading-trailing="true"
placeholder="请输入你的解释"
></pl-string-input>
<pl-submission-panel> {{feedback.desc}} </pl-submission-panel>
server.py
def generate(data):
# 关键词匹配类题目通常不设置唯一标准答案,在 grade() 中评分
pass
def grade(data):
text = str(data["submitted_answers"]["desc"]).lower()
has_not_found = ("not found" in text) or ("不存在" in text)
has_resource = ("resource" in text) or ("资源" in text)
if has_not_found and has_resource:
data["partial_scores"]["desc"]["score"] = 1.0
data["feedback"]["desc"] = "解释完整。"
elif has_not_found or has_resource:
data["partial_scores"]["desc"]["score"] = 0.5
data["feedback"]["desc"] = "方向正确,可补充“请求资源不存在”的完整语义。"
else:
data["partial_scores"]["desc"]["score"] = 0.0
data["feedback"]["desc"] = "建议围绕“资源不存在 / Not Found”作答。"
8.6 文字题(主观题,人工评分)¶
适合“论述题/开放题”。核心是改为人工评分策略。
info.json
{
"uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a006",
"type": "v3",
"title": "算法设计思路说明",
"topic": "算法",
"tags": ["essay", "manual"],
"gradingMethod": "Manual"
}
question.html
<pl-question-panel>
请描述你设计该算法的核心思路、时间复杂度与正确性依据(200 字以内)。
</pl-question-panel>
<pl-string-input
answers-name="essay"
multiline="true"
remove-leading-trailing="true"
placeholder="请输入简答内容"
></pl-string-input>
server.py
def generate(data):
pass
8.7 一题多空(多个填空联合评分)¶
info.json
{
"uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a007",
"type": "v3",
"title": "基础代数多空",
"topic": "代数",
"tags": ["multi-blank"]
}
question.html
<pl-question-panel> 已知 $a={{params.a}}, b={{params.b}}$,请填写: </pl-question-panel>
<p>$a+b=$ <pl-integer-input answers-name="sum"></pl-integer-input></p>
<p>$a-b=$ <pl-integer-input answers-name="diff"></pl-integer-input></p>
server.py
import random
def generate(data):
a = random.randint(3, 10)
b = random.randint(1, 2)
data["params"]["a"] = a
data["params"]["b"] = b
data["correct_answers"]["sum"] = a + b
data["correct_answers"]["diff"] = a - b
9.从建题到发布:一条完整的SOP¶
- 在
Questions新建问题(填写QID/标题) - 补齐三文件:
info.json元信息question.html题面与输入server.py参数与评分
- 预览并反复生成新变体测试
- 检查错误输入时反馈是否合理
- 将题加入评估,配置分值/尝试次数/时间策略
- 发布前做一次学生视角走查
10. 你当前项目的 AI 出题入口(现状)¶
当前项目已具备:
- 问题解答页
Generate question from upload按钮 - 点击进入上传页面
- 可上传题目截图或文本草稿
- 调用Cursor API 生成
info.json、question.html和server.py - 自动下载包含问题完整目录的 ZIP 压缩包
- 保留ZJUI-Learn 企业版原有的AI题目草稿入口
使用前需在进程环境或仓库根目录的.env.ai中配置:
CURSOR_API_KEY=your_api_key
# 以下配置可选
CURSOR_API_BASE_URL=https://api.cursor.com
CURSOR_MODEL_ID=default
CURSOR_MODE=agent
生成的题目仍需人工校对和预览测试后再发布。
11. 高频问题速查¶
Q1: docker 结果缺少¶
- 首先确认 Docker Desktop 已启动
- 重启终端或IDE
docker --version检查 PATH 是否生效
Q2:No rule to make target 'deps'¶
- 大概率不在项目根目录
- 先看到
ls,确认当前目录能Makefile
Q3: 改了页面文案,浏览器看不到变化¶
rs重启开发进程- 浏览器硬刷新
- 检查是否命中当前条件渲染分支
Q4: 测试偶发超时¶
- 优先怀疑环境慢,不一定是业务代码错误
- 增加钩子/测试超时重新复测
Q5: 不跑完整服务,如何验证单个页面修改¶
为页面添加或更新一个接口的 Vitest 集成测试,然后只运行该测试文件:
pnpm test apps/prairielearn/src/tests/yourPageName.test.ts
优点:
- 不需要
make dev全量起服务流程 - 验证成本低,适合频繁改 UI 文案/按钮/可见性逻辑
12. 发布前检查清单(教师/开发共用)¶
- 题目
info.json字段完整(uuid、type、title、topic) question.html中answers-name全部唯一server.py生成了题面所需全部参数- 至少测试多个变体
- 错误答案反馈可读、可执行
- 评估配置(分值、时间、次数)已核对
13. 参考文档(仓库内)¶
docs/question/overview.mddocs/question/server.mddocs/question/template.mddocs/question/runtime.mddocs/instructor-guide/index.mddocs/assessment/docs/QUESTION_SETUP_GUIDE_ZH.mddocs/TEACHER_ADMIN_ALL_PAGES_MANUAL_ZH.mddocs/TEACHER_TRAINING_PLAYBOOK_ZH.md