feat: rebuild linux learning lab experience
This commit is contained in:
589
server.py
589
server.py
@@ -1,9 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Linux 学习平台 Server(知识导向版)
|
||||
- 提供课程结构、练习判题、沙盒执行
|
||||
- 课程模型:module -> lesson -> exercise
|
||||
"""
|
||||
"""Linux learning lab HTTP server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -11,6 +7,7 @@ import hashlib
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import urllib.parse
|
||||
from typing import Any
|
||||
|
||||
@@ -25,19 +22,183 @@ HTML_FILE = os.path.join(os.path.dirname(__file__), "index.html")
|
||||
PRIVACY_FILE = os.path.join(os.path.dirname(__file__), "privacy.html")
|
||||
SANDBOX = LinuxSandbox()
|
||||
|
||||
PUBLIC_GET_PATHS = {
|
||||
"/",
|
||||
"/privacy",
|
||||
"/privacy.html",
|
||||
"/api/course",
|
||||
"/api/course/search",
|
||||
"/api/health",
|
||||
"/api/lesson",
|
||||
"/api/overview",
|
||||
}
|
||||
PUBLIC_POST_PATHS = {
|
||||
"/api/login",
|
||||
}
|
||||
SAFE_REMOTE_HOST = "xiaoxiaoluohao.indevs.in"
|
||||
|
||||
|
||||
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 course: {e}")
|
||||
with open(TASKS_FILE, "r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
except Exception as exc:
|
||||
print(f"Error loading course: {exc}")
|
||||
return {"meta": {}, "modules": []}
|
||||
|
||||
|
||||
COURSE = load_course()
|
||||
|
||||
|
||||
def looks_garbled(value: Any) -> bool:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
return False
|
||||
if "\ufffd" in value:
|
||||
return True
|
||||
asciiish = sum(ch.isascii() and (ch.isalnum() or ch in " -_/.,:;`()[]{}'\"") for ch in value)
|
||||
non_ascii = sum(not ch.isascii() for ch in value)
|
||||
return non_ascii >= 3 and non_ascii > asciiish
|
||||
|
||||
|
||||
def safe_text(value: Any, fallback: str) -> str:
|
||||
if isinstance(value, str) and value.strip() and not looks_garbled(value):
|
||||
return value.strip()
|
||||
return fallback
|
||||
|
||||
|
||||
def safe_list(values: Any, fallback: list[str]) -> list[str]:
|
||||
if not isinstance(values, list) or not values:
|
||||
return fallback
|
||||
cleaned = [str(item).strip() for item in values if str(item).strip()]
|
||||
if not cleaned:
|
||||
return fallback
|
||||
garbled = sum(1 for item in cleaned if looks_garbled(item))
|
||||
if garbled >= max(1, len(cleaned) // 2):
|
||||
return fallback
|
||||
merged: list[str] = []
|
||||
for index, item in enumerate(cleaned):
|
||||
if looks_garbled(item):
|
||||
if index < len(fallback):
|
||||
merged.append(fallback[index])
|
||||
continue
|
||||
merged.append(item)
|
||||
return merged or fallback
|
||||
|
||||
|
||||
def split_commands(command: str | None, lesson_id: str | None = None) -> list[str]:
|
||||
if command:
|
||||
tokens = [part.strip() for part in re.split(r"\s*/\s*|\s*,\s*", command) if part.strip()]
|
||||
if tokens:
|
||||
return tokens
|
||||
if lesson_id:
|
||||
tail = lesson_id.split("_")[-1].strip()
|
||||
if tail:
|
||||
return [tail]
|
||||
return []
|
||||
|
||||
|
||||
def command_label(commands: list[str]) -> str:
|
||||
if not commands:
|
||||
return "mixed commands"
|
||||
if len(commands) == 1:
|
||||
return commands[0]
|
||||
return " / ".join(commands[:3])
|
||||
|
||||
|
||||
def module_number(module_id: str | None) -> str | None:
|
||||
if not module_id:
|
||||
return None
|
||||
match = re.search(r"(\d+)", module_id)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def module_title(module: dict[str, Any]) -> str:
|
||||
raw = module.get("title")
|
||||
fallback = f"Module {module_number(module.get('id'))}" if module_number(module.get("id")) else "Course module"
|
||||
return safe_text(raw, fallback)
|
||||
|
||||
|
||||
def module_summary(module: dict[str, Any]) -> str:
|
||||
fallback = "Learn command purpose, expected output, and how each step fits into a Linux troubleshooting flow."
|
||||
return safe_text(module.get("summary"), fallback)
|
||||
|
||||
|
||||
def lesson_title(lesson: dict[str, Any]) -> str:
|
||||
commands = split_commands(lesson.get("command"), lesson.get("id"))
|
||||
fallback = f"Learn {command_label(commands)}"
|
||||
return safe_text(lesson.get("title"), fallback)
|
||||
|
||||
|
||||
def default_goal(commands: list[str]) -> str:
|
||||
return f"Understand what {command_label(commands)} does, how to run it, and how to read the result."
|
||||
|
||||
|
||||
def default_why(commands: list[str]) -> str:
|
||||
return f"In day-to-day operations work, {command_label(commands)} helps you confirm the current state before taking the next step."
|
||||
|
||||
|
||||
def default_concepts(commands: list[str]) -> list[str]:
|
||||
label = command_label(commands)
|
||||
return [
|
||||
f"What problem {label} solves",
|
||||
"Typical syntax and common options",
|
||||
"How to verify the output",
|
||||
"How it fits into a troubleshooting chain",
|
||||
]
|
||||
|
||||
|
||||
def default_pitfalls(commands: list[str]) -> list[str]:
|
||||
label = command_label(commands)
|
||||
return [
|
||||
f"Running {label} without checking the context first",
|
||||
"Reading the output but not validating the target state",
|
||||
"Ignoring current directory, permissions, or file state",
|
||||
]
|
||||
|
||||
|
||||
def default_scenarios(commands: list[str]) -> list[str]:
|
||||
label = command_label(commands)
|
||||
return [
|
||||
f"Use {label} to inspect a host before editing files or services",
|
||||
"Confirm current state before and after a change",
|
||||
"Connect one command to the next action in a real incident flow",
|
||||
]
|
||||
|
||||
|
||||
def default_flow(commands: list[str]) -> list[str]:
|
||||
label = command_label(commands)
|
||||
return [
|
||||
"Confirm the goal and current working context",
|
||||
f"Run {label} and inspect the output carefully",
|
||||
"Decide the next command based on what the output shows",
|
||||
]
|
||||
|
||||
|
||||
def default_takeaways(commands: list[str]) -> list[str]:
|
||||
label = command_label(commands)
|
||||
return [
|
||||
f"Explain when to use {label} in a real Linux task",
|
||||
"Know what output matters most",
|
||||
"Link the command to the next verification or repair step",
|
||||
]
|
||||
|
||||
|
||||
def default_after_class(commands: list[str]) -> str:
|
||||
return f"Repeat this lesson in the sandbox two or three more times and explain every step out loud using {command_label(commands)}."
|
||||
|
||||
|
||||
def flatten_lessons() -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for module in COURSE.get("modules", []):
|
||||
for lesson in module.get("lessons", []):
|
||||
item = dict(lesson)
|
||||
item["module_id"] = module.get("id")
|
||||
item["module_title"] = module.get("title")
|
||||
item["module_summary"] = module.get("summary")
|
||||
rows.append(item)
|
||||
return rows
|
||||
|
||||
|
||||
def flatten_exercises() -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for module in COURSE.get("modules", []):
|
||||
@@ -48,24 +209,260 @@ def flatten_exercises() -> list[dict[str, Any]]:
|
||||
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
|
||||
def find_lesson(lesson_id: str) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
||||
for module in COURSE.get("modules", []):
|
||||
for lesson in module.get("lessons", []):
|
||||
if lesson.get("id") == lesson_id:
|
||||
return module, lesson
|
||||
return None, None
|
||||
|
||||
|
||||
def find_exercise(exercise_id: str) -> dict[str, Any] | None:
|
||||
for exercise in flatten_exercises():
|
||||
if exercise.get("id") == exercise_id:
|
||||
return exercise
|
||||
return None
|
||||
|
||||
|
||||
def build_lesson_preview(module: dict[str, Any], lesson: dict[str, Any]) -> dict[str, Any]:
|
||||
commands = split_commands(lesson.get("command"), lesson.get("id"))
|
||||
return {
|
||||
"id": lesson.get("id"),
|
||||
"display_title": lesson_title(lesson),
|
||||
"display_goal": safe_text(lesson.get("goal"), default_goal(commands)),
|
||||
"command": lesson.get("command") or command_label(commands),
|
||||
"command_tokens": commands,
|
||||
"exercise_count": len(lesson.get("exercises", [])),
|
||||
}
|
||||
|
||||
|
||||
def build_module_preview(module: dict[str, Any]) -> dict[str, Any]:
|
||||
lessons = [build_lesson_preview(module, lesson) for lesson in module.get("lessons", [])]
|
||||
return {
|
||||
"id": module.get("id"),
|
||||
"display_title": module_title(module),
|
||||
"display_summary": module_summary(module),
|
||||
"lesson_count": len(module.get("lessons", [])),
|
||||
"exercise_count": sum(len(lesson.get("exercises", [])) for lesson in module.get("lessons", [])),
|
||||
"lessons": lessons,
|
||||
}
|
||||
|
||||
|
||||
def build_exercise(exercise: dict[str, Any], lesson_stub: dict[str, Any]) -> dict[str, Any]:
|
||||
commands = split_commands(lesson_stub.get("command"), lesson_stub.get("id"))
|
||||
label = command_label(commands)
|
||||
first_solution = ""
|
||||
if isinstance(exercise.get("solution"), list) and exercise["solution"]:
|
||||
first_solution = str(exercise["solution"][0]).strip()
|
||||
|
||||
payload = {
|
||||
"id": exercise.get("id"),
|
||||
"type": exercise.get("type") or "operation",
|
||||
"hint": safe_text(exercise.get("hint"), f"Try {first_solution or label} first, then inspect the result."),
|
||||
"success_msg": safe_text(exercise.get("success_msg"), f"Nice work. You completed a {label} practice task."),
|
||||
"solution": exercise.get("solution", []),
|
||||
}
|
||||
|
||||
if payload["type"] == "operation":
|
||||
payload["title"] = safe_text(exercise.get("title"), f"Run {first_solution or label}")
|
||||
else:
|
||||
payload["question"] = safe_text(
|
||||
exercise.get("question"),
|
||||
f"Explain when you would use {label} in a real Linux operations flow.",
|
||||
)
|
||||
payload["answer"] = safe_text(
|
||||
exercise.get("answer"),
|
||||
f"Focus on purpose, expected output, and the next step after {label}.",
|
||||
)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def build_lesson_payload(module: dict[str, Any], lesson: dict[str, Any]) -> dict[str, Any]:
|
||||
commands = split_commands(lesson.get("command"), lesson.get("id"))
|
||||
all_lessons = flatten_lessons()
|
||||
lesson_ids = [item.get("id") for item in all_lessons]
|
||||
current_id = lesson.get("id")
|
||||
index = lesson_ids.index(current_id) if current_id in lesson_ids else -1
|
||||
|
||||
previous_lesson = None
|
||||
next_lesson = None
|
||||
if index > 0:
|
||||
prev_module, prev_lesson = find_lesson(lesson_ids[index - 1] or "")
|
||||
if prev_module and prev_lesson:
|
||||
previous_lesson = build_lesson_preview(prev_module, prev_lesson)
|
||||
if index >= 0 and index + 1 < len(lesson_ids):
|
||||
next_module, next_lesson_item = find_lesson(lesson_ids[index + 1] or "")
|
||||
if next_module and next_lesson_item:
|
||||
next_lesson = build_lesson_preview(next_module, next_lesson_item)
|
||||
|
||||
return {
|
||||
"id": lesson.get("id"),
|
||||
"display_title": lesson_title(lesson),
|
||||
"display_goal": safe_text(lesson.get("goal"), default_goal(commands)),
|
||||
"display_why": safe_text(lesson.get("why_it_matters"), default_why(commands)),
|
||||
"display_module_title": module_title(module),
|
||||
"display_module_summary": module_summary(module),
|
||||
"command": lesson.get("command") or command_label(commands),
|
||||
"command_tokens": commands,
|
||||
"examples": safe_list(lesson.get("examples"), commands or ["pwd"]),
|
||||
"concepts": safe_list(lesson.get("concepts"), default_concepts(commands)),
|
||||
"pitfalls": safe_list(lesson.get("pitfalls"), default_pitfalls(commands)),
|
||||
"scenarios": safe_list(lesson.get("scenarios"), default_scenarios(commands)),
|
||||
"troubleshooting_flow": safe_list(lesson.get("troubleshooting_flow"), default_flow(commands)),
|
||||
"takeaways": safe_list(lesson.get("takeaways"), default_takeaways(commands)),
|
||||
"classic_view": safe_text(
|
||||
lesson.get("classic_view"),
|
||||
f"Treat {command_label(commands)} as part of a workflow, not as an isolated command to memorize.",
|
||||
),
|
||||
"after_class": safe_text(lesson.get("after_class"), default_after_class(commands)),
|
||||
"related_commands": safe_list(lesson.get("related_commands"), commands or ["pwd"]),
|
||||
"exercises": [build_exercise(exercise, lesson) for exercise in lesson.get("exercises", [])],
|
||||
"exercise_count": len(lesson.get("exercises", [])),
|
||||
"module_id": module.get("id"),
|
||||
"previous_lesson": previous_lesson,
|
||||
"next_lesson": next_lesson,
|
||||
}
|
||||
|
||||
|
||||
def build_overview() -> dict[str, Any]:
|
||||
modules = [build_module_preview(module) for module in COURSE.get("modules", [])]
|
||||
lessons = flatten_lessons()
|
||||
commands = sorted(
|
||||
{
|
||||
command
|
||||
for lesson in lessons
|
||||
for command in split_commands(lesson.get("command"), lesson.get("id"))
|
||||
if command
|
||||
}
|
||||
)
|
||||
exercise_count = sum(len(lesson.get("exercises", [])) for lesson in lessons)
|
||||
meta = COURSE.get("meta", {})
|
||||
|
||||
return {
|
||||
"meta": {
|
||||
"version": meta.get("version", "4.0"),
|
||||
"title": safe_text(meta.get("title"), "Linux Learning Lab"),
|
||||
"description": safe_text(
|
||||
meta.get("description"),
|
||||
"Searchable Linux lessons with command previews, sandbox practice, and guided troubleshooting.",
|
||||
),
|
||||
"updated": meta.get("updated", ""),
|
||||
"module_count": meta.get("module_count", len(modules)),
|
||||
"lesson_count": meta.get("total_lessons", len(lessons)),
|
||||
"exercise_count": meta.get("total_exercises", exercise_count),
|
||||
"command_count": len(commands),
|
||||
},
|
||||
"runtime": {
|
||||
"cwd": SANDBOX.cwd,
|
||||
"user": SANDBOX.user,
|
||||
},
|
||||
"modules": modules,
|
||||
"commands": commands[:24],
|
||||
}
|
||||
|
||||
|
||||
def search_course(query: str) -> list[dict[str, Any]]:
|
||||
needle = query.strip().lower()
|
||||
if not needle:
|
||||
return []
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
for module in COURSE.get("modules", []):
|
||||
module_view = build_module_preview(module)
|
||||
module_score = 0
|
||||
module_id = str(module.get("id", "")).lower()
|
||||
if needle == module_id:
|
||||
module_score += 120
|
||||
elif needle in module_id:
|
||||
module_score += 80
|
||||
if needle in module_view["display_title"].lower():
|
||||
module_score += 60
|
||||
if needle in module_view["display_summary"].lower():
|
||||
module_score += 40
|
||||
if module_score and module.get("lessons"):
|
||||
results.append(
|
||||
{
|
||||
"type": "module",
|
||||
"title": module_view["display_title"],
|
||||
"subtitle": module_view["display_summary"],
|
||||
"module_id": module.get("id"),
|
||||
"lesson_id": module.get("lessons", [{}])[0].get("id"),
|
||||
"score": module_score,
|
||||
}
|
||||
)
|
||||
|
||||
for lesson in module.get("lessons", []):
|
||||
lesson_view = build_lesson_preview(module, lesson)
|
||||
lesson_score = 0
|
||||
targets = [
|
||||
str(lesson.get("id", "")).lower(),
|
||||
lesson_view["display_title"].lower(),
|
||||
lesson_view["display_goal"].lower(),
|
||||
str(lesson_view["command"]).lower(),
|
||||
" ".join(token.lower() for token in lesson_view["command_tokens"]),
|
||||
]
|
||||
for text in targets:
|
||||
if not text:
|
||||
continue
|
||||
if needle == text:
|
||||
lesson_score += 110
|
||||
elif needle in text:
|
||||
lesson_score += 55
|
||||
if lesson_score:
|
||||
results.append(
|
||||
{
|
||||
"type": "lesson",
|
||||
"title": lesson_view["display_title"],
|
||||
"subtitle": f"{module_view['display_title']} | {command_label(lesson_view['command_tokens'])}",
|
||||
"module_id": module.get("id"),
|
||||
"lesson_id": lesson.get("id"),
|
||||
"score": lesson_score,
|
||||
}
|
||||
)
|
||||
|
||||
for exercise in lesson.get("exercises", []):
|
||||
exercise_view = build_exercise(exercise, lesson)
|
||||
exercise_score = 0
|
||||
for text in [
|
||||
str(exercise.get("id", "")).lower(),
|
||||
str(exercise_view.get("title", "")).lower(),
|
||||
str(exercise_view.get("question", "")).lower(),
|
||||
str(exercise_view.get("hint", "")).lower(),
|
||||
]:
|
||||
if not text:
|
||||
continue
|
||||
if needle == text:
|
||||
exercise_score += 90
|
||||
elif needle in text:
|
||||
exercise_score += 40
|
||||
if exercise_score:
|
||||
results.append(
|
||||
{
|
||||
"type": "exercise",
|
||||
"title": exercise_view.get("title") or exercise_view.get("question") or exercise.get("id", ""),
|
||||
"subtitle": f"{lesson_view['display_title']} | {exercise.get('id', '')}",
|
||||
"module_id": module.get("id"),
|
||||
"lesson_id": lesson.get("id"),
|
||||
"exercise_id": exercise.get("id"),
|
||||
"score": exercise_score,
|
||||
}
|
||||
)
|
||||
|
||||
ranked = sorted(results, key=lambda item: (-item["score"], item["title"]))
|
||||
return [{key: value for key, value in item.items() if key != "score"} for item in ranked[:24]]
|
||||
|
||||
|
||||
class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def send_json(self, data: Any, status=200):
|
||||
def send_json(self, data: Any, status: int = 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")
|
||||
@@ -74,14 +471,21 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.wfile.write(raw)
|
||||
|
||||
def send_file(self, path: str, content_type: str):
|
||||
with open(path, "rb") as f:
|
||||
body = f.read()
|
||||
with open(path, "rb") as file:
|
||||
body = file.read()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def is_public_path(self, path: str, method: str) -> bool:
|
||||
if method == "GET":
|
||||
return path in PUBLIC_GET_PATHS
|
||||
if method == "POST":
|
||||
return path in PUBLIC_POST_PATHS
|
||||
return False
|
||||
|
||||
def check_auth(self, auth_header: str, token: str) -> bool:
|
||||
if self.client_address[0] == "127.0.0.1":
|
||||
return True
|
||||
@@ -91,16 +495,23 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
||||
return True
|
||||
return False
|
||||
|
||||
def require_auth_if_needed(self, path: str, method: str) -> bool:
|
||||
if self.is_public_path(path, method):
|
||||
return True
|
||||
host = self.headers.get("Host", "")
|
||||
auth_header = self.headers.get("Authorization", "")
|
||||
token = self.headers.get("X-Token", "")
|
||||
if SAFE_REMOTE_HOST in host and not self.check_auth(auth_header, token):
|
||||
self.send_json({"error": "Authentication required"}, 401)
|
||||
return False
|
||||
return True
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
path = parsed.path
|
||||
|
||||
if path not in ["/", "/privacy", "/privacy.html", "/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", ""):
|
||||
self.send_json({"error": "Authentication required"}, 401)
|
||||
return
|
||||
if not self.require_auth_if_needed(path, "GET"):
|
||||
return
|
||||
|
||||
if path == "/":
|
||||
self.send_file(HTML_FILE, "text/html; charset=utf-8")
|
||||
@@ -111,16 +522,46 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
if path == "/api/health":
|
||||
self.send_json({"ok": True, "cwd": SANDBOX.cwd, "user": SANDBOX.user, "modules": len(COURSE.get("modules", []))})
|
||||
overview = build_overview()
|
||||
self.send_json(
|
||||
{
|
||||
"ok": True,
|
||||
"cwd": SANDBOX.cwd,
|
||||
"user": SANDBOX.user,
|
||||
"module_count": overview["meta"]["module_count"],
|
||||
"lesson_count": overview["meta"]["lesson_count"],
|
||||
"exercise_count": overview["meta"]["exercise_count"],
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
if path == "/api/course":
|
||||
self.send_json(COURSE)
|
||||
return
|
||||
|
||||
if path == "/api/overview":
|
||||
self.send_json(build_overview())
|
||||
return
|
||||
|
||||
if path == "/api/lesson":
|
||||
lesson_id = urllib.parse.parse_qs(parsed.query).get("id", [""])[0]
|
||||
if not lesson_id:
|
||||
self.send_json({"error": "id required"}, 400)
|
||||
return
|
||||
module, lesson = find_lesson(lesson_id)
|
||||
if not module or not lesson:
|
||||
self.send_json({"error": "Lesson not found"}, 404)
|
||||
return
|
||||
self.send_json({"lesson": build_lesson_payload(module, lesson)})
|
||||
return
|
||||
|
||||
if path == "/api/course/search":
|
||||
query = urllib.parse.parse_qs(parsed.query).get("q", [""])[0]
|
||||
self.send_json({"query": query, "results": search_course(query)})
|
||||
return
|
||||
|
||||
if path == "/api/run":
|
||||
query = urllib.parse.parse_qs(parsed.query)
|
||||
cmd = query.get("cmd", [""])[0]
|
||||
cmd = urllib.parse.parse_qs(parsed.query).get("cmd", [""])[0]
|
||||
if not cmd:
|
||||
self.send_json({"error": "No command provided"}, 400)
|
||||
return
|
||||
@@ -129,16 +570,17 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
if path == "/api/check":
|
||||
query = urllib.parse.parse_qs(parsed.query)
|
||||
ex_id = query.get("exercise_id", [""])[0]
|
||||
exercise_id = query.get("exercise_id", [""])[0]
|
||||
cmd = query.get("last_cmd", [""])[0]
|
||||
output = query.get("output", [""])[0]
|
||||
if not ex_id:
|
||||
if not exercise_id:
|
||||
self.send_json({"error": "exercise_id required"}, 400)
|
||||
return
|
||||
exercise = find_exercise(ex_id)
|
||||
exercise = find_exercise(exercise_id)
|
||||
if not exercise:
|
||||
self.send_json({"error": "Exercise not found"}, 404)
|
||||
return
|
||||
|
||||
state = {
|
||||
"cmd": cmd,
|
||||
"output": output,
|
||||
@@ -147,26 +589,32 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
||||
"is_executable": SANDBOX.is_executable,
|
||||
}
|
||||
success, reason = self.evaluate_exercise(exercise, state)
|
||||
self.send_json({
|
||||
"exercise_id": ex_id,
|
||||
"success": success,
|
||||
"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),
|
||||
})
|
||||
lesson_stub = {"command": exercise.get("lesson_command"), "id": exercise.get("lesson_id")}
|
||||
exercise_view = build_exercise(exercise, lesson_stub)
|
||||
self.send_json(
|
||||
{
|
||||
"exercise_id": exercise_id,
|
||||
"success": success,
|
||||
"message": exercise_view["success_msg"] if success else reason,
|
||||
"hint": exercise_view.get("hint"),
|
||||
"lesson_title": safe_text(exercise.get("lesson_title"), lesson_title(lesson_stub)),
|
||||
"module_title": safe_text(exercise.get("module_title"), "Course module"),
|
||||
"next_suggestion": self.build_next_suggestion(exercise_id) if success else None,
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Not Found")
|
||||
self.send_json({"error": "Not found"}, 404)
|
||||
|
||||
def do_POST(self):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
path = parsed.path
|
||||
|
||||
if not self.require_auth_if_needed(path, "POST"):
|
||||
return
|
||||
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
raw = self.rfile.read(content_length).decode() if content_length else "{}"
|
||||
raw = self.rfile.read(content_length).decode("utf-8") if content_length else "{}"
|
||||
|
||||
if path == "/api/login":
|
||||
try:
|
||||
@@ -174,59 +622,66 @@ class LinuxLearningHandler(http.server.BaseHTTPRequestHandler):
|
||||
except Exception:
|
||||
self.send_json({"success": False, "error": "Invalid JSON"}, 400)
|
||||
return
|
||||
|
||||
username = data.get("username", "")
|
||||
password = data.get("password", "")
|
||||
if username in USERS and hashlib.sha256(password.encode()).hexdigest() == USERS[username]:
|
||||
self.send_json({"success": True, "token": "safe_linux_2026", "message": "✅ 登录成功!"})
|
||||
if username in USERS and hashlib.sha256(password.encode("utf-8")).hexdigest() == USERS[username]:
|
||||
self.send_json({"success": True, "token": "safe_linux_2026", "message": "Login succeeded"})
|
||||
return
|
||||
self.send_json({"success": False, "error": "❌ 用户名或密码错误"}, 401)
|
||||
|
||||
self.send_json({"success": False, "error": "Invalid username or password"}, 401)
|
||||
return
|
||||
|
||||
if path == "/api/logout":
|
||||
self.send_json({"success": True, "message": "👋 已退出登录"})
|
||||
self.send_json({"success": True, "message": "Logged out"})
|
||||
return
|
||||
|
||||
if path == "/api/reset":
|
||||
SANDBOX.reset()
|
||||
self.send_json({"success": True, "message": "♻️ 沙盒环境已重置"})
|
||||
self.send_json({"success": True, "message": "Sandbox reset"})
|
||||
return
|
||||
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Not Found")
|
||||
self.send_json({"error": "Not found"}, 404)
|
||||
|
||||
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, "📝 这是理解类练习,请先阅读讲解并思考答案。"
|
||||
exercise_type = exercise.get("type")
|
||||
if exercise_type in {"understanding", "scenario"}:
|
||||
return False, "This is a reflection task. Read the reference answer, then explain it in your own words."
|
||||
|
||||
cmd = state["cmd"].strip()
|
||||
if exercise.get("solution"):
|
||||
for sol in exercise["solution"]:
|
||||
if cmd == sol.strip():
|
||||
return True, ""
|
||||
for solution in exercise.get("solution", []):
|
||||
if cmd == str(solution).strip():
|
||||
return True, ""
|
||||
|
||||
success_test = exercise.get("success_test")
|
||||
if not success_test:
|
||||
return False, "❌ 暂未命中练习要求"
|
||||
return False, "The command ran, but this task does not have an automated check yet."
|
||||
|
||||
try:
|
||||
ok = bool(eval(success_test, {"__builtins__": {}}, state))
|
||||
return ok, "❌ 结果还没达到练习要求,再试一次"
|
||||
if ok:
|
||||
return True, ""
|
||||
return False, "The command ran, but the target state has not been reached yet."
|
||||
except Exception:
|
||||
return False, "❌ 当前命令没有通过判定,建议对照示例重新尝试"
|
||||
return False, "The checker could not validate this attempt. Compare your command with the example and try again."
|
||||
|
||||
def build_next_suggestion(self, current_ex_id: str) -> str | None:
|
||||
def build_next_suggestion(self, current_exercise_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')}"
|
||||
for index, item in enumerate(rows):
|
||||
if item.get("id") != current_exercise_id:
|
||||
continue
|
||||
if index + 1 >= len(rows):
|
||||
return None
|
||||
next_item = rows[index + 1]
|
||||
lesson_stub = {"command": next_item.get("lesson_command"), "id": next_item.get("lesson_id")}
|
||||
exercise_view = build_exercise(next_item, lesson_stub)
|
||||
title = exercise_view.get("title") or exercise_view.get("question") or "the next exercise"
|
||||
return f"Next up: {lesson_title(lesson_stub)} | {title}"
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = 8084
|
||||
print(f"🐧 Linux 学习平台启动中... http://127.0.0.1:{port}")
|
||||
print("📚 线上地址: https://linux.xiaoxiaoluohao.indevs.in")
|
||||
print(f"Linux Learning Lab is running at http://127.0.0.1:{port}")
|
||||
print("Remote host: https://linux.xiaoxiaoluohao.indevs.in")
|
||||
http.server.ThreadingHTTPServer(("127.0.0.1", port), LinuxLearningHandler).serve_forever()
|
||||
|
||||
Reference in New Issue
Block a user