feat: redesign linux course around learning-first structure

This commit is contained in:
likingcode
2026-03-10 07:41:38 +08:00
parent dacc1cb735
commit 5ca924cadd
4 changed files with 902 additions and 827 deletions

155
server.py
View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""
Linux 命令沙盒练习平台 Server增强版)
- 集成 sandbox.py 沙盒引擎
- 提供课程、命令执行、任务判定、学习建议等 API
Linux 习平台 Server知识导向版)
- 提供课程结构、练习判题、沙盒执行
- 课程模型module -> lesson -> exercise
"""
from __future__ import annotations
@@ -20,40 +20,51 @@ USERS = {
"admin": hashlib.sha256(b"safe_linux_2026").hexdigest(),
}
TOKEN_TTL = 86400
TASKS_FILE = os.path.join(os.path.dirname(__file__), "COURSE_TASKS.json")
HTML_FILE = os.path.join(os.path.dirname(__file__), "index.html")
SANDBOX = LinuxSandbox()
def load_tasks() -> dict[str, Any]:
def load_course() -> dict[str, Any]:
try:
with open(TASKS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"Error loading tasks: {e}")
return {"meta": {}, "levels": []}
print(f"Error loading course: {e}")
return {"meta": {}, "modules": []}
TASKS = load_tasks()
COURSE = load_course()
def find_task(task_id: str) -> dict[str, Any] | None:
for level in TASKS.get("levels", []):
for task in level.get("challenges", []):
if task.get("id") == task_id:
task["level_title"] = level.get("title", "")
task["level_id"] = level.get("id", "")
return task
def flatten_exercises() -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for module in COURSE.get("modules", []):
for lesson in module.get("lessons", []):
for exercise in lesson.get("exercises", []):
item = dict(exercise)
item["module_id"] = module.get("id")
item["module_title"] = module.get("title")
item["lesson_id"] = lesson.get("id")
item["lesson_title"] = lesson.get("title")
item["lesson_goal"] = lesson.get("goal")
item["lesson_command"] = lesson.get("command")
rows.append(item)
return rows
def find_exercise(ex_id: str) -> dict[str, Any] | None:
for item in flatten_exercises():
if item.get("id") == ex_id:
return item
return None
class LinuxSandboxHandler(http.server.BaseHTTPRequestHandler):
class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass
def send_json(self, data: dict[str, Any], status=200):
def send_json(self, data: Any, status=200):
raw = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
@@ -61,14 +72,14 @@ class LinuxSandboxHandler(http.server.BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(raw)
def _send_file(self, path: str, content_type: str):
def send_file(self, path: str, content_type: str):
with open(path, "rb") as f:
content = f.read()
body = f.read()
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(content)))
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(content)
self.wfile.write(body)
def check_auth(self, auth_header: str, token: str) -> bool:
if self.client_address[0] == "127.0.0.1":
@@ -83,7 +94,7 @@ class LinuxSandboxHandler(http.server.BaseHTTPRequestHandler):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
if path not in ["/", "/login.html", "/api/login", "/api/logout", "/api/tasks", "/api/health"]:
if path not in ["/", "/api/login", "/api/logout", "/api/course", "/api/health"]:
auth_header = self.headers.get("Authorization", "")
token = self.headers.get("X-Token", "")
if not self.check_auth(auth_header, token) and "xiaoxiaoluohao.indevs.in" in self.headers.get("Host", ""):
@@ -91,11 +102,15 @@ class LinuxSandboxHandler(http.server.BaseHTTPRequestHandler):
return
if path == "/":
self._send_file(HTML_FILE, "text/html; charset=utf-8")
self.send_file(HTML_FILE, "text/html; charset=utf-8")
return
if path == "/api/health":
self.send_json({"ok": True, "cwd": SANDBOX.cwd, "user": SANDBOX.user})
self.send_json({"ok": True, "cwd": SANDBOX.cwd, "user": SANDBOX.user, "modules": len(COURSE.get("modules", []))})
return
if path == "/api/course":
self.send_json(COURSE)
return
if path == "/api/run":
@@ -104,44 +119,40 @@ class LinuxSandboxHandler(http.server.BaseHTTPRequestHandler):
if not cmd:
self.send_json({"error": "No command provided"}, 400)
return
result = SANDBOX.execute(cmd)
self.send_json(result)
self.send_json(SANDBOX.execute(cmd))
return
if path == "/api/check":
query = urllib.parse.parse_qs(parsed.query)
task_id = query.get("task_id", [""])[0]
ex_id = query.get("exercise_id", [""])[0]
cmd = query.get("last_cmd", [""])[0]
output = query.get("output", [""])[0]
sandbox_state = {
if not ex_id:
self.send_json({"error": "exercise_id required"}, 400)
return
exercise = find_exercise(ex_id)
if not exercise:
self.send_json({"error": "Exercise not found"}, 404)
return
state = {
"cmd": cmd,
"output": output,
"cwd": SANDBOX.cwd,
"exists": SANDBOX.exists,
"is_executable": SANDBOX.is_executable,
"cmd": cmd,
"output": output,
}
if not task_id:
self.send_json({"error": "task_id required"}, 400)
return
task = find_task(task_id)
if not task:
self.send_json({"error": "Task not found"}, 404)
return
success, reason = self.evaluate_task(task, sandbox_state)
success, reason = self.evaluate_exercise(exercise, state)
self.send_json({
"task_id": task_id,
"exercise_id": ex_id,
"success": success,
"message": task.get("success_msg", "回答正确!") if success else reason or task.get("fail_message", "❌ 未通过测试"),
"hint": task.get("hint", "继续尝试,或者看看相关命令示例"),
"title": task.get("title", "未知任务"),
"next_suggestion": self.build_next_suggestion(task_id),
"message": exercise.get("success_msg", "练习通过") if success else reason,
"hint": exercise.get("hint"),
"lesson_title": exercise.get("lesson_title"),
"module_title": exercise.get("module_title"),
"next_suggestion": self.build_next_suggestion(ex_id),
})
return
if path == "/api/tasks":
self.send_json(TASKS)
return
self.send_response(404)
self.end_headers()
self.wfile.write(b"Not Found")
@@ -179,36 +190,38 @@ class LinuxSandboxHandler(http.server.BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(b"Not Found")
def evaluate_task(self, task: dict[str, Any], state: dict[str, Any]) -> tuple[bool, str]:
cmd = state["cmd"]
output = state["output"]
success_test = task.get("success_test")
if task.get("solution"):
for sol in task["solution"]:
if cmd.strip() == sol.strip():
def evaluate_exercise(self, exercise: dict[str, Any], state: dict[str, Any]) -> tuple[bool, str]:
ex_type = exercise.get("type")
if ex_type in {"understanding", "scenario"}:
return False, "📝 这是理解类练习,请先阅读讲解并思考答案。"
cmd = state["cmd"].strip()
if exercise.get("solution"):
for sol in exercise["solution"]:
if cmd == sol.strip():
return True, ""
success_test = exercise.get("success_test")
if not success_test:
return False, "还没达到任务要求"
return False, "暂未命中练习要求"
try:
ok = bool(eval(success_test, {"__builtins__": {}}, state))
return ok, "命令执行了,但结果还没达到题目要求"
return ok, "结果还没达到练习要求,再试一次"
except Exception:
return False, "❌ 当前判题规则未命中,换个更准确的命令试"
return False, "❌ 当前命令没有通过判定,建议对照示例重新尝"
def build_next_suggestion(self, current_task_id: str) -> str | None:
all_tasks = []
for level in TASKS.get("levels", []):
all_tasks.extend(level.get("challenges", []))
for idx, task in enumerate(all_tasks):
if task.get("id") == current_task_id and idx + 1 < len(all_tasks):
nxt = all_tasks[idx + 1]
return f"下一题:{nxt.get('title', '继续挑战')}"
def build_next_suggestion(self, current_ex_id: str) -> str | None:
rows = flatten_exercises()
for i, item in enumerate(rows):
if item.get("id") == current_ex_id and i + 1 < len(rows):
nxt = rows[i + 1]
return f"继续下一练:{nxt.get('title') or nxt.get('question') or nxt.get('id')}"
return None
if __name__ == "__main__":
PORT = 8084
print(f"🌱 Linux Sandbox Practice Server 启动中... http://127.0.0.1:{PORT}")
print("📚 地址: https://linux.xiaoxiaoluohao.indevs.in")
print("🔑 Account: admin / safe_linux_2026")
http.server.ThreadingHTTPServer(("127.0.0.1", PORT), LinuxSandboxHandler).serve_forever()
port = 8084
print(f"🐧 Linux 学习平台启动中... http://127.0.0.1:{port}")
print("📚 线上地址: https://linux.xiaoxiaoluohao.indevs.in")
http.server.ThreadingHTTPServer(("127.0.0.1", port), LinuxLearningHandler).serve_forever()