跳转至

ZJUI-Learn 从 0 到 1 部署与教师使用手册(Windows 11 + WSL + Docker)

这是一份单文件总手册,目标是让你下次从零开始也能快速完成:

  • 本地环境搭建
  • 代码开发与测试
  • 教师端题目录入与发布
  • 题目三核心文件(info.jsonquestion.htmlserver.py)实操

1. 一次性准备(推荐方案)

推荐环境:

  • 视窗11
  • Docker Desktop(启用WSL2引擎)
  • Ubuntu(WSL发行版)
  • 代码放在WSL文件系统中(如~/dev/ZJUI-Learn

为什么:

  • /mnt/c/...更快,尤其是文件监听与测试阶段
  • 路径/权限问题更少
  • 开发稳定性更高

2. 从 0 开始部署(可直接照做)

2.1 安装与检查 Docker Desktop

  1. 安装 Docker 桌面
  2. 设置中确认:
  • Use the WSL 2 based engine 已开启
  • Ubuntu 集成已开启
  1. 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
  • 应用并重启后执行:
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. 教师操作主线(课程使用)

建议按这个顺序:

  1. 进入课程管理(课程/课程实例)
  2. Questions 里建题与维护题库
  3. Assessments 中组卷和配置规则
  4. 预览(教师视角 + 学生视角)
  5. 发布后观察结果并迭代

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:筛选标签
  • singleVarianttrue 表示单变体(不随机)
  • partialCredit:是否允许部分得分
  • showCorrectAnswer:面板是否显示正确答案
  • gradingMethodInternal / 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.pygenerate() 赋值中
  • 所有输入组件的 answers-name 必须唯一
  • 优先使用官方元素(如pl-number-inputpl-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

  1. Questions 新建问题(填写QID/标题)
  2. 补齐三文件:
  • info.json 元信息
  • question.html 题面与输入
  • server.py 参数与评分
  1. 预览并反复生成新变体测试
  2. 检查错误输入时反馈是否合理
  3. 将题加入评估,配置分值/尝试次数/时间策略
  4. 发布前做一次学生视角走查

10. 你当前项目的 AI 出题入口(现状)

当前项目已具备:

  • 问题解答页 Generate question from upload 按钮
  • 点击进入上传页面
  • 可上传题目截图或文本草稿
  • 调用Cursor API 生成info.jsonquestion.htmlserver.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 字段完整(uuidtypetitletopic
  • question.htmlanswers-name 全部唯一
  • server.py 生成了题面所需全部参数
  • 至少测试多个变体
  • 错误答案反馈可读、可执行
  • 评估配置(分值、时间、次数)已核对

13. 参考文档(仓库内)

  • docs/question/overview.md
  • docs/question/server.md
  • docs/question/template.md
  • docs/question/runtime.md
  • docs/instructor-guide/index.md
  • docs/assessment/
  • docs/QUESTION_SETUP_GUIDE_ZH.md
  • docs/TEACHER_ADMIN_ALL_PAGES_MANUAL_ZH.md
  • docs/TEACHER_TRAINING_PLAYBOOK_ZH.md