From 3f8d4c0ce6a01616e7ffe39a0050844c49678f51 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 18 Mar 2026 18:12:04 +0800 Subject: [PATCH] feat: rebuild linux learning lab experience --- index.html | 914 ++++++++++++++++++++++++++++----------------------- privacy.html | 109 +++--- server.py | 589 +++++++++++++++++++++++++++++---- 3 files changed, 1094 insertions(+), 518 deletions(-) diff --git a/index.html b/index.html index ae5f602..2cd6532 100644 --- a/index.html +++ b/index.html @@ -1,494 +1,593 @@ - + - Linux 运维全场景学习平台 + Linux Learning Lab -
-
-
-

🐧 Linux 运维全场景学习平台

-

以知识理解为中心,尽量覆盖运维强相关全场景,帮助建立真正可迁移的 Linux 能力。

+
+
+
+
Linux Ops Learning Lab
+

Linux Learning Lab

+

Search Linux lessons, practice commands in a sandbox, and build better troubleshooting habits.

-
- - - +
+ + + Privacy
-
-
+
-
- +
+ -
-

这一课学什么

-

-
+
+
+
+
+
Pick a lesson to begin
+

Turn Linux practice into workflow thinking

+

Choose a lesson from the left to see goals, examples, common pitfalls, and runnable exercises.

+
+
+ + +
+
+
+
+
+

Module summary

+

This card shows the learning direction of the current module.

+
+
+

Sandbox runtime

+

You can execute commands directly below. The platform returns simulated output and exercise feedback.

+
    +
  • Current directory: /
  • +
  • Current user: sandbox_user
  • +
+
+
+
-
-

为什么重要

-

-
+
+
+
+

Why this matters

+

Understanding when to use a command matters more than memorizing flags in isolation.

+
+
+

Teaching angle

+

Each lesson connects the command to a broader troubleshooting flow.

+
+
+

Core ideas

+
    +
    +
    +

    Common scenarios

    +
      +
      +
      +

      Common pitfalls

      +
        +
        +
        +

        Troubleshooting flow

        +
          +
          +
          +

          Takeaways

          +
            +

            +
            +
            +

            Example commands

            +
            +
            +
            +
            -
            -

            核心知识点

            -
              -
              - -
              -

              最小示例

              -
              -
              - -
              -

              常见误区

              -
                -
                - -
                -

                典型使用场景

                -
                  -
                  - -
                  -

                  教材视角

                  -

                  -
                  - -
                  -

                  相关命令

                  -
                  -
                  - -
                  -

                  排障链路 / 处理顺序

                  -
                    -
                    - -
                    -

                    课后总结

                    -
                      -

                      -
                      - -
                      -

                      本课练习

                      +
                      +
                      Practice
                      +

                      Exercises and sandbox terminal

                      +

                      Operation tasks run in the simulated terminal. Reflection tasks keep the learning flow readable even when source data is noisy.

                      -
                      - -
                      - - + + +
                      diff --git a/privacy.html b/privacy.html index 9f545b7..b4cce43 100644 --- a/privacy.html +++ b/privacy.html @@ -1,67 +1,92 @@ - + - Bot 隐私政策 / Privacy Policy + Linux Learning Lab Privacy Notice
                      -

                      Bot 隐私政策 / Privacy Policy

                      -

                      最后更新:2026-03-10

                      +
                      Privacy Notice
                      +

                      Linux Learning Lab Privacy Notice

                      +

                      Last updated: 2026-03-18

                      -

                      1. 我们是谁

                      -

                      本隐私政策适用于与本 Telegram Bot(以下简称“Bot”)的交互。本 Bot 用于提供个人助理与技术学习/排障相关的对话服务。

                      +

                      This page applies to the Linux Learning Lab web interface, lesson search endpoints, and sandbox command simulation features. The platform is designed to teach Linux command usage, troubleshooting flow, and common operations scenarios.

                      -

                      2. 我们收集哪些数据

                      +

                      1. What data the platform processes

                      -

                      3. 数据如何被使用

                      +

                      2. Why the data is used

                      -

                      4. 数据共享与第三方

                      -

                      Bot 运行依赖以下第三方服务,数据会按功能需要流转:

                      - -

                      我们不会将你的个人数据出售给任何第三方。

                      +

                      3. Sandbox and safety boundary

                      +

                      The command interface is a controlled simulation, not a direct pass-through to a real operating system. The platform blocks clearly unsafe inputs such as destructive shell patterns. Even so, the sandbox endpoints such as /api/run should only be used for learning and testing purposes.

                      -

                      5. 数据保留

                      -

                      我们会在实现功能与保障稳定性的必要范围内保留数据(例如对话上下文、课程数据、运行日志)。保留时间可能因用途不同而不同。

                      +

                      4. Retention and sharing

                      +

                      The platform does not sell personal data. Runtime logs may be kept for a limited period to support debugging and service maintenance. If the platform is hosted behind a proxy or third-party provider, that infrastructure may also process request metadata such as IP address, timestamp, and path.

                      -

                      6. 你的权利

                      +

                      5. Your controls

                      -

                      7. 安全

                      -

                      我们采取合理的技术措施保护数据(例如服务访问控制、最小权限原则)。但任何系统都无法保证绝对安全。

                      - -

                      8. 联系方式

                      -

                      如需数据删除/导出或有隐私问题,请通过你与 Bot 的对话渠道联系管理员。

                      - -
                      -

                      English summary: We process the messages you send to provide replies and task automation. Basic Telegram metadata and operational logs may be stored for reliability and debugging. Data may be transmitted to Telegram and model providers as needed to generate responses. We do not sell personal data. You may request deletion/export where feasible.

                      +

                      6. Contact and maintenance

                      +

                      If you have questions about privacy, logging, or sandbox safety, contact the current service owner. For self-hosted deployments, review the web server, proxy, and hosting platform logging settings alongside this application.

                      diff --git a/server.py b/server.py index 4fe1568..e5ccea6 100755 --- a/server.py +++ b/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()