feat: redesign linux course around learning-first structure
This commit is contained in:
319
COURSE.md
319
COURSE.md
@@ -1,147 +1,274 @@
|
||||
# 📚 Linux 命令学习课程体系(入门 → 高手)
|
||||
# Linux 学习平台课程设计(重构版)
|
||||
|
||||
> 学习路径:**理论 → 演示 → 沙盒练习 → 测试 → 徽章认证**
|
||||
## 设计原则
|
||||
|
||||
这次课程重构,目标不再是“闯关刷题”,而是建立一套**适合系统学习 Linux 的知识结构**。
|
||||
|
||||
核心原则:
|
||||
- **先理解,再操作**
|
||||
- **先场景,再命令**
|
||||
- **先最小可用,再扩展参数**
|
||||
- **练习服务于理解,不反客为主**
|
||||
- **每个模块都能迁移到真实工作场景**
|
||||
|
||||
---
|
||||
|
||||
## 🌱 Level 1:入门(新手村)
|
||||
## 新课程结构
|
||||
|
||||
### 🎯 目标:熟悉终端、查看文件、当前目录、简单操作
|
||||
课程分为 6 大模块,不再按简单等级推进,而按学习逻辑推进。
|
||||
|
||||
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|
||||
|------|------|---------|-------------|---------|
|
||||
| 1 | `pwd` - 我在哪? | 当前工作目录 | `pwd` 返回 `/sandbox` | 问:你现在的位置是? |
|
||||
| 2 | `ls` - 看看周围 | 列出目录内容 | `ls` 显示 `users projects logs` | 找出 `/sandbox` 下有几个子目录? |
|
||||
| 3 | `cd` - 移动位置 | 切换目录 | `cd users` → 进入用户区 | 从 `/sandbox` 进到 `/projects` |
|
||||
| 4 | `echo` - 说话 | 打印文本 | `echo "Hello Linux"` | 打印你的名字 |
|
||||
| 5 | `cat` - 看内容 | 查看文件内容 | `cat users/alice.txt` | 读出 alice.txt 的内容 |
|
||||
### 模块 1:建立 Linux 基本认知
|
||||
目标:先搞清楚“我在哪、我看到什么、我怎么移动”。
|
||||
|
||||
**徽章**:` beginner_1 ← 目录旅行者`
|
||||
包含内容:
|
||||
- 什么是终端 / Shell
|
||||
- 什么是当前目录
|
||||
- 什么是绝对路径 / 相对路径
|
||||
- `pwd`
|
||||
- `ls`
|
||||
- `cd`
|
||||
- `echo`
|
||||
- `cat`
|
||||
|
||||
输出能力:
|
||||
- 能在目录中定位自己
|
||||
- 能读懂基础路径
|
||||
- 能查看基础文件内容
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Level 2:文件操作(手艺人)
|
||||
### 模块 2:文件与目录操作
|
||||
目标:建立文件系统操作能力。
|
||||
|
||||
### 🎯 目标:创建、复制、移动、删除(安全版)、查看
|
||||
包含内容:
|
||||
- 文件与目录的区别
|
||||
- 创建 / 复制 / 移动 / 删除
|
||||
- `mkdir`
|
||||
- `touch`
|
||||
- `cp`
|
||||
- `mv`
|
||||
- `rm`
|
||||
- `stat`
|
||||
|
||||
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|
||||
|------|------|---------|-------------|---------|
|
||||
| 6 | `touch` - 创建空文件 | 创建新文件 | `touch mynote.txt` | 创建一个名为 `test.txt` 的文件 |
|
||||
| 7 | `cp` - 复制 | 复制文件/目录 | `cp users/alice.txt backup/` | 复制 `project/backend/app.py` 到 `archive/` |
|
||||
| 8 | `mv` - 移动/重命名 | 移动或重命名 | `mv old.txt new.txt` | 把 `logs/access.log` 重命名为 `old_access.log` |
|
||||
| 9 | `mkdir` - 创建目录 | 创建级联目录 | `mkdir -p a/b/c` | 创建 `myproject/src/main` |
|
||||
| 10 | `head/tail` - 看头尾 | 查看文件前/后几行 | `tail -n 5 logs/access.log` | 查看 `app.py` 最后 3 行 |
|
||||
|
||||
**徽章**:` beginner_2 ← 文件管理员`
|
||||
输出能力:
|
||||
- 能完成日常文件整理
|
||||
- 能理解“创建、备份、迁移、清理”四种动作
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Level 3:搜索高手(情报员)
|
||||
### 模块 3:阅读与筛选信息
|
||||
目标:学会从文件和日志中找信息。
|
||||
|
||||
### 🎯 目标:快速定位、查找、筛选内容
|
||||
包含内容:
|
||||
- `head`
|
||||
- `tail`
|
||||
- `grep`
|
||||
- `wc`
|
||||
- `sort`
|
||||
- `find`
|
||||
- 日志查看思路
|
||||
- 搜索与过滤思路
|
||||
|
||||
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|
||||
|------|------|---------|-------------|---------|
|
||||
| 11 | `grep` - 搜索关键词 | 正则匹配文本 | `grep "Linux" *.txt` | 在 `users/` 下找包含 "Alice" 的文件 |
|
||||
| 12 | `find` - 按条件找文件 | 时间/大小/类型 | `find /logs -type f -name "*.log"` | 找出所有 `.py` 文件 |
|
||||
| 13 | `du` - 查看占用 | 磁盘使用情况 | `du -sh *` | 评估 `/projects` 每个子目录大小 |
|
||||
| 14 | `sort` - 排序 | 排序输出 | `ls \| sort` | 按文件大小升序排列 `/logs` |
|
||||
| 15 | `wc` - 统计 | 行/词/字节数 | `wc -l app.py` | `find /projects -type f \| wc -l` 有几个文件? |
|
||||
|
||||
**徽章**:` intermediate_1 ← 情报专家`
|
||||
输出能力:
|
||||
- 能读日志
|
||||
- 能查关键词
|
||||
- 能定位配置文件
|
||||
- 能做基础统计
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Level 4:文本编辑(文字工作者)
|
||||
### 模块 4:系统状态与资源认知
|
||||
目标:知道系统现在在干什么。
|
||||
|
||||
### 🎯 目标:预览/编辑文本(只读模式)
|
||||
包含内容:
|
||||
- 进程是什么
|
||||
- 磁盘 / 内存 / 挂载点是什么
|
||||
- `ps`
|
||||
- `top`
|
||||
- `df`
|
||||
- `du`
|
||||
- `free`
|
||||
- `mount`
|
||||
- `history`
|
||||
|
||||
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|
||||
|------|------|---------|-------------|---------|
|
||||
| 16 | `nano/vim` 基础 | 只读模式(演示) | `view project/backend/app.py` | 显示文件内容(用 `cat` 替代 vim) |
|
||||
| 17 | `>`/`>>` 重定向 | 输出到文件 | `echo "test" > test.txt` | 把 `grep "def" app.py` 结果保存到 `methods.txt` |
|
||||
| 18 | `|` 管道 | 连接命令 | `cat users/* \| grep Alice` | 找出所有包含 "Linux" 的内容 |
|
||||
|
||||
**徽章**:` intermediate_2 ← 文字工匠`
|
||||
输出能力:
|
||||
- 能做基础排查
|
||||
- 能看懂资源占用
|
||||
- 能理解系统运行状态
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Level 5:高级技巧(小黑客)
|
||||
### 模块 5:网络与服务基础
|
||||
目标:建立 Linux 运维里的连接意识。
|
||||
|
||||
### 🎯 目标:权限、查找大文件、进程、自动化
|
||||
包含内容:
|
||||
- 网络接口是什么
|
||||
- 端口与监听是什么
|
||||
- `ifconfig` / `ip addr`
|
||||
- `ping`
|
||||
- `ss` / `netstat`
|
||||
- `curl`
|
||||
- `wget`
|
||||
- `which` / `whereis`
|
||||
|
||||
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|
||||
|------|------|---------|-------------|---------|
|
||||
| 19 | `find -size` - 找大文件 | 按大小查找 | `find / -size +100M` | 找出 `/logs` 下大于 10KB 的文件 |
|
||||
| 20 | `chmod` 原理 | 权限概念(只读) | 解释 `rwxr-xr-x` | 问:`644` 是什么权限? |
|
||||
| 21 | `ps`/`top` 基础 | 进程概念 | `ps aux \| grep python` | 找出所有 `java` 进程 |
|
||||
| 22 | `history` - 命令历史 | 查看历史 | `history \| tail -n 10` | 看最近 3 条执行的命令 |
|
||||
| 23 | `man` - 查手册 | 查帮助 | `man ls`(模拟) | 问:`ls -a` 是什么作用? |
|
||||
|
||||
**徽章**:` advanced_1 ← 系统法师`
|
||||
输出能力:
|
||||
- 能判断服务通不通
|
||||
- 能理解“域名、端口、监听、请求”的基本关系
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Level 6:实战项目(通关玩家)
|
||||
### 模块 6:权限、习惯与实际运维思维
|
||||
目标:从“会敲命令”过渡到“知道为什么这么做”。
|
||||
|
||||
### 🎯 综合应用:解决真实场景
|
||||
包含内容:
|
||||
- 权限模型基础
|
||||
- `chmod`
|
||||
- `chown`
|
||||
- `chgrp`
|
||||
- `alias`
|
||||
- `export`
|
||||
- `tar`
|
||||
- `crontab`
|
||||
- 常见运维习惯
|
||||
- 风险命令认知
|
||||
|
||||
| 场景 | 任务 | 所需命令 | 难度 |
|
||||
|------|------|----------|------|
|
||||
| 📁 备份项目 | 将 `/projects` 下所有 `.py` 文件备份到 `/backup` | `find`, `cp`, `mkdir` | ⚔️⚔️⚔️ |
|
||||
| 🔎 搜索日志 | 找出所有包含 `"error"` 的日志行 | `grep`, `find`, `cat` | ⚔️⚔️⚔️⚔️ |
|
||||
| 📏 磁盘分析 | 写出 `/projects` 中最大的 3 个文件 | `du`, `sort`, `tail` | ⚔️⚔️ |
|
||||
| 📝 生成报告 | 把所有 `.py` 文件的行数统计保存到 `stats.txt` | `wc`, `find`, `>` | ⚔️⚔️⚔️⚔️⚔️ |
|
||||
| 🔐 权限检查 | 找出所有`.sh` 脚本并检查权限是否为 `755` | `find`, `stat`, `grep` | ⚔️⚔️⚔️⚔️⚔️ |
|
||||
输出能力:
|
||||
- 能理解权限修改的意义
|
||||
- 能建立基础运维安全感
|
||||
- 能开始形成 Linux 使用习惯
|
||||
|
||||
---
|
||||
|
||||
## 🏆 完整通关徽章体系
|
||||
## 每个课时的新结构
|
||||
|
||||
```bash
|
||||
beginner_1 ← 目录旅行者 (pwd/ls/cd)
|
||||
beginner_2 ← 文件管理员 (touch/cp/mv/mkdir)
|
||||
intermediate_1 ← 情报专家 (grep/find/du/sort/wc)
|
||||
intermediate_2 ← 文字工匠 (cat/echo/pipe/redirection)
|
||||
advanced_1 ← 系统法师 (find-size/chmod/ps/history/man)
|
||||
expert_1 ← 实战大师 (综合项目通关)
|
||||
legend ← Linux 大师 (所有课程 + 心得分享)
|
||||
```
|
||||
每一课统一用下面 6 段结构:
|
||||
|
||||
### 1. 学什么
|
||||
一句话说清这个命令/主题在解决什么问题。
|
||||
|
||||
### 2. 为什么重要
|
||||
它在真实 Linux 使用、开发、运维里有什么价值。
|
||||
|
||||
### 3. 核心知识点
|
||||
包括:
|
||||
- 命令作用
|
||||
- 常见参数
|
||||
- 常见组合
|
||||
- 输出怎么看
|
||||
- 容易误解的点
|
||||
|
||||
### 4. 最小示例
|
||||
给 2~4 个最有代表性的示例。
|
||||
|
||||
### 5. 常见场景
|
||||
把命令放进真实场景里:
|
||||
- 查配置
|
||||
- 查日志
|
||||
- 找文件
|
||||
- 看资源
|
||||
- 改权限
|
||||
|
||||
### 6. 练习题
|
||||
练习题不再喧宾夺主,而是用于确认理解。
|
||||
|
||||
---
|
||||
|
||||
## 📝 测验模式设计
|
||||
## 练习设计原则
|
||||
|
||||
每个课时结束后,自动弹出:
|
||||
练习题分成 3 类:
|
||||
|
||||
```bash
|
||||
🎯 当前任务:_____________
|
||||
💡 提示:_________________
|
||||
(stdin) > [输入命令]
|
||||
### A. 理解题
|
||||
检验有没有理解命令用途。
|
||||
例:
|
||||
- 查看当前目录应该用什么命令?
|
||||
- 为什么 `ls -a` 会比 `ls` 多看到东西?
|
||||
|
||||
✅ 回答正确!获得经验值 +100
|
||||
❌ 还未达标!提示:试试 `xxx`
|
||||
```
|
||||
### B. 操作题
|
||||
检验能否正确写出命令。
|
||||
例:
|
||||
- 进入 `/tmp`
|
||||
- 查找 `/etc` 下所有 `.conf` 文件
|
||||
|
||||
答对 3 次 → 解锁下一关
|
||||
### C. 场景题
|
||||
检验是否能把命令放进真实问题中。
|
||||
例:
|
||||
- 日志太大,不想全看,只看最后 20 行怎么办?
|
||||
- 想找出包含 `error` 的日志应该怎么做?
|
||||
|
||||
---
|
||||
|
||||
## 🎯 课程特色
|
||||
## 页面呈现结构(学习优先)
|
||||
|
||||
- ✅ **零风险沙盒**:所有命令在虚拟环境中执行,不会影响真实系统
|
||||
- ✅ **闯关式学习**:从入门到高手,逐步解锁新技能
|
||||
- ✅ **即时反馈**:答对/错都有针对性提示
|
||||
- ✅ **实战导向**:每个级别都有真实业务场景
|
||||
- ✅ **徽章认证**:每完成一个阶段获得专属徽章
|
||||
前端页面不再以“终端交互”为主,而改为:
|
||||
|
||||
### 左侧:课程目录
|
||||
- 模块
|
||||
- 小节
|
||||
- 学习进度
|
||||
|
||||
### 中间:学习正文
|
||||
- 概念讲解
|
||||
- 示例
|
||||
- 场景
|
||||
- 总结
|
||||
|
||||
### 右侧:知识辅助
|
||||
- 关键概念
|
||||
- 易错点
|
||||
- 相关命令
|
||||
- 推荐练习
|
||||
|
||||
### 底部:练习区
|
||||
- 简洁练习,不抢正文
|
||||
- 只做必要反馈
|
||||
- 重点是“学完再练”
|
||||
|
||||
---
|
||||
|
||||
需要我根据这个课程体系开始实现吗?包括:
|
||||
1. `COURSE_TASKS.json`(所有练习题)
|
||||
2. 沙盒模拟器 `sandbox.py`
|
||||
3. 熟悉 `server.py` 重构
|
||||
4. UI 改造(闯关式界面)
|
||||
5. `README.md` 使用文档
|
||||
## 新平台定位
|
||||
|
||||
还是先调整下课程内容?😄
|
||||
重构后的 Linux 平台定位为:
|
||||
|
||||
> **Linux 系统学习平台 + 轻量练习环境**
|
||||
|
||||
不是刷题站,也不是单纯命令模拟器。
|
||||
|
||||
目标用户看到平台后,应该感受到:
|
||||
- 这是能认真学东西的
|
||||
- 不是只会点按钮
|
||||
- 不是只会猜答案
|
||||
- 学完真的能迁移到实际 Linux 使用场景
|
||||
|
||||
---
|
||||
|
||||
## 重构顺序
|
||||
|
||||
### 第一阶段:课程蓝图重构
|
||||
- 重写课程结构
|
||||
- 重写模块划分
|
||||
- 重写题目组织方式
|
||||
|
||||
### 第二阶段:前 3 个模块内容重写
|
||||
- Linux 基本认知
|
||||
- 文件与目录操作
|
||||
- 阅读与筛选信息
|
||||
|
||||
### 第三阶段:页面重构
|
||||
- 课程目录页
|
||||
- 学习正文页
|
||||
- 轻练习区
|
||||
|
||||
### 第四阶段:后续模块补齐
|
||||
- 系统状态
|
||||
- 网络基础
|
||||
- 权限与运维习惯
|
||||
|
||||
---
|
||||
|
||||
## 当前结论
|
||||
|
||||
这次不是“继续补旧平台”,而是:
|
||||
|
||||
> **把 Linux 平台从“交互导向”改造成“知识导向”的系统学习平台。**
|
||||
|
||||
后续所有改动,以这个文档为准。
|
||||
|
||||
@@ -1,191 +1,284 @@
|
||||
{
|
||||
"meta": {
|
||||
"version": "3.0",
|
||||
"title": "Linux 运维工程师完整教程",
|
||||
"author": "PMClaw",
|
||||
"updated": "2026-03-06",
|
||||
"description": "覆盖菜鸟教程 Linux 全部内容,从入门到精通",
|
||||
"total_levels": 12,
|
||||
"total_challenges": 80
|
||||
"version": "4.0",
|
||||
"title": "Linux 系统学习课程(重构版)",
|
||||
"author": "OpenClaw Dev",
|
||||
"updated": "2026-03-10",
|
||||
"description": "强调知识理解、场景迁移与轻量练习的 Linux 学习课程",
|
||||
"module_count": 6,
|
||||
"total_lessons": 18,
|
||||
"total_exercises": 54
|
||||
},
|
||||
"levels": [
|
||||
"modules": [
|
||||
{
|
||||
"id": "level_1_basic",
|
||||
"title": "🌱 Level 1: Linux 基础入门",
|
||||
"description": "Linux 简介、目录结构、基本操作",
|
||||
"challenges": [
|
||||
{"id": "l1_1_pwd", "title": "查看当前目录", "description": "使用 pwd 命令显示当前工作目录的完整路径", "hint": "直接输入 pwd", "success_test": "'/' in output", "solution": ["pwd"], "success_msg": "📍 定位成功!"},
|
||||
{"id": "l1_2_ls", "title": "列出目录内容", "description": "使用 ls 命令查看当前目录下的文件和文件夹", "hint": "输入 ls", "success_test": "len(output) > 0", "solution": ["ls"], "success_msg": "📂 目录内容已显示!"},
|
||||
{"id": "l1_3_ls_l", "title": "详细列表", "description": "使用 ls -l 显示详细信息,包括权限、所有者、大小等", "hint": "ls -l", "success_test": "'total' in output or '-' in output[:20]", "solution": ["ls -l"], "success_msg": "📋 详细信息已获取!"},
|
||||
{"id": "l1_4_ls_a", "title": "显示隐藏文件", "description": "使用 ls -a 显示包括隐藏文件在内的所有文件", "hint": "ls -a", "success_test": "'.' in output and '..' in output", "solution": ["ls -a"], "success_msg": "👻 隐藏文件已显示!"},
|
||||
{"id": "l1_5_cd", "title": "切换目录", "description": "使用 cd 命令进入 /tmp 目录", "hint": "cd /tmp", "success_test": "cwd == '/tmp'", "solution": ["cd /tmp"], "success_msg": "🚶 目录切换成功!"},
|
||||
{"id": "l1_6_cd_back", "title": "返回上级目录", "description": "使用 cd .. 返回上级目录", "hint": "cd ..", "success_test": "cwd != '/tmp'", "solution": ["cd .."], "success_msg": "⬆️ 返回成功!"},
|
||||
{"id": "l1_7_cd_home", "title": "返回用户主目录", "description": "使用 cd ~ 或 cd 返回用户主目录", "hint": "cd ~", "success_test": "'home' in cwd or cwd == '/'", "solution": ["cd ~", "cd"], "success_msg": "🏠 已回到家目录!"},
|
||||
{"id": "l1_8_clear", "title": "清屏", "description": "使用 clear 命令清屏", "hint": "clear", "success_test": "cmd == 'clear'", "solution": ["clear"], "success_msg": "🧹 屏幕已清空!"}
|
||||
"id": "module_1_foundation",
|
||||
"title": "模块 1:建立 Linux 基本认知",
|
||||
"summary": "先理解终端、目录、路径和最基础命令,建立 Linux 使用的空间感。",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "m1_l1_pwd",
|
||||
"title": "认识当前目录:pwd",
|
||||
"goal": "理解当前工作目录的意义,知道自己在文件系统中的位置。",
|
||||
"why_it_matters": "很多 Linux 操作依赖路径。如果不知道自己当前在哪,后续命令容易出错。",
|
||||
"concepts": [
|
||||
"当前工作目录",
|
||||
"绝对路径与相对路径",
|
||||
"为什么要先定位再操作"
|
||||
],
|
||||
"command": "pwd",
|
||||
"examples": [
|
||||
"pwd",
|
||||
"cd /tmp && pwd"
|
||||
],
|
||||
"pitfalls": [
|
||||
"以为终端默认总在同一个目录",
|
||||
"不分清当前目录和目标目录"
|
||||
],
|
||||
"scenarios": [
|
||||
"切目录后确认自己到了哪里",
|
||||
"写脚本前确认当前运行位置"
|
||||
],
|
||||
"exercises": [
|
||||
{
|
||||
"id": "m1_l1_e1",
|
||||
"type": "understanding",
|
||||
"question": "查看当前工作目录应该使用什么命令?",
|
||||
"answer": "pwd"
|
||||
},
|
||||
{
|
||||
"id": "m1_l1_e2",
|
||||
"type": "operation",
|
||||
"title": "输出当前目录",
|
||||
"hint": "直接输入 pwd",
|
||||
"success_test": "cmd == 'pwd'",
|
||||
"solution": ["pwd"],
|
||||
"success_msg": "你已经能确认自己所在的位置了。"
|
||||
},
|
||||
{
|
||||
"id": "m1_l1_e3",
|
||||
"type": "scenario",
|
||||
"question": "如果你不确定自己当前在哪个目录,第一反应应该做什么?",
|
||||
"answer": "先执行 pwd 确认当前目录"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m1_l2_ls",
|
||||
"title": "看见目录内容:ls",
|
||||
"goal": "理解 ls 的作用,并掌握查看隐藏文件和详细信息的基本方式。",
|
||||
"why_it_matters": "Linux 下很多探索行为都从 ls 开始,它决定你如何观察目录结构。",
|
||||
"concepts": [
|
||||
"目录内容查看",
|
||||
"隐藏文件",
|
||||
"长列表信息"
|
||||
],
|
||||
"command": "ls",
|
||||
"examples": [
|
||||
"ls",
|
||||
"ls -la",
|
||||
"ls -lh /etc"
|
||||
],
|
||||
"pitfalls": [
|
||||
"误以为 ls 看不到的文件就不存在",
|
||||
"不会区分普通 ls 和 ls -l 的用途"
|
||||
],
|
||||
"scenarios": [
|
||||
"排查目录里到底有哪些文件",
|
||||
"检查配置目录中是否有隐藏文件"
|
||||
],
|
||||
"exercises": [
|
||||
{
|
||||
"id": "m1_l2_e1",
|
||||
"type": "understanding",
|
||||
"question": "为什么 ls -a 会比 ls 多看到一些文件?",
|
||||
"answer": "因为它会显示隐藏文件,包括以点开头的文件"
|
||||
},
|
||||
{
|
||||
"id": "m1_l2_e2",
|
||||
"type": "operation",
|
||||
"title": "列出当前目录内容",
|
||||
"hint": "输入 ls",
|
||||
"success_test": "cmd == 'ls'",
|
||||
"solution": ["ls"],
|
||||
"success_msg": "你已经会观察目录内容了。"
|
||||
},
|
||||
{
|
||||
"id": "m1_l2_e3",
|
||||
"type": "operation",
|
||||
"title": "显示隐藏文件和详细信息",
|
||||
"hint": "使用 ls -la",
|
||||
"success_test": "cmd == 'ls -la' or cmd == 'ls -al'",
|
||||
"solution": ["ls -la", "ls -al"],
|
||||
"success_msg": "你已经会用更完整的方式查看目录了。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m1_l3_cd_cat_echo",
|
||||
"title": "移动、读文件、输出文本",
|
||||
"goal": "掌握 cd、cat、echo 这些最基础但最常用的命令。",
|
||||
"why_it_matters": "这三个命令几乎贯穿 Linux 入门阶段的所有练习。",
|
||||
"concepts": [
|
||||
"切换目录",
|
||||
"读取文件",
|
||||
"输出文本与变量"
|
||||
],
|
||||
"command": "cd / cat / echo",
|
||||
"examples": [
|
||||
"cd /tmp",
|
||||
"cat /etc/hosts",
|
||||
"echo Hello Linux"
|
||||
],
|
||||
"pitfalls": [
|
||||
"把 cd 和 ls 混用",
|
||||
"用 cat 去看过大的文件",
|
||||
"不知道 echo 也常用于脚本调试"
|
||||
],
|
||||
"scenarios": [
|
||||
"进入指定目录继续操作",
|
||||
"快速读取配置文件",
|
||||
"验证变量和命令输出"
|
||||
],
|
||||
"exercises": [
|
||||
{
|
||||
"id": "m1_l3_e1",
|
||||
"type": "operation",
|
||||
"title": "进入 /tmp 目录",
|
||||
"hint": "cd /tmp",
|
||||
"success_test": "cmd == 'cd /tmp' and cwd == '/tmp'",
|
||||
"solution": ["cd /tmp"],
|
||||
"success_msg": "你已经能切换到目标目录了。"
|
||||
},
|
||||
{
|
||||
"id": "m1_l3_e2",
|
||||
"type": "operation",
|
||||
"title": "读取 hosts 文件",
|
||||
"hint": "cat /etc/hosts",
|
||||
"success_test": "cmd == 'cat /etc/hosts' and 'localhost' in output",
|
||||
"solution": ["cat /etc/hosts"],
|
||||
"success_msg": "你已经会读取基础文本文件了。"
|
||||
},
|
||||
{
|
||||
"id": "m1_l3_e3",
|
||||
"type": "operation",
|
||||
"title": "输出 Hello Linux",
|
||||
"hint": "echo Hello Linux",
|
||||
"success_test": "cmd == 'echo Hello Linux' and 'Hello Linux' in output",
|
||||
"solution": ["echo Hello Linux"],
|
||||
"success_msg": "你已经掌握了最基础的文本输出命令。"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_2_file",
|
||||
"title": "📁 Level 2: 文件与目录管理",
|
||||
"description": "创建、删除、复制、移动文件和目录",
|
||||
"challenges": [
|
||||
{"id": "l2_1_mkdir", "title": "创建目录", "description": "使用 mkdir 创建 /tmp/testdir 目录", "hint": "mkdir /tmp/testdir", "success_test": "exists('/tmp/testdir')", "solution": ["mkdir /tmp/testdir"], "success_msg": "📂 目录创建成功!"},
|
||||
{"id": "l2_2_mkdir_p", "title": "递归创建目录", "description": "使用 mkdir -p 创建 /tmp/a/b/c 多级目录", "hint": "mkdir -p /tmp/a/b/c", "success_test": "exists('/tmp/a/b/c')", "solution": ["mkdir -p /tmp/a/b/c"], "success_msg": "📁 多级目录创建成功!"},
|
||||
{"id": "l2_3_touch", "title": "创建空文件", "description": "使用 touch 创建 /tmp/test.txt 文件", "hint": "touch /tmp/test.txt", "success_test": "exists('/tmp/test.txt')", "solution": ["touch /tmp/test.txt"], "success_msg": "📄 文件创建成功!"},
|
||||
{"id": "l2_4_cp", "title": "复制文件", "description": "将 /etc/hosts 复制到 /tmp/hosts.bak", "hint": "cp /etc/hosts /tmp/hosts.bak", "success_test": "exists('/tmp/hosts.bak')", "solution": ["cp /etc/hosts /tmp/hosts.bak"], "success_msg": "📋 文件复制成功!"},
|
||||
{"id": "l2_5_cp_r", "title": "复制目录", "description": "使用 cp -r 复制 /etc/skel 到 /tmp/skel_backup", "hint": "cp -r /etc/skel /tmp/skel_backup", "success_test": "exists('/tmp/skel_backup')", "solution": ["cp -r /etc/skel /tmp/skel_backup"], "success_msg": "📁 目录复制成功!"},
|
||||
{"id": "l2_6_mv", "title": "移动文件", "description": "将 /tmp/test.txt 移动到 /tmp/testdir/", "hint": "mv /tmp/test.txt /tmp/testdir/", "success_test": "exists('/tmp/testdir/test.txt')", "solution": ["mv /tmp/test.txt /tmp/testdir/"], "success_msg": "🚚 文件移动成功!"},
|
||||
{"id": "l2_7_mv_rename", "title": "重命名文件", "description": "将 /tmp/hosts.bak 重命名为 /tmp/hosts.backup", "hint": "mv /tmp/hosts.bak /tmp/hosts.backup", "success_test": "exists('/tmp/hosts.backup')", "solution": ["mv /tmp/hosts.bak /tmp/hosts.backup"], "success_msg": "✏️ 重命名成功!"},
|
||||
{"id": "l2_8_rm", "title": "删除文件", "description": "删除 /tmp/hosts.backup 文件", "hint": "rm /tmp/hosts.backup", "success_test": "not exists('/tmp/hosts.backup')", "solution": ["rm /tmp/hosts.backup"], "success_msg": "🗑️ 文件删除成功!"},
|
||||
{"id": "l2_9_rm_r", "title": "删除目录", "description": "使用 rm -r 删除 /tmp/skel_backup 目录", "hint": "rm -r /tmp/skel_backup", "success_test": "not exists('/tmp/skel_backup')", "solution": ["rm -r /tmp/skel_backup"], "success_msg": "🗑️ 目录删除成功!"},
|
||||
{"id": "l2_10_rm_rf", "title": "强制删除", "description": "使用 rm -rf 强制删除 /tmp/a 目录及其所有内容", "hint": "rm -rf /tmp/a", "success_test": "not exists('/tmp/a')", "solution": ["rm -rf /tmp/a"], "success_msg": "💥 强制删除成功!"}
|
||||
"id": "module_2_filesystem",
|
||||
"title": "模块 2:文件与目录操作",
|
||||
"summary": "围绕创建、复制、移动、删除和查看文件属性建立文件系统操作能力。",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "m2_l1_create",
|
||||
"title": "创建文件与目录:mkdir / touch",
|
||||
"goal": "理解目录和文件的创建逻辑,学会递归创建多级目录。",
|
||||
"why_it_matters": "很多项目初始化、环境准备都从创建目录结构开始。",
|
||||
"concepts": ["目录创建", "多级目录", "空文件创建"],
|
||||
"command": "mkdir / touch",
|
||||
"examples": ["mkdir demo", "mkdir -p /tmp/a/b/c", "touch notes.txt"],
|
||||
"pitfalls": ["忘记使用 -p 创建多级目录", "目标父目录不存在时 touch 失败"],
|
||||
"scenarios": ["初始化项目目录结构", "创建占位文件和日志文件"],
|
||||
"exercises": [
|
||||
{"id": "m2_l1_e1", "type": "operation", "title": "递归创建目录", "hint": "mkdir -p /tmp/a/b/c", "success_test": "cmd == 'mkdir -p /tmp/a/b/c' and exists('/tmp/a/b/c')", "solution": ["mkdir -p /tmp/a/b/c"], "success_msg": "多级目录创建成功。"},
|
||||
{"id": "m2_l1_e2", "type": "operation", "title": "创建空文件", "hint": "touch /tmp/a/b/c/readme.txt", "success_test": "cmd == 'touch /tmp/a/b/c/readme.txt' and exists('/tmp/a/b/c/readme.txt')", "solution": ["touch /tmp/a/b/c/readme.txt"], "success_msg": "空文件创建成功。"},
|
||||
{"id": "m2_l1_e3", "type": "scenario", "question": "为什么 mkdir -p 适合项目初始化?", "answer": "因为它可以一次创建多级目录,即使上层目录不存在也能自动补齐"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m2_l2_move_copy_delete",
|
||||
"title": "复制、移动与删除:cp / mv / rm",
|
||||
"goal": "理解文件操作中的备份、迁移、重命名和清理。",
|
||||
"why_it_matters": "日常 Linux 使用里最常见的就是处理文件的生命周期。",
|
||||
"concepts": ["复制与备份", "移动与重命名", "删除风险"],
|
||||
"command": "cp / mv / rm",
|
||||
"examples": ["cp /etc/hosts /tmp/hosts.bak", "mv old.txt new.txt", "rm -r /tmp/testdir"],
|
||||
"pitfalls": ["把删除当成移动", "对目录使用 cp 却忘记 -r", "rm -rf 风险极高"],
|
||||
"scenarios": ["做配置备份", "整理日志文件", "清理无用目录"],
|
||||
"exercises": [
|
||||
{"id": "m2_l2_e1", "type": "operation", "title": "复制 hosts 文件", "hint": "cp /etc/hosts /tmp/hosts.bak", "success_test": "cmd == 'cp /etc/hosts /tmp/hosts.bak' and exists('/tmp/hosts.bak')", "solution": ["cp /etc/hosts /tmp/hosts.bak"], "success_msg": "文件备份成功。"},
|
||||
{"id": "m2_l2_e2", "type": "operation", "title": "重命名备份文件", "hint": "mv /tmp/hosts.bak /tmp/hosts.backup", "success_test": "cmd == 'mv /tmp/hosts.bak /tmp/hosts.backup' and exists('/tmp/hosts.backup')", "solution": ["mv /tmp/hosts.bak /tmp/hosts.backup"], "success_msg": "文件重命名成功。"},
|
||||
{"id": "m2_l2_e3", "type": "understanding", "question": "为什么 rm -rf 是高风险命令?", "answer": "因为它会递归并强制删除文件和目录,执行错误会造成不可恢复的数据丢失"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m2_l3_stat_permissions",
|
||||
"title": "认识文件属性:stat 与权限基础",
|
||||
"goal": "开始理解文件属性和权限表达。",
|
||||
"why_it_matters": "文件权限是 Linux 系统安全和协作的重要基础。",
|
||||
"concepts": ["文件元信息", "权限三元组", "目录与文件权限差异"],
|
||||
"command": "stat / chmod",
|
||||
"examples": ["stat /etc/hosts", "chmod 755 script.sh", "chmod +x run.sh"],
|
||||
"pitfalls": ["不了解 755 / 644 的含义", "给不该执行的文件随意加执行权限"],
|
||||
"scenarios": ["检查脚本是否可执行", "排查权限导致的运行失败"],
|
||||
"exercises": [
|
||||
{"id": "m2_l3_e1", "type": "operation", "title": "查看 hosts 属性", "hint": "stat /etc/hosts", "success_test": "cmd == 'stat /etc/hosts' and 'File:' in output", "solution": ["stat /etc/hosts"], "success_msg": "你已经会查看文件属性了。"},
|
||||
{"id": "m2_l3_e2", "type": "understanding", "question": "755 和 644 最核心的区别是什么?", "answer": "755 允许拥有者读写执行,其他人读执行;644 没有执行权限"},
|
||||
{"id": "m2_l3_e3", "type": "operation", "title": "给文件添加执行权限", "hint": "chmod +x /tmp/a/b/c/readme.txt", "success_test": "cmd == 'chmod +x /tmp/a/b/c/readme.txt'", "solution": ["chmod +x /tmp/a/b/c/readme.txt"], "success_msg": "你已经完成了权限修改练习。"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_3_view",
|
||||
"title": "👁️ Level 3: 文件内容查看",
|
||||
"description": "cat、more、less、head、tail 等查看命令",
|
||||
"challenges": [
|
||||
{"id": "l3_1_cat", "title": "查看文件内容", "description": "使用 cat 查看 /etc/passwd 文件内容", "hint": "cat /etc/passwd", "success_test": "'root' in output", "solution": ["cat /etc/passwd"], "success_msg": "📖 文件内容已显示!"},
|
||||
{"id": "l3_2_cat_n", "title": "显示行号", "description": "使用 cat -n 显示 /etc/passwd 并带行号", "hint": "cat -n /etc/passwd", "success_test": "'1 ' in output or ' 1 ' in output", "solution": ["cat -n /etc/passwd"], "success_msg": "🔢 行号已显示!"},
|
||||
{"id": "l3_3_head", "title": "查看开头", "description": "使用 head 查看 /etc/passwd 前 10 行", "hint": "head /etc/passwd", "success_test": "len(output.split('\\n')) >= 5", "solution": ["head /etc/passwd"], "success_msg": "👆 开头内容已显示!"},
|
||||
{"id": "l3_4_head_n", "title": "指定行数", "description": "使用 head -n 5 查看前 5 行", "hint": "head -n 5 /etc/passwd", "success_test": "len(output.split('\\n')) <= 7", "solution": ["head -5 /etc/passwd", "head -n 5 /etc/passwd"], "success_msg": "📏 指定行数已显示!"},
|
||||
{"id": "l3_5_tail", "title": "查看结尾", "description": "使用 tail 查看 /etc/passwd 最后 10 行", "hint": "tail /etc/passwd", "success_test": "len(output) > 10", "solution": ["tail /etc/passwd"], "success_msg": "👇 结尾内容已显示!"},
|
||||
{"id": "l3_6_tail_f", "title": "实时追踪", "description": "使用 tail -f 实时查看 /var/log/syslog(按 Ctrl+C 退出)", "hint": "tail -f /var/log/syslog", "success_test": "cmd.startswith('tail -f')", "solution": ["tail -f /var/log/syslog"], "success_msg": "👀 实时追踪模式!"},
|
||||
{"id": "l3_7_more", "title": "分页查看", "description": "使用 more 分页查看 /etc/passwd", "hint": "more /etc/passwd", "success_test": "cmd.startswith('more')", "solution": ["more /etc/passwd"], "success_msg": "📄 分页查看模式!"},
|
||||
{"id": "l3_8_less", "title": "可滚动查看", "description": "使用 less 查看 /etc/passwd(支持上下滚动)", "hint": "less /etc/passwd", "success_test": "cmd.startswith('less')", "solution": ["less /etc/passwd"], "success_msg": "📜 可滚动查看模式!"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_4_permission",
|
||||
"title": "🔐 Level 4: 文件权限管理",
|
||||
"description": "chmod、chown、chgrp 权限控制",
|
||||
"challenges": [
|
||||
{"id": "l4_1_ls_l", "title": "查看权限", "description": "使用 ls -l /etc/passwd 查看文件权限", "hint": "ls -l /etc/passwd", "success_test": "'-' in output or 'r' in output", "solution": ["ls -l /etc/passwd"], "success_msg": "👀 权限信息已显示!"},
|
||||
{"id": "l4_2_chmod_755", "title": "修改权限为755", "description": "将 /tmp/testdir 权限改为 rwxr-xr-x (755)", "hint": "chmod 755 /tmp/testdir", "success_test": "cmd == 'chmod 755 /tmp/testdir'", "solution": ["chmod 755 /tmp/testdir"], "success_msg": "🔐 权限修改成功!"},
|
||||
{"id": "l4_3_chmod_644", "title": "修改权限为644", "description": "将 /tmp/testdir/test.txt 权限改为 rw-r--r-- (644)", "hint": "chmod 644 /tmp/testdir/test.txt", "success_test": "cmd == 'chmod 644 /tmp/testdir/test.txt'", "solution": ["chmod 644 /tmp/testdir/test.txt"], "success_msg": "🔐 权限修改成功!"},
|
||||
{"id": "l4_4_chmod_x", "title": "添加执行权限", "description": "给 /tmp/testdir/test.txt 添加可执行权限", "hint": "chmod +x /tmp/testdir/test.txt", "success_test": "cmd == 'chmod +x /tmp/testdir/test.txt'", "solution": ["chmod +x /tmp/testdir/test.txt"], "success_msg": "⚡ 执行权限已添加!"},
|
||||
{"id": "l4_5_chmod_r", "title": "移除读权限", "description": "移除 /tmp/testdir/test.txt 的读权限", "hint": "chmod -r /tmp/testdir/test.txt", "success_test": "cmd == 'chmod -r /tmp/testdir/test.txt'", "solution": ["chmod -r /tmp/testdir/test.txt"], "success_msg": "🚫 读权限已移除!"},
|
||||
{"id": "l4_6_chown", "title": "修改所有者", "description": "将 /tmp/testdir/test.txt 所有者改为 root", "hint": "chown root /tmp/testdir/test.txt", "success_test": "cmd.startswith('chown')", "solution": ["chown root /tmp/testdir/test.txt"], "success_msg": "👤 所有者已修改!"},
|
||||
{"id": "l4_7_chgrp", "title": "修改所属组", "description": "将 /tmp/testdir/test.txt 所属组改为 root", "hint": "chgrp root /tmp/testdir/test.txt", "success_test": "cmd.startswith('chgrp')", "solution": ["chgrp root /tmp/testdir/test.txt"], "success_msg": "👥 所属组已修改!"},
|
||||
{"id": "l4_8_chown_r", "title": "递归修改", "description": "递归修改 /tmp/testdir 及其所有内容的所有者为 root", "hint": "chown -R root /tmp/testdir", "success_test": "cmd.startswith('chown -R')", "solution": ["chown -R root /tmp/testdir"], "success_msg": "🔄 递归修改成功!"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_5_search",
|
||||
"title": "🔍 Level 5: 搜索与过滤",
|
||||
"description": "find、grep、which、whereis 搜索命令",
|
||||
"challenges": [
|
||||
{"id": "l5_1_find_name", "title": "按名称查找", "description": "在 /etc 下查找所有 .conf 文件", "hint": "find /etc -name '*.conf'", "success_test": "'.conf' in output", "solution": ["find /etc -name '*.conf'"], "success_msg": "🔍 文件已找到!"},
|
||||
{"id": "l5_2_find_type", "title": "按类型查找", "description": "查找 /tmp 下的所有目录", "hint": "find /tmp -type d", "success_test": "cmd.startswith('find') and '-type d' in cmd", "solution": ["find /tmp -type d"], "success_msg": "📁 目录已找到!"},
|
||||
{"id": "l5_3_find_size", "title": "按大小查找", "description": "查找 /var/log 下大于 1M 的文件", "hint": "find /var/log -size +1M", "success_test": "cmd.startswith('find') and '-size' in cmd", "solution": ["find /var/log -size +1M"], "success_msg": "📏 大文件已找到!"},
|
||||
{"id": "l5_4_find_exec", "title": "执行操作", "description": "查找 /tmp 下所有 .log 文件并删除", "hint": "find /tmp -name '*.log' -exec rm {} \\;", "success_test": "'-exec' in cmd", "solution": ["find /tmp -name '*.log' -exec rm {} \\;"], "success_msg": "🗑️ 操作执行成功!"},
|
||||
{"id": "l5_5_grep", "title": "文本搜索", "description": "在 /etc/passwd 中搜索 root 用户", "hint": "grep root /etc/passwd", "success_test": "'root' in output", "solution": ["grep root /etc/passwd"], "success_msg": "🎯 搜索成功!"},
|
||||
{"id": "l5_6_grep_i", "title": "忽略大小写", "description": "使用 grep -i 搜索 ROOT(不区分大小写)", "hint": "grep -i root /etc/passwd", "success_test": "'-i' in cmd", "solution": ["grep -i root /etc/passwd"], "success_msg": "🔤 忽略大小写搜索成功!"},
|
||||
{"id": "l5_7_grep_v", "title": "反向匹配", "description": "显示 /etc/passwd 中不包含 nologin 的行", "hint": "grep -v nologin /etc/passwd", "success_test": "'-v' in cmd", "solution": ["grep -v nologin /etc/passwd"], "success_msg": "🔄 反向匹配成功!"},
|
||||
{"id": "l5_8_grep_n", "title": "显示行号", "description": "使用 grep -n 显示匹配行的行号", "hint": "grep -n root /etc/passwd", "success_test": "'-n' in cmd and ':' in output", "solution": ["grep -n root /etc/passwd"], "success_msg": "🔢 行号已显示!"},
|
||||
{"id": "l5_9_which", "title": "查找命令位置", "description": "查找 ls 命令的位置", "hint": "which ls", "success_test": "'/bin' in output or '/usr' in output", "solution": ["which ls"], "success_msg": "📍 命令位置已找到!"},
|
||||
{"id": "l5_10_whereis", "title": "查找相关文件", "description": "使用 whereis 查找 ls 命令的相关文件", "hint": "whereis ls", "success_test": "cmd.startswith('whereis')", "solution": ["whereis ls"], "success_msg": "📚 相关文件已找到!"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_6_user",
|
||||
"title": "👥 Level 6: 用户和组管理",
|
||||
"description": "useradd、usermod、passwd、group 管理",
|
||||
"challenges": [
|
||||
{"id": "l6_1_whoami", "title": "查看当前用户", "description": "使用 whoami 查看当前登录用户名", "hint": "whoami", "success_test": "len(output.strip()) > 0", "solution": ["whoami"], "success_msg": "👤 当前用户已显示!"},
|
||||
{"id": "l6_2_id", "title": "查看用户ID", "description": "使用 id 查看当前用户的 UID、GID 和所属组", "hint": "id", "success_test": "'uid' in output.lower()", "solution": ["id"], "success_msg": "🆔 用户信息已显示!"},
|
||||
{"id": "l6_3_w", "title": "查看登录用户", "description": "使用 w 查看当前登录系统的用户", "hint": "w", "success_test": "'USER' in output or 'TTY' in output", "solution": ["w"], "success_msg": "👥 登录用户已显示!"},
|
||||
{"id": "l6_4_last", "title": "查看登录历史", "description": "使用 last 查看最近的登录记录", "hint": "last", "success_test": "len(output) > 10", "solution": ["last"], "success_msg": "📜 登录历史已显示!"},
|
||||
{"id": "l6_5_cat_passwd", "title": "查看用户列表", "description": "查看 /etc/passwd 文件了解系统用户", "hint": "cat /etc/passwd", "success_test": "':' in output", "solution": ["cat /etc/passwd"], "success_msg": "📋 用户列表已显示!"},
|
||||
{"id": "l6_6_cat_group", "title": "查看组列表", "description": "查看 /etc/group 文件了解系统用户组", "hint": "cat /etc/group", "success_test": "':' in output", "solution": ["cat /etc/group"], "success_msg": "📋 组列表已显示!"},
|
||||
{"id": "l6_7_passwd", "title": "修改密码", "description": "使用 passwd 修改当前用户密码(输入当前密码和新密码)", "hint": "passwd", "success_test": "cmd == 'passwd'", "solution": ["passwd"], "success_msg": "🔑 密码修改命令已执行!"},
|
||||
{"id": "l6_8_su", "title": "切换用户", "description": "使用 su - 切换到 root 用户", "hint": "su -", "success_test": "cmd.startswith('su')", "solution": ["su -", "su"], "success_msg": "🔄 用户切换命令已执行!"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_7_disk",
|
||||
"title": "💾 Level 7: 磁盘管理",
|
||||
"description": "df、du、fdisk、mount 磁盘操作",
|
||||
"challenges": [
|
||||
{"id": "l7_1_df", "title": "查看磁盘空间", "description": "使用 df 查看文件系统磁盘空间使用情况", "hint": "df", "success_test": "'Filesystem' in output or '文件系统' in output", "solution": ["df"], "success_msg": "💾 磁盘空间已显示!"},
|
||||
{"id": "l7_2_df_h", "title": "人类可读格式", "description": "使用 df -h 以人类可读格式显示磁盘空间", "hint": "df -h", "success_test": "'G' in output or 'M' in output or 'K' in output", "solution": ["df -h"], "success_msg": "📊 磁盘空间(易读格式)!"},
|
||||
{"id": "l7_3_du", "title": "查看目录大小", "description": "使用 du 查看 /tmp 目录的大小", "hint": "du /tmp", "success_test": "len(output) > 0", "solution": ["du /tmp"], "success_msg": "📏 目录大小已显示!"},
|
||||
{"id": "l7_4_du_sh", "title": "查看总大小", "description": "使用 du -sh 查看 /tmp 的总大小", "hint": "du -sh /tmp", "success_test": "'-sh' in cmd", "solution": ["du -sh /tmp"], "success_msg": "📊 总大小已显示!"},
|
||||
{"id": "l7_5_du_sort", "title": "排序查看", "description": "查看 /tmp 下最大的 5 个目录", "hint": "du -sh /tmp/* | sort -rh | head -5", "success_test": "'sort' in cmd", "solution": ["du -sh /tmp/* | sort -rh | head -5"], "success_msg": "🏆 最大目录已排序!"},
|
||||
{"id": "l7_6_mount", "title": "查看挂载点", "description": "使用 mount 查看已挂载的文件系统", "hint": "mount", "success_test": "'on' in output or 'type' in output", "solution": ["mount"], "success_msg": "🔗 挂载点已显示!"},
|
||||
{"id": "l7_7_fdisk", "title": "查看分区表", "description": "使用 fdisk -l 查看磁盘分区表(需要 root 权限)", "hint": "fdisk -l", "success_test": "cmd.startswith('fdisk')", "solution": ["fdisk -l"], "success_msg": "💿 分区表已显示!"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_8_process",
|
||||
"title": "⚙️ Level 8: 进程管理",
|
||||
"description": "ps、top、kill、nohup 进程控制",
|
||||
"challenges": [
|
||||
{"id": "l8_1_ps", "title": "查看进程", "description": "使用 ps 查看当前用户的进程", "hint": "ps", "success_test": "'PID' in output", "solution": ["ps"], "success_msg": "📋 进程列表已显示!"},
|
||||
{"id": "l8_2_ps_aux", "title": "查看所有进程", "description": "使用 ps aux 查看系统所有进程", "hint": "ps aux", "success_test": "'%CPU' in output or 'RSS' in output", "solution": ["ps aux"], "success_msg": "📊 所有进程已显示!"},
|
||||
{"id": "l8_3_ps_grep", "title": "查找特定进程", "description": "查找包含 ssh 的进程", "hint": "ps aux | grep ssh", "success_test": "'|' in cmd", "solution": ["ps aux | grep ssh"], "success_msg": "🎯 进程已找到!"},
|
||||
{"id": "l8_4_top", "title": "实时进程监控", "description": "使用 top 实时查看进程(按 q 退出)", "hint": "top", "success_test": "cmd == 'top'", "solution": ["top"], "success_msg": "📈 实时监控已启动!"},
|
||||
{"id": "l8_5_kill", "title": "终止进程", "description": "使用 kill 命令(查看 PID 后终止)", "hint": "kill PID", "success_test": "cmd.startswith('kill')", "solution": ["kill 1234"], "success_msg": "💀 终止命令已执行!"},
|
||||
{"id": "l8_6_kill9", "title": "强制终止", "description": "使用 kill -9 强制终止进程", "hint": "kill -9 PID", "success_test": "'-9' in cmd", "solution": ["kill -9 1234"], "success_msg": "💥 强制终止已执行!"},
|
||||
{"id": "l8_7_pkill", "title": "按名称终止", "description": "使用 pkill 按进程名终止", "hint": "pkill process_name", "success_test": "cmd.startswith('pkill')", "solution": ["pkill python"], "success_msg": "🎯 按名称终止已执行!"},
|
||||
{"id": "l8_8_nohup", "title": "后台运行", "description": "使用 nohup 让命令在后台运行", "hint": "nohup command &", "success_test": "cmd.startswith('nohup')", "solution": ["nohup sleep 10 &"], "success_msg": "🌙 后台运行已设置!"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_9_network",
|
||||
"title": "🌐 Level 9: 网络管理",
|
||||
"description": "ifconfig、ping、netstat、curl 网络命令",
|
||||
"challenges": [
|
||||
{"id": "l9_1_ifconfig", "title": "查看网卡", "description": "使用 ifconfig 查看网络接口配置", "hint": "ifconfig", "success_test": "'inet' in output or 'eth' in output or 'lo' in output", "solution": ["ifconfig"], "success_msg": "🎴 网卡信息已显示!"},
|
||||
{"id": "l9_2_ip_addr", "title": "现代方式查看", "description": "使用 ip addr 查看网络接口", "hint": "ip addr", "success_test": "cmd.startswith('ip addr')", "solution": ["ip addr"], "success_msg": "🎴 网络接口已显示!"},
|
||||
{"id": "l9_3_ping", "title": "测试连通性", "description": "ping 127.0.0.1 测试本地网络", "hint": "ping -c 4 127.0.0.1", "success_test": "cmd.startswith('ping')", "solution": ["ping -c 4 127.0.0.1"], "success_msg": "📡 Ping 测试完成!"},
|
||||
{"id": "l9_4_netstat", "title": "查看网络连接", "description": "使用 netstat -tlnp 查看监听端口", "hint": "netstat -tlnp", "success_test": "'LISTEN' in output or 'tcp' in output.lower()", "solution": ["netstat -tlnp"], "success_msg": "🔗 网络连接已显示!"},
|
||||
{"id": "l9_5_ss", "title": "现代方式查看端口", "description": "使用 ss -tlnp 查看监听端口", "hint": "ss -tlnp", "success_test": "cmd.startswith('ss')", "solution": ["ss -tlnp"], "success_msg": "🔗 端口信息已显示!"},
|
||||
{"id": "l9_6_curl", "title": "HTTP 请求", "description": "使用 curl 访问 http://localhost:8080", "hint": "curl http://localhost:8080", "success_test": "cmd.startswith('curl')", "solution": ["curl http://localhost:8080"], "success_msg": "🌐 HTTP 请求已发送!"},
|
||||
{"id": "l9_7_wget", "title": "下载文件", "description": "使用 wget 下载网页", "hint": "wget http://localhost:8080 -O /tmp/test.html", "success_test": "cmd.startswith('wget')", "solution": ["wget http://localhost:8080 -O /tmp/test.html"], "success_msg": "📥 文件下载命令已执行!"},
|
||||
{"id": "l9_8_traceroute", "title": "路由追踪", "description": "使用 traceroute 追踪到目标的路由", "hint": "traceroute 8.8.8.8", "success_test": "cmd.startswith('traceroute') or cmd.startswith('tracepath')", "solution": ["traceroute 8.8.8.8"], "success_msg": "🛤️ 路由追踪已启动!"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_10_package",
|
||||
"title": "📦 Level 10: 软件包管理",
|
||||
"description": "yum、apt、rpm、dpkg 包管理",
|
||||
"challenges": [
|
||||
{"id": "l10_1_yum_list", "title": "列出已安装包", "description": "使用 yum list installed 查看已安装的包", "hint": "yum list installed", "success_test": "cmd.startswith('yum')", "solution": ["yum list installed"], "success_msg": "📋 已安装包列表!"},
|
||||
{"id": "l10_2_yum_search", "title": "搜索包", "description": "使用 yum search 搜索 nginx", "hint": "yum search nginx", "success_test": "cmd.startswith('yum search')", "solution": ["yum search nginx"], "success_msg": "🔍 包搜索完成!"},
|
||||
{"id": "l10_3_rpm_q", "title": "查询包信息", "description": "使用 rpm -q 查询 bash 包", "hint": "rpm -q bash", "success_test": "cmd.startswith('rpm')", "solution": ["rpm -q bash"], "success_msg": "📦 包信息已显示!"},
|
||||
{"id": "l10_4_apt_update", "title": "更新包列表", "description": "使用 apt update 更新包列表", "hint": "apt update", "success_test": "cmd.startswith('apt update')", "solution": ["apt update"], "success_msg": "🔄 包列表已更新!"},
|
||||
{"id": "l10_5_dpkg_l", "title": "列出 Debian 包", "description": "使用 dpkg -l 列出已安装的包", "hint": "dpkg -l", "success_test": "cmd.startswith('dpkg')", "solution": ["dpkg -l"], "success_msg": "📋 Debian 包列表!"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_11_vim",
|
||||
"title": "✏️ Level 11: Vi/Vim 编辑器",
|
||||
"description": "vim 基本操作和常用命令",
|
||||
"challenges": [
|
||||
{"id": "l11_1_vim", "title": "打开文件", "description": "使用 vim 打开 /tmp/test.txt", "hint": "vim /tmp/test.txt", "success_test": "cmd.startswith('vim') or cmd.startswith('vi')", "solution": ["vim /tmp/test.txt"], "success_msg": "📝 Vim 已启动!"},
|
||||
{"id": "l11_2_vim_i", "title": "插入模式", "description": "在 vim 中按 i 进入插入模式", "hint": "按 i 键", "success_test": "cmd == 'i'", "solution": ["i"], "success_msg": "⌨️ 插入模式!"},
|
||||
{"id": "l11_3_vim_esc", "title": "退出插入模式", "description": "按 Esc 退出插入模式", "hint": "按 Esc 键", "success_test": "cmd == 'esc' or cmd == 'Esc'", "solution": ["esc"], "success_msg": "🚪 退出插入模式!"},
|
||||
{"id": "l11_4_vim_wq", "title": "保存退出", "description": "输入 :wq 保存并退出", "hint": ":wq", "success_test": "cmd == ':wq'", "solution": [":wq"], "success_msg": "💾 保存并退出!"},
|
||||
{"id": "l11_5_vim_q", "title": "不保存退出", "description": "输入 :q! 强制退出不保存", "hint": ":q!", "success_test": "cmd == ':q!'", "solution": [":q!"], "success_msg": "🚪 强制退出!"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "level_12_shell",
|
||||
"title": "🚀 Level 12: Shell 脚本编程",
|
||||
"description": "Shell 脚本基础、变量、循环、条件判断",
|
||||
"challenges": [
|
||||
{"id": "l12_1_echo_var", "title": "输出变量", "description": "使用 echo 输出环境变量 $HOME", "hint": "echo $HOME", "success_test": "'$HOME' in cmd or 'echo $' in cmd", "solution": ["echo $HOME"], "success_msg": "🔤 变量已输出!"},
|
||||
{"id": "l12_2_env", "title": "查看环境变量", "description": "使用 env 查看所有环境变量", "hint": "env", "success_test": "cmd == 'env'", "solution": ["env"], "success_msg": "🌍 环境变量已显示!"},
|
||||
{"id": "l12_3_export", "title": "设置环境变量", "description": "使用 export 设置 MYVAR=test", "hint": "export MYVAR=test", "success_test": "cmd.startswith('export')", "solution": ["export MYVAR=test"], "success_msg": "📤 环境变量已设置!"},
|
||||
{"id": "l12_4_alias", "title": "命令别名", "description": "创建别名 ll='ls -l'", "hint": "alias ll='ls -l'", "success_test": "cmd.startswith('alias')", "solution": ["alias ll='ls -l'"], "success_msg": "🏷️ 别名已创建!"},
|
||||
{"id": "l12_5_date", "title": "显示日期", "description": "使用 date 显示当前日期时间", "hint": "date", "success_test": "cmd == 'date'", "solution": ["date"], "success_msg": "📅 日期已显示!"},
|
||||
{"id": "l12_6_cal", "title": "显示日历", "description": "使用 cal 显示本月日历", "hint": "cal", "success_test": "cmd == 'cal'", "solution": ["cal"], "success_msg": "📆 日历已显示!"},
|
||||
{"id": "l12_7_bc", "title": "计算器", "description": "使用 bc 计算 1+1", "hint": "echo '1+1' | bc", "success_test": "'bc' in cmd", "solution": ["echo '1+1' | bc"], "success_msg": "🧮 计算完成!"},
|
||||
{"id": "l12_8_tar", "title": "打包压缩", "description": "将 /tmp/testdir 打包为 /tmp/testdir.tar.gz", "hint": "tar -czf /tmp/testdir.tar.gz /tmp/testdir", "success_test": "cmd.startswith('tar')", "solution": ["tar -czf /tmp/testdir.tar.gz /tmp/testdir"], "success_msg": "📦 打包完成!"},
|
||||
{"id": "l12_9_untar", "title": "解压", "description": "解压 /tmp/testdir.tar.gz 到 /tmp/extract/", "hint": "tar -xzf /tmp/testdir.tar.gz -C /tmp/extract/", "success_test": "'-x' in cmd or '--extract' in cmd", "solution": ["tar -xzf /tmp/testdir.tar.gz -C /tmp/"], "success_msg": "📂 解压完成!"},
|
||||
{"id": "l12_10_crontab", "title": "定时任务", "description": "使用 crontab -l 查看定时任务", "hint": "crontab -l", "success_test": "cmd.startswith('crontab')", "solution": ["crontab -l"], "success_msg": "⏰ 定时任务已显示!"}
|
||||
"id": "module_3_searching",
|
||||
"title": "模块 3:阅读与筛选信息",
|
||||
"summary": "把 Linux 当成信息检索工具来学,围绕日志、配置和统计建立阅读能力。",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "m3_l1_read_logs",
|
||||
"title": "看文件头尾:head / tail",
|
||||
"goal": "学会快速读取大文件的局部内容。",
|
||||
"why_it_matters": "日志通常很大,不可能总是整份去看。",
|
||||
"concepts": ["查看前几行", "查看后几行", "实时追踪"],
|
||||
"command": "head / tail",
|
||||
"examples": ["head -n 5 /var/log/syslog", "tail -n 20 /var/log/syslog", "tail -f /var/log/syslog"],
|
||||
"pitfalls": ["大文件直接 cat 影响阅读效率", "不会区分查看历史和跟踪新增日志"],
|
||||
"scenarios": ["看配置文件开头", "盯日志尾部排查实时错误"],
|
||||
"exercises": [
|
||||
{"id": "m3_l1_e1", "type": "operation", "title": "查看 syslog 前 5 行", "hint": "head -n 5 /var/log/syslog", "success_test": "(cmd == 'head -n 5 /var/log/syslog' or cmd == 'head -5 /var/log/syslog') and len(output.split('\n')) >= 5", "solution": ["head -n 5 /var/log/syslog", "head -5 /var/log/syslog"], "success_msg": "你已经会局部查看大文件开头了。"},
|
||||
{"id": "m3_l1_e2", "type": "operation", "title": "查看 syslog 最后 3 行", "hint": "tail -n 3 /var/log/syslog", "success_test": "(cmd == 'tail -n 3 /var/log/syslog' or cmd == 'tail -3 /var/log/syslog') and len(output.split('\n')) >= 3", "solution": ["tail -n 3 /var/log/syslog", "tail -3 /var/log/syslog"], "success_msg": "你已经会快速查看日志尾部了。"},
|
||||
{"id": "m3_l1_e3", "type": "scenario", "question": "为什么排查线上报错时更常先用 tail 而不是 cat?", "answer": "因为日志通常很大,tail 可以更快聚焦最近发生的问题"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m3_l2_grep",
|
||||
"title": "关键词搜索:grep",
|
||||
"goal": "理解 grep 作为日志排障和文本定位核心工具的价值。",
|
||||
"why_it_matters": "没有 grep,查日志和配置会慢很多。",
|
||||
"concepts": ["大小写忽略", "显示行号", "反向匹配", "递归搜索"],
|
||||
"command": "grep",
|
||||
"examples": ["grep error /var/log/syslog", "grep -in root /etc/passwd", "grep -v nologin /etc/passwd"],
|
||||
"pitfalls": ["不会结合 -n 定位行号", "不知道 -i 和 -v 的常见用途"],
|
||||
"scenarios": ["查错误日志", "找配置项", "过滤无效行"],
|
||||
"exercises": [
|
||||
{"id": "m3_l2_e1", "type": "operation", "title": "查找 syslog 中的 error", "hint": "grep error /var/log/syslog", "success_test": "cmd == 'grep error /var/log/syslog' and 'error' in output.lower()", "solution": ["grep error /var/log/syslog"], "success_msg": "你已经会在日志里搜关键词了。"},
|
||||
{"id": "m3_l2_e2", "type": "operation", "title": "忽略大小写搜索 root", "hint": "grep -i root /etc/passwd", "success_test": "cmd == 'grep -i root /etc/passwd'", "solution": ["grep -i root /etc/passwd"], "success_msg": "你已经知道如何处理大小写差异了。"},
|
||||
{"id": "m3_l2_e3", "type": "understanding", "question": "grep -n 的意义是什么?", "answer": "显示匹配结果所在的行号,方便快速定位原文位置"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "m3_l3_find_wc_sort",
|
||||
"title": "查找与统计:find / wc / sort",
|
||||
"goal": "建立查找文件和做基础统计的能力。",
|
||||
"why_it_matters": "Linux 的很多效率来自组合式查找与统计。",
|
||||
"concepts": ["按名称查找", "行数字数统计", "排序输出"],
|
||||
"command": "find / wc / sort",
|
||||
"examples": ["find /etc -name '*.conf'", "wc -l /var/log/syslog", "ls | sort"],
|
||||
"pitfalls": ["把 find 和 grep 混淆", "不会根据任务选文件查找还是内容查找"],
|
||||
"scenarios": ["找配置文件", "统计日志行数", "整理输出结果"],
|
||||
"exercises": [
|
||||
{"id": "m3_l3_e1", "type": "operation", "title": "查找 /etc 下所有 .conf 文件", "hint": "find /etc -name '*.conf'", "success_test": "cmd == \"find /etc -name '*.conf'\" and '.conf' in output", "solution": ["find /etc -name '*.conf'"], "success_msg": "你已经会用 find 定位文件了。"},
|
||||
{"id": "m3_l3_e2", "type": "operation", "title": "统计 syslog 行数", "hint": "wc -l /var/log/syslog", "success_test": "cmd == 'wc -l /var/log/syslog' and output.strip().isdigit()", "solution": ["wc -l /var/log/syslog"], "success_msg": "你已经会做基础统计了。"},
|
||||
{"id": "m3_l3_e3", "type": "understanding", "question": "找文件位置应该优先想到 find 还是 grep?为什么?", "answer": "优先用 find,因为这是文件定位问题,不是文件内容搜索问题"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
806
index.html
806
index.html
@@ -3,42 +3,39 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Linux 命令学习平台 - 从入门到精通</title>
|
||||
<title>Linux 系统学习平台</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--primary: #1677ff;
|
||||
--primary-dark: #0f5ed7;
|
||||
--primary-soft: #eef5ff;
|
||||
--secondary: #36a3ff;
|
||||
--accent: #22c55e;
|
||||
--accent: #36a3ff;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--bg: #f4f8ff;
|
||||
--bg: #f5f9ff;
|
||||
--bg-2: #edf4ff;
|
||||
--card: rgba(255,255,255,0.95);
|
||||
--text: #0f172a;
|
||||
--text-2: #334155;
|
||||
--text-3: #64748b;
|
||||
--border: #dbe7f5;
|
||||
--terminal: #0b1220;
|
||||
--terminal-panel: #111b2e;
|
||||
--terminal-text: #d8e7ff;
|
||||
--shadow: 0 18px 45px rgba(15, 94, 215, 0.12);
|
||||
--radius: 20px;
|
||||
--radius: 22px;
|
||||
--terminal: #0b1220;
|
||||
--terminal-soft: #101a2f;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg: #09111f;
|
||||
--bg-2: #0e1930;
|
||||
--card: rgba(17, 27, 46, 0.95);
|
||||
--text: #e8f0ff;
|
||||
--text-2: #cbd8f0;
|
||||
--text-3: #8ea2c8;
|
||||
[data-theme='dark'] {
|
||||
--bg: #08101d;
|
||||
--bg-2: #101b31;
|
||||
--card: rgba(16, 26, 47, 0.96);
|
||||
--text: #edf4ff;
|
||||
--text-2: #cfdbf4;
|
||||
--text-3: #90a5ca;
|
||||
--border: #1d3358;
|
||||
--terminal: #050a13;
|
||||
--terminal-panel: #0b1220;
|
||||
--terminal-text: #d8e7ff;
|
||||
--shadow: 0 20px 55px rgba(2, 8, 23, 0.45);
|
||||
--shadow: 0 18px 55px rgba(2, 8, 23, 0.42);
|
||||
--primary-soft: rgba(22,119,255,.12);
|
||||
}
|
||||
body {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
@@ -47,594 +44,439 @@
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #0f5ed7 0%, #1677ff 48%, #36a3ff 100%);
|
||||
background: linear-gradient(135deg, #0f5ed7 0%, #1677ff 45%, #36a3ff 100%);
|
||||
color: #fff;
|
||||
padding: 18px 24px;
|
||||
box-shadow: 0 12px 35px rgba(22, 119, 255, 0.2);
|
||||
box-shadow: 0 12px 35px rgba(22,119,255,.2);
|
||||
}
|
||||
.header-content {
|
||||
max-width: 1440px;
|
||||
.header-inner {
|
||||
max-width: 1480px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.brand h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
.brand h1 { font-size: 26px; font-weight: 800; }
|
||||
.brand p { margin-top: 6px; opacity: .92; font-size: 13px; }
|
||||
.header-actions { display:flex; gap:10px; flex-wrap:wrap; }
|
||||
.chip-btn {
|
||||
border: none; border-radius: 999px; padding: 10px 15px; cursor: pointer; font-weight: 700;
|
||||
background: rgba(255,255,255,.14); color:#fff;
|
||||
}
|
||||
.brand p {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.header-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
.pill-btn {
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.16);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.pill-btn:hover, .pill-btn.active { background: #fff; color: var(--primary); }
|
||||
.chip-btn:hover, .chip-btn.active { background:#fff; color:var(--primary); }
|
||||
.layout {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: 300px minmax(0, 1fr) 360px;
|
||||
gap: 18px;
|
||||
min-height: calc(100vh - 92px);
|
||||
max-width: 1480px; margin: 0 auto; padding: 20px;
|
||||
display: grid; grid-template-columns: 320px minmax(0, 1fr) 340px; gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
background: var(--card); border:1px solid var(--border); border-radius: var(--radius);
|
||||
box-shadow: var(--shadow); backdrop-filter: blur(10px);
|
||||
}
|
||||
.sidebar, .learning-panel { padding: 18px; }
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 14px;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.sidebar, .content, .aside { padding: 20px; }
|
||||
.title-row { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:14px; }
|
||||
.title-row h2 { font-size: 18px; font-weight: 800; }
|
||||
.muted { color: var(--text-3); }
|
||||
.course-meta {
|
||||
display:grid; grid-template-columns:repeat(2, 1fr); gap:10px; margin-bottom:16px;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
.meta-box {
|
||||
padding: 14px; border-radius: 16px; border:1px solid var(--border);
|
||||
background: linear-gradient(180deg, var(--primary-soft), rgba(255,255,255,.75));
|
||||
}
|
||||
.stat-box {
|
||||
background: linear-gradient(180deg, var(--primary-soft) 0%, rgba(255,255,255,0.7) 100%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
.meta-box .label { font-size:12px; color:var(--text-3); }
|
||||
.meta-box .value { margin-top:6px; font-size:22px; font-weight:800; }
|
||||
.module-item {
|
||||
border:1px solid var(--border); border-radius:18px; margin-bottom:14px; overflow:hidden;
|
||||
background: rgba(255,255,255,.55);
|
||||
}
|
||||
.stat-box .label { font-size: 12px; color: var(--text-3); }
|
||||
.stat-box .value { font-size: 24px; font-weight: 800; margin-top: 6px; }
|
||||
.progress-wrap { margin-bottom: 16px; }
|
||||
.progress-bar {
|
||||
height: 10px; background: rgba(148,163,184,0.15); border-radius: 999px; overflow: hidden;
|
||||
.module-head {
|
||||
padding: 14px 16px; cursor:pointer; display:flex; justify-content:space-between; gap:12px; align-items:center;
|
||||
background: linear-gradient(90deg, #eef6ff 0%, #f8fbff 100%);
|
||||
color: var(--primary-dark); font-weight: 800;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%; background: linear-gradient(90deg, var(--primary), var(--secondary)); transition: width .3s ease;
|
||||
.lesson-list { padding: 12px; }
|
||||
.lesson-item {
|
||||
padding: 12px 14px; border-radius: 14px; cursor:pointer; transition: all .2s ease; margin-bottom:8px;
|
||||
border:1px solid transparent;
|
||||
}
|
||||
.progress-text { margin-top: 8px; font-size: 13px; color: var(--text-3); }
|
||||
.level-header {
|
||||
display: flex; justify-content: space-between; align-items: center; cursor: pointer;
|
||||
padding: 12px 14px; margin-bottom: 10px; border-radius: 14px;
|
||||
background: linear-gradient(90deg, #eef6ff 0%, #f8fbff 100%); color: var(--primary-dark); font-weight: 700;
|
||||
border: 1px solid #d8e7fb;
|
||||
.lesson-item:hover { background: var(--primary-soft); }
|
||||
.lesson-item.active {
|
||||
background: linear-gradient(90deg, var(--primary) 0%, var(--accent) 100%); color:#fff;
|
||||
}
|
||||
.task-list { list-style: none; margin-bottom: 12px; }
|
||||
.task-item {
|
||||
padding: 12px 14px; border-radius: 14px; display: flex; gap: 10px; align-items: start; cursor: pointer;
|
||||
transition: all .2s ease; color: var(--text-2); margin-bottom: 8px;
|
||||
}
|
||||
.task-item:hover { background: var(--primary-soft); }
|
||||
.task-item.active { background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); color: #fff; }
|
||||
.task-item.completed:not(.active) { background: rgba(34,197,94,.08); }
|
||||
.task-status { font-size: 16px; line-height: 1.2; }
|
||||
.main-panel { padding: 20px; display: flex; flex-direction: column; gap: 18px; }
|
||||
.lesson-item .name { font-weight: 700; }
|
||||
.lesson-item .desc { font-size: 12px; margin-top: 5px; opacity: .85; }
|
||||
.hero {
|
||||
padding: 26px; border-radius: 24px;
|
||||
background: linear-gradient(135deg, rgba(22,119,255,.12), rgba(54,163,255,.1));
|
||||
border: 1px solid #d8e7fb;
|
||||
padding: 24px; border-radius: 24px; border:1px solid #d8e7fb;
|
||||
background: linear-gradient(135deg, rgba(22,119,255,.12), rgba(54,163,255,.08));
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.hero h2 { font-size: 30px; margin-bottom: 10px; }
|
||||
.hero p { color: var(--text-3); line-height: 1.8; }
|
||||
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; }
|
||||
.hero h2 { font-size: 32px; margin-bottom: 10px; }
|
||||
.hero p { line-height: 1.9; color: var(--text-3); }
|
||||
.hero-actions { display:flex; gap:12px; flex-wrap:wrap; margin-top:16px; }
|
||||
.btn {
|
||||
border: none; border-radius: 14px; padding: 12px 18px; cursor: pointer; font-size: 14px; font-weight: 700;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.btn-primary { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: #fff; }
|
||||
.btn-primary:hover { transform: translateY(-1px); }
|
||||
.btn-secondary { background: #edf3ff; color: var(--primary-dark); }
|
||||
.btn-warning { background: #fff4df; color: #b76a00; }
|
||||
.task-shell { display: none; flex-direction: column; gap: 18px; }
|
||||
.task-shell.show { display: flex; }
|
||||
.task-meta {
|
||||
display: flex; gap: 10px; flex-wrap: wrap; color: var(--text-3); font-size: 13px;
|
||||
border:none; border-radius:14px; padding: 12px 18px; cursor:pointer; font-weight:800;
|
||||
}
|
||||
.btn-primary { background: linear-gradient(135deg, var(--primary), var(--accent)); color:#fff; }
|
||||
.btn-soft { background: #edf4ff; color: var(--primary-dark); }
|
||||
.lesson-shell { display:none; }
|
||||
.lesson-shell.show { display:block; }
|
||||
.badge-row { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:14px; }
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; padding: 6px 10px; border-radius: 999px; font-size: 12px; font-weight: 700;
|
||||
background: var(--primary-soft); color: var(--primary-dark);
|
||||
display:inline-flex; align-items:center; padding:6px 10px; border-radius:999px;
|
||||
font-size:12px; font-weight:700; background: var(--primary-soft); color: var(--primary-dark);
|
||||
}
|
||||
.task-desc {
|
||||
padding: 18px; border-radius: 18px; background: linear-gradient(180deg, #f8fbff 0%, #f1f7ff 100%);
|
||||
border: 1px solid #dce9f8; line-height: 1.8;
|
||||
.lesson-title { font-size: 30px; font-weight: 800; margin-bottom: 10px; }
|
||||
.panel {
|
||||
border:1px solid var(--border); border-radius:18px; padding:18px; margin-bottom:14px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.8), rgba(248,251,255,.92));
|
||||
}
|
||||
.terminal {
|
||||
background: linear-gradient(180deg, var(--terminal) 0%, var(--terminal-panel) 100%);
|
||||
border-radius: 22px; padding: 16px; color: var(--terminal-text); border: 1px solid rgba(59,130,246,.18);
|
||||
.panel h3 { font-size:16px; font-weight:800; margin-bottom:12px; color: var(--text-2); }
|
||||
.panel p, .panel li { line-height: 1.9; color: var(--text-2); }
|
||||
.panel ul { padding-left: 18px; }
|
||||
.example-list, .exercise-list { display:flex; flex-direction:column; gap:10px; }
|
||||
.code-block {
|
||||
padding: 12px 14px; border-radius: 14px; background:#0b1220; color:#dbeafe; font-family: Consolas, monospace;
|
||||
overflow:auto;
|
||||
}
|
||||
.terminal-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||
.terminal-title { font-weight: 700; color: #b9d6ff; }
|
||||
.cwd-chip {
|
||||
background: rgba(22,119,255,.18); border: 1px solid rgba(96,165,250,.28); color: #dbeafe;
|
||||
font-size: 12px; padding: 6px 10px; border-radius: 999px;
|
||||
.exercise-card {
|
||||
border:1px solid var(--border); border-radius:16px; padding:14px; background: rgba(255,255,255,.85);
|
||||
}
|
||||
.prompt-line {
|
||||
display: flex; align-items: center; gap: 10px; padding: 12px 14px;
|
||||
border-radius: 14px; background: rgba(255,255,255,.03); border: 1px solid rgba(96,165,250,.15);
|
||||
.exercise-type {
|
||||
display:inline-flex; margin-bottom:8px; padding:4px 8px; border-radius:999px; font-size:12px; font-weight:700;
|
||||
background: #eef4ff; color: var(--primary-dark);
|
||||
}
|
||||
.prompt { color: #4ade80; font-weight: 700; font-family: Consolas, monospace; }
|
||||
.command-input {
|
||||
flex: 1; background: transparent; border: none; color: #fff; outline: none; font-family: Consolas, monospace; font-size: 15px;
|
||||
.terminal-box {
|
||||
margin-top: 12px; border-radius: 18px; overflow:hidden; border:1px solid rgba(59,130,246,.18);
|
||||
background: linear-gradient(180deg, var(--terminal) 0%, var(--terminal-soft) 100%);
|
||||
}
|
||||
.terminal-output {
|
||||
margin-top: 12px; min-height: 170px; max-height: 380px; overflow: auto;
|
||||
border-radius: 14px; padding: 14px; background: rgba(2, 6, 23, 0.45);
|
||||
font-family: Consolas, monospace; white-space: pre-wrap; line-height: 1.6;
|
||||
.terminal-header { padding: 12px 14px; color:#b9d6ff; font-weight:700; border-bottom:1px solid rgba(148,163,184,.15); }
|
||||
.terminal-input-row { display:flex; gap:10px; padding: 14px; align-items:center; }
|
||||
.prompt { color:#4ade80; font-family: Consolas, monospace; font-weight:700; }
|
||||
.cmd-input {
|
||||
flex:1; border:none; outline:none; background:transparent; color:#fff; font-family: Consolas, monospace; font-size:15px;
|
||||
}
|
||||
.feedback {
|
||||
display: none; padding: 16px 18px; border-radius: 16px; font-size: 14px; font-weight: 600;
|
||||
.terminal-output { padding: 14px; min-height: 120px; white-space: pre-wrap; color:#dbeafe; font-family: Consolas, monospace; }
|
||||
.feedback { display:none; margin-top: 12px; padding: 14px 16px; border-radius: 14px; font-weight:700; }
|
||||
.feedback.show { display:block; }
|
||||
.feedback.success { background: rgba(34,197,94,.1); border:1px solid rgba(34,197,94,.2); color:#15803d; }
|
||||
.feedback.warn { background: rgba(245,158,11,.1); border:1px solid rgba(245,158,11,.2); color:#b45309; }
|
||||
.aside-card {
|
||||
border:1px solid var(--border); border-radius:18px; padding:16px; margin-bottom:14px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.88), rgba(247,250,255,.92));
|
||||
}
|
||||
.feedback.show { display: block; }
|
||||
.feedback.success { background: rgba(34,197,94,.1); color: #15803d; border: 1px solid rgba(34,197,94,.2); }
|
||||
.feedback.warn { background: rgba(245,158,11,.1); color: #b45309; border: 1px solid rgba(245,158,11,.2); }
|
||||
.feedback.error { background: rgba(239,68,68,.1); color: #b91c1c; border: 1px solid rgba(239,68,68,.2); }
|
||||
.action-row { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.learning-panel .hint-card,
|
||||
.learning-panel .knowledge-card,
|
||||
.learning-panel .milestone-card {
|
||||
padding: 16px; border-radius: 16px; margin-bottom: 14px; border: 1px solid var(--border);
|
||||
.aside-card h3 { font-size:15px; font-weight:800; margin-bottom:10px; }
|
||||
.aside-card li { margin-left: 18px; line-height: 1.8; color: var(--text-2); }
|
||||
.qa-box {
|
||||
padding:12px; border-radius:14px; background: var(--primary-soft); margin-top:10px; color:var(--text-2);
|
||||
}
|
||||
.hint-card { background: linear-gradient(180deg, #f8fbff 0%, #f3f8ff 100%); }
|
||||
.knowledge-card { background: linear-gradient(180deg, #eef6ff 0%, #f8fbff 100%); }
|
||||
.milestone-card { background: linear-gradient(180deg, rgba(34,197,94,.08), rgba(255,255,255,.9)); }
|
||||
.small-title { font-size: 13px; font-weight: 800; color: var(--text-2); margin-bottom: 8px; }
|
||||
.code-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.code-chip {
|
||||
display: block; padding: 10px 12px; border-radius: 12px; background: rgba(15, 23, 42, 0.04); font-family: Consolas, monospace;
|
||||
color: var(--primary-dark); font-size: 13px;
|
||||
}
|
||||
.empty-state { color: var(--text-3); line-height: 1.8; }
|
||||
@media (max-width: 1240px) {
|
||||
.layout { grid-template-columns: 280px minmax(0,1fr); }
|
||||
.learning-panel { display: none; }
|
||||
.layout { grid-template-columns: 320px minmax(0, 1fr); }
|
||||
.aside { display:none; }
|
||||
}
|
||||
@media (max-width: 820px) {
|
||||
@media (max-width: 860px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.sidebar { order: 2; }
|
||||
.main-panel { order: 1; }
|
||||
.header-content { flex-direction: column; align-items: start; }
|
||||
.header-inner { flex-direction: column; align-items: flex-start; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<div class="header-inner">
|
||||
<div class="brand">
|
||||
<h1>🐧 Linux 运维学习平台</h1>
|
||||
<p>内容更系统 · 练习更真实 · 判题更可靠</p>
|
||||
<h1>🐧 Linux 系统学习平台</h1>
|
||||
<p>以知识理解为中心,练习为辅,帮助建立真正可迁移的 Linux 能力。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="pill-btn active" id="learnMode" onclick="setMode('learn')">📖 学习模式</button>
|
||||
<button class="pill-btn" id="practiceMode" onclick="setMode('practice')">⚔️ 实战模式</button>
|
||||
<button class="pill-btn" onclick="resetSandbox()">♻️ 重置环境</button>
|
||||
<button class="pill-btn" onclick="toggleTheme()">🌓 主题</button>
|
||||
<button class="chip-btn active" onclick="setTheme('light')">浅色</button>
|
||||
<button class="chip-btn" onclick="setTheme('dark')">深色</button>
|
||||
<button class="chip-btn" onclick="resetSandbox()">重置沙盒</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="sidebar card">
|
||||
<div class="section-title">📚 学习地图</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-box">
|
||||
<div class="label">课程关卡</div>
|
||||
<div class="value" id="levelCount">12</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="label">总任务数</div>
|
||||
<div class="value" id="taskCount">80</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="label">已完成</div>
|
||||
<div class="value" id="doneCount">0</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="label">当前模式</div>
|
||||
<div class="value" id="modeLabel" style="font-size:18px">学习</div>
|
||||
</div>
|
||||
<div class="title-row">
|
||||
<h2>课程地图</h2>
|
||||
<span class="muted" id="courseVersion">v4.0</span>
|
||||
</div>
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
|
||||
<div class="progress-text" id="progressText">0 / 80 完成 (0%)</div>
|
||||
<div class="course-meta">
|
||||
<div class="meta-box"><div class="label">模块数</div><div class="value" id="moduleCount">6</div></div>
|
||||
<div class="meta-box"><div class="label">课时数</div><div class="value" id="lessonCount">18</div></div>
|
||||
<div class="meta-box"><div class="label">练习数</div><div class="value" id="exerciseCount">54</div></div>
|
||||
<div class="meta-box"><div class="label">学习方向</div><div class="value" style="font-size:18px">知识优先</div></div>
|
||||
</div>
|
||||
<div id="courseNav"></div>
|
||||
</aside>
|
||||
|
||||
<main class="main-panel card">
|
||||
<main class="content card">
|
||||
<section class="hero" id="heroPanel">
|
||||
<h2>从命令入门,到运维思维</h2>
|
||||
<h2>先把 Linux 学明白,再去练</h2>
|
||||
<p>
|
||||
这个版本不再只是“输入答案过题”。你现在会得到更真实的沙盒环境、状态可变的文件系统、
|
||||
更合理的后端判题,以及更清晰的学习建议。
|
||||
这个版本不再以“闯关感”作为核心,而是把 Linux 当成一门真正要学懂的技能来组织:
|
||||
每个课时会先说明为什么重要、核心知识点是什么、常见误区在哪里,再给最小示例和少量练习。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<button class="btn btn-primary" onclick="startLearning()">开始第一关</button>
|
||||
<button class="btn btn-secondary" onclick="jumpToFirstUnfinished()">继续上次进度</button>
|
||||
<button class="btn btn-primary" onclick="openFirstLesson()">从第一课开始</button>
|
||||
<button class="btn btn-soft" onclick="openFirstPracticeLesson()">直接看第一组练习</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="task-shell" id="taskShell">
|
||||
<div>
|
||||
<div class="task-meta">
|
||||
<span class="badge" id="taskLevel">Level 1</span>
|
||||
<span class="badge" id="taskIndex">1 / 80</span>
|
||||
<span class="badge" id="taskModeBadge">当前:学习模式</span>
|
||||
</div>
|
||||
<h2 style="margin-top:12px; font-size:28px;" id="taskTitle">任务标题</h2>
|
||||
<section class="lesson-shell" id="lessonShell">
|
||||
<div class="badge-row">
|
||||
<span class="badge" id="moduleBadge">模块 1</span>
|
||||
<span class="badge" id="commandBadge">命令</span>
|
||||
</div>
|
||||
<div class="lesson-title" id="lessonTitle">课时标题</div>
|
||||
<p class="muted" id="lessonSummary" style="margin-bottom: 18px;"></p>
|
||||
|
||||
<div class="panel">
|
||||
<h3>这一课学什么</h3>
|
||||
<p id="lessonGoal"></p>
|
||||
</div>
|
||||
|
||||
<div class="task-desc" id="taskDescription"></div>
|
||||
|
||||
<div class="terminal">
|
||||
<div class="terminal-top">
|
||||
<div class="terminal-title">🖥️ Linux 终端</div>
|
||||
<div class="cwd-chip" id="cwdChip">当前目录:/</div>
|
||||
</div>
|
||||
<div class="prompt-line">
|
||||
<span class="prompt">sandbox@linux:$</span>
|
||||
<input id="cmdInput" class="command-input" placeholder="输入 Linux 命令,例如 ls -la /etc" onkeypress="if(event.key==='Enter') executeCommand()" />
|
||||
</div>
|
||||
<div class="terminal-output" id="cmdOutput">欢迎进入 Linux 沙盒。你可以安全练习命令,不会影响真实系统。</div>
|
||||
<div class="panel">
|
||||
<h3>为什么重要</h3>
|
||||
<p id="lessonWhy"></p>
|
||||
</div>
|
||||
|
||||
<div class="feedback" id="feedbackBox"></div>
|
||||
<div class="panel">
|
||||
<h3>核心知识点</h3>
|
||||
<ul id="conceptList"></ul>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<button class="btn btn-primary" onclick="executeCommand()">▶ 执行命令</button>
|
||||
<button class="btn btn-secondary" onclick="showHint()">💡 提示</button>
|
||||
<button class="btn btn-warning" onclick="showAnswer()">👀 查看答案</button>
|
||||
<button class="btn btn-secondary" onclick="nextTask()">下一题 →</button>
|
||||
<div class="panel">
|
||||
<h3>最小示例</h3>
|
||||
<div class="example-list" id="exampleList"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>常见误区</h3>
|
||||
<ul id="pitfallList"></ul>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>典型使用场景</h3>
|
||||
<ul id="scenarioList"></ul>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>本课练习</h3>
|
||||
<div class="exercise-list" id="exerciseList"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<aside class="learning-panel card">
|
||||
<div class="section-title">🧠 学习辅助</div>
|
||||
<div class="hint-card">
|
||||
<div class="small-title">本题提示</div>
|
||||
<div id="hintBox" class="empty-state">选择一题后,这里会显示更聚焦的提示。</div>
|
||||
<aside class="aside card">
|
||||
<div class="aside-card">
|
||||
<h3>学习建议</h3>
|
||||
<ul>
|
||||
<li>先理解“命令解决什么问题”,再记参数。</li>
|
||||
<li>不要一开始背太多选项,先掌握最常用组合。</li>
|
||||
<li>日志、配置、目录、进程,是 Linux 学习的四大核心场景。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="knowledge-card">
|
||||
<div class="small-title">命令讲解</div>
|
||||
<div id="knowledgeBox" class="empty-state">选择任务后,这里会提供命令用途、示例与注意点。</div>
|
||||
<div class="aside-card">
|
||||
<h3>当前课时提示</h3>
|
||||
<div id="asideHint" class="muted">打开课程后,这里会显示与当前课时相关的补充提醒。</div>
|
||||
</div>
|
||||
<div class="milestone-card">
|
||||
<div class="small-title">当前目标</div>
|
||||
<div id="milestoneBox" class="empty-state">开始第一关,逐步建立 Linux 运维基本功。</div>
|
||||
<div class="aside-card">
|
||||
<h3>理解型问题</h3>
|
||||
<div id="qaBox" class="muted">选择课时后,这里会显示理解题和场景题,帮助你形成真正的命令认识。</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let COURSE_DATA = null;
|
||||
let currentTask = null;
|
||||
let currentMode = localStorage.getItem('linux_mode') || 'learn';
|
||||
let currentTheme = localStorage.getItem('linux_theme') || 'light';
|
||||
let completedTasks = JSON.parse(localStorage.getItem('linux_completed') || '[]');
|
||||
let currentCwd = '/';
|
||||
|
||||
const COMMAND_KNOWLEDGE = {
|
||||
pwd: {
|
||||
desc: 'pwd 用来显示当前工作目录,是你在文件系统中的当前位置。',
|
||||
examples: ['pwd', 'cd /tmp && pwd'],
|
||||
tip: '一旦不知道自己在哪,先敲 pwd。'
|
||||
},
|
||||
ls: {
|
||||
desc: 'ls 用于查看目录内容。-l 看详细信息,-a 看隐藏文件。',
|
||||
examples: ['ls', 'ls -la /etc', 'ls -lh /var/log'],
|
||||
tip: 'ls -la 是最常用组合。'
|
||||
},
|
||||
cd: {
|
||||
desc: 'cd 用于切换目录,配合 pwd 使用能快速建立路径感。',
|
||||
examples: ['cd /tmp', 'cd ..', 'cd ~'],
|
||||
tip: 'cd .. 返回上级目录,cd ~ 回主目录。'
|
||||
},
|
||||
cat: {
|
||||
desc: 'cat 直接输出文件内容,适合看短文件。大文件更适合 less。',
|
||||
examples: ['cat /etc/passwd', 'cat -n /etc/hosts'],
|
||||
tip: '看配置文件或小文本时很顺手。'
|
||||
},
|
||||
grep: {
|
||||
desc: 'grep 用于文本搜索,是日志排障和配置定位的核心命令。',
|
||||
examples: ['grep root /etc/passwd', 'grep -in error /var/log/syslog'],
|
||||
tip: '日志场景优先想到 grep -n / grep -i。'
|
||||
},
|
||||
find: {
|
||||
desc: 'find 用于按名字、类型、大小等条件查找文件。',
|
||||
examples: ["find /etc -name '*.conf'", 'find /tmp -type d'],
|
||||
tip: '排查文件位置时,find 非常强。'
|
||||
},
|
||||
mkdir: {
|
||||
desc: 'mkdir 用于创建目录。-p 可以递归创建多级目录。',
|
||||
examples: ['mkdir /tmp/demo', 'mkdir -p /tmp/a/b/c'],
|
||||
tip: '多级目录优先加 -p。'
|
||||
},
|
||||
chmod: {
|
||||
desc: 'chmod 用于改权限,是 Linux 文件安全基础。',
|
||||
examples: ['chmod 755 script.sh', 'chmod +x run.sh'],
|
||||
tip: '755 给目录或脚本很常见。'
|
||||
},
|
||||
ps: {
|
||||
desc: 'ps 用于查看进程,运维定位服务状态离不开它。',
|
||||
examples: ['ps', 'ps aux', 'ps -ef'],
|
||||
tip: 'ps aux | grep 进程名 是经典组合。'
|
||||
}
|
||||
};
|
||||
let COURSE = null;
|
||||
let currentLesson = null;
|
||||
let currentTheme = localStorage.getItem('linux_course_theme') || 'light';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
applyTheme(currentTheme);
|
||||
setMode(currentMode, false);
|
||||
await loadCourseData();
|
||||
setTheme(currentTheme, false);
|
||||
await loadCourse();
|
||||
renderCourseNav();
|
||||
updateStats();
|
||||
});
|
||||
|
||||
async function loadCourseData() {
|
||||
const res = await fetch('/api/tasks');
|
||||
COURSE_DATA = await res.json();
|
||||
document.getElementById('levelCount').textContent = COURSE_DATA.meta.total_levels || COURSE_DATA.levels.length;
|
||||
document.getElementById('taskCount').textContent = getAllTasks().length;
|
||||
async function loadCourse() {
|
||||
const res = await fetch('/api/course');
|
||||
COURSE = await res.json();
|
||||
document.getElementById('courseVersion').textContent = 'v' + (COURSE.meta.version || '4.0');
|
||||
document.getElementById('moduleCount').textContent = COURSE.meta.module_count || COURSE.modules.length;
|
||||
document.getElementById('lessonCount').textContent = COURSE.meta.total_lessons || getAllLessons().length;
|
||||
document.getElementById('exerciseCount').textContent = COURSE.meta.total_exercises || getAllExercises().length;
|
||||
}
|
||||
|
||||
function getAllTasks() {
|
||||
return COURSE_DATA ? COURSE_DATA.levels.flatMap(level => level.challenges.map(task => ({...task, levelTitle: level.title, levelId: level.id}))) : [];
|
||||
function getAllLessons() {
|
||||
return COURSE.modules.flatMap(module => module.lessons.map(lesson => ({ ...lesson, moduleId: module.id, moduleTitle: module.title, moduleSummary: module.summary })));
|
||||
}
|
||||
|
||||
function setMode(mode, save = true) {
|
||||
currentMode = mode;
|
||||
document.getElementById('learnMode').classList.toggle('active', mode === 'learn');
|
||||
document.getElementById('practiceMode').classList.toggle('active', mode === 'practice');
|
||||
document.getElementById('modeLabel').textContent = mode === 'learn' ? '学习' : '实战';
|
||||
document.getElementById('taskModeBadge').textContent = `当前:${mode === 'learn' ? '学习模式' : '实战模式'}`;
|
||||
if (save) localStorage.setItem('linux_mode', mode);
|
||||
if (currentTask) renderTask(currentTask);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
applyTheme(currentTheme);
|
||||
localStorage.setItem('linux_theme', currentTheme);
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
function getAllExercises() {
|
||||
return getAllLessons().flatMap(lesson => lesson.exercises.map(ex => ({ ...ex, lessonTitle: lesson.title, moduleTitle: lesson.moduleTitle })));
|
||||
}
|
||||
|
||||
function renderCourseNav() {
|
||||
if (!COURSE_DATA) return;
|
||||
const nav = document.getElementById('courseNav');
|
||||
nav.innerHTML = COURSE_DATA.levels.map((level, levelIndex) => {
|
||||
const items = level.challenges.map(task => {
|
||||
const active = currentTask && currentTask.id === task.id;
|
||||
const completed = completedTasks.includes(task.id);
|
||||
nav.innerHTML = COURSE.modules.map((module, index) => {
|
||||
const lessonHtml = module.lessons.map(lesson => {
|
||||
const active = currentLesson && currentLesson.id === lesson.id;
|
||||
return `
|
||||
<li class="task-item ${active ? 'active' : ''} ${completed ? 'completed' : ''}" onclick="selectTask('${level.id}','${task.id}')">
|
||||
<span class="task-status">${completed ? '✅' : (active ? '▶' : '○')}</span>
|
||||
<div>
|
||||
<div style="font-weight:700;">${task.title}</div>
|
||||
<div style="font-size:12px; opacity:.85; margin-top:3px;">${(task.description || task.desc || '').slice(0, 28)}...</div>
|
||||
</div>
|
||||
</li>`;
|
||||
<div class="lesson-item ${active ? 'active' : ''}" onclick="openLesson('${module.id}', '${lesson.id}')">
|
||||
<div class="name">${lesson.title}</div>
|
||||
<div class="desc">${lesson.goal}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div style="margin-bottom:12px;">
|
||||
<div class="level-header" onclick="toggleLevel(${levelIndex})">
|
||||
<span>${level.title}</span>
|
||||
<span>共 ${level.challenges.length} 题</span>
|
||||
<div class="module-item">
|
||||
<div class="module-head" onclick="toggleModule(${index})">
|
||||
<span>${module.title}</span>
|
||||
<span>${module.lessons.length} 课</span>
|
||||
</div>
|
||||
<ul class="task-list" id="level-${levelIndex}">${items}</ul>
|
||||
<div class="lesson-list" id="module-${index}">${lessonHtml}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function toggleLevel(index) {
|
||||
const el = document.getElementById(`level-${index}`);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
function toggleModule(index) {
|
||||
const el = document.getElementById(`module-${index}`);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'block';
|
||||
}
|
||||
|
||||
function selectTask(levelId, taskId) {
|
||||
const level = COURSE_DATA.levels.find(l => l.id === levelId);
|
||||
const flat = getAllTasks();
|
||||
const index = flat.findIndex(t => t.id === taskId);
|
||||
const task = level.challenges.find(t => t.id === taskId);
|
||||
currentTask = { ...task, levelTitle: level.title, levelId: level.id, globalIndex: index + 1, totalCount: flat.length };
|
||||
renderTask(currentTask);
|
||||
function openLesson(moduleId, lessonId) {
|
||||
const module = COURSE.modules.find(m => m.id === moduleId);
|
||||
const lesson = module.lessons.find(l => l.id === lessonId);
|
||||
currentLesson = { ...lesson, moduleTitle: module.title, moduleSummary: module.summary };
|
||||
renderCourseNav();
|
||||
renderLesson(currentLesson);
|
||||
}
|
||||
|
||||
function renderTask(task) {
|
||||
function renderLesson(lesson) {
|
||||
document.getElementById('heroPanel').style.display = 'none';
|
||||
document.getElementById('taskShell').classList.add('show');
|
||||
document.getElementById('taskTitle').textContent = task.title;
|
||||
document.getElementById('taskLevel').textContent = task.levelTitle;
|
||||
document.getElementById('taskIndex').textContent = `${task.globalIndex} / ${task.totalCount}`;
|
||||
document.getElementById('taskModeBadge').textContent = `当前:${currentMode === 'learn' ? '学习模式' : '实战模式'}`;
|
||||
document.getElementById('hintBox').textContent = task.hint || '先思考命令目标,再尝试最短的正确命令。';
|
||||
document.getElementById('milestoneBox').innerHTML = `当前目标:<strong>${task.title}</strong><br/>完成后将解锁后续任务。`;
|
||||
currentCwd = '/';
|
||||
updateCwd(task.cwd || currentCwd);
|
||||
const desc = task.description || task.desc || '完成本题要求。';
|
||||
document.getElementById('taskDescription').innerHTML = `
|
||||
<div style="font-size:15px; color:var(--text-2);">${desc}</div>
|
||||
${currentMode === 'learn' ? `<div style="margin-top:14px; padding:12px 14px; border-radius:14px; background:rgba(22,119,255,.08); color:var(--text-2);"><strong>学习提示:</strong> ${task.hint || '试着从命令用途出发。'}</div>` : ''}
|
||||
`;
|
||||
document.getElementById('cmdInput').value = '';
|
||||
document.getElementById('cmdOutput').textContent = '准备好了就开始输入命令。';
|
||||
hideFeedback();
|
||||
updateKnowledgePanel(task);
|
||||
document.getElementById('lessonShell').classList.add('show');
|
||||
document.getElementById('moduleBadge').textContent = lesson.moduleTitle;
|
||||
document.getElementById('commandBadge').textContent = lesson.command || '综合命令';
|
||||
document.getElementById('lessonTitle').textContent = lesson.title;
|
||||
document.getElementById('lessonSummary').textContent = lesson.moduleSummary || '';
|
||||
document.getElementById('lessonGoal').textContent = lesson.goal || '';
|
||||
document.getElementById('lessonWhy').textContent = lesson.why_it_matters || '';
|
||||
renderList('conceptList', lesson.concepts || []);
|
||||
renderList('pitfallList', lesson.pitfalls || []);
|
||||
renderList('scenarioList', lesson.scenarios || []);
|
||||
document.getElementById('exampleList').innerHTML = (lesson.examples || []).map(ex => `<div class="code-block">${escapeHtml(ex)}</div>`).join('');
|
||||
document.getElementById('exerciseList').innerHTML = (lesson.exercises || []).map(ex => renderExercise(ex)).join('');
|
||||
document.getElementById('asideHint').textContent = (lesson.pitfalls || [])[0] || '这一课没有额外提示。';
|
||||
document.getElementById('qaBox').innerHTML = buildQaBox(lesson.exercises || []);
|
||||
}
|
||||
|
||||
function updateKnowledgePanel(task) {
|
||||
const text = task.description || task.desc || '';
|
||||
const match = text.match(/<code>([\w-]+)<\/code>/) || text.match(/使用\s+([\w-]+)/);
|
||||
const cmd = match ? match[1] : null;
|
||||
const box = document.getElementById('knowledgeBox');
|
||||
if (cmd && COMMAND_KNOWLEDGE[cmd]) {
|
||||
const info = COMMAND_KNOWLEDGE[cmd];
|
||||
box.innerHTML = `
|
||||
<div style="font-weight:800; color:var(--primary-dark); margin-bottom:8px;">${cmd}</div>
|
||||
<div style="color:var(--text-2); line-height:1.8; margin-bottom:12px;">${info.desc}</div>
|
||||
<div class="code-list">${info.examples.map(item => `<span class="code-chip">${item}</span>`).join('')}</div>
|
||||
<div style="margin-top:12px; font-size:13px; color:var(--text-3);">💡 ${info.tip}</div>
|
||||
function renderList(targetId, list) {
|
||||
const el = document.getElementById(targetId);
|
||||
el.innerHTML = list.map(item => `<li>${escapeHtml(item)}</li>`).join('');
|
||||
}
|
||||
|
||||
function renderExercise(ex) {
|
||||
const typeLabel = ex.type === 'operation' ? '操作练习' : ex.type === 'understanding' ? '理解题' : '场景题';
|
||||
const body = ex.type === 'operation'
|
||||
? `
|
||||
<div style="font-weight:700; margin-bottom:8px;">${ex.title || '练习'}</div>
|
||||
<div class="muted" style="line-height:1.8; margin-bottom:10px;">${ex.hint || '请完成对应命令。'}</div>
|
||||
<div class="terminal-box">
|
||||
<div class="terminal-header">终端练习区</div>
|
||||
<div class="terminal-input-row">
|
||||
<span class="prompt">$</span>
|
||||
<input class="cmd-input" id="input-${ex.id}" placeholder="输入命令,例如 ${ex.solution ? ex.solution[0] : 'pwd'}" onkeypress="if(event.key==='Enter') runExercise('${ex.id}')" />
|
||||
<button class="btn btn-primary" onclick="runExercise('${ex.id}')">执行</button>
|
||||
</div>
|
||||
<div class="terminal-output" id="output-${ex.id}">等待输入命令...</div>
|
||||
</div>
|
||||
<div class="feedback" id="feedback-${ex.id}"></div>
|
||||
`
|
||||
: `
|
||||
<div style="font-weight:700; margin-bottom:8px;">${ex.question || '理解题'}</div>
|
||||
<div class="qa-box">参考答案方向:${escapeHtml(ex.answer || '请结合本课内容自行总结')}</div>
|
||||
`;
|
||||
} else {
|
||||
box.innerHTML = '<div class="empty-state">这题更偏向综合操作。先理解目标,再尝试一步步拆分命令。</div>';
|
||||
}
|
||||
return `
|
||||
<div class="exercise-card">
|
||||
<div class="exercise-type">${typeLabel}</div>
|
||||
${body}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function executeCommand() {
|
||||
const input = document.getElementById('cmdInput');
|
||||
function buildQaBox(exercises) {
|
||||
const textItems = exercises.filter(ex => ex.type !== 'operation');
|
||||
if (!textItems.length) return '本课暂无理解型问题。';
|
||||
return textItems.map(ex => `<div class="qa-box"><strong>${escapeHtml(ex.question || '问题')}</strong><br/>${escapeHtml(ex.answer || '')}</div>`).join('');
|
||||
}
|
||||
|
||||
async function runExercise(exerciseId) {
|
||||
const input = document.getElementById(`input-${exerciseId}`);
|
||||
const output = document.getElementById(`output-${exerciseId}`);
|
||||
const feedback = document.getElementById(`feedback-${exerciseId}`);
|
||||
const cmd = input.value.trim();
|
||||
if (!cmd || !currentTask) return;
|
||||
const outputEl = document.getElementById('cmdOutput');
|
||||
outputEl.textContent = `$ ${cmd}\n\n执行中...`;
|
||||
hideFeedback();
|
||||
if (!cmd) return;
|
||||
output.textContent = `$ ${cmd}\n\n执行中...`;
|
||||
feedback.className = 'feedback';
|
||||
feedback.textContent = '';
|
||||
try {
|
||||
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
|
||||
const data = await res.json();
|
||||
currentCwd = data.cwd || currentCwd;
|
||||
updateCwd(currentCwd);
|
||||
outputEl.textContent = `$ ${cmd}\n\n${data.output || data.message || '(无输出)'}`;
|
||||
await checkAnswer(cmd, data.output || '');
|
||||
const runRes = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
|
||||
const runData = await runRes.json();
|
||||
output.textContent = `$ ${cmd}\n\n${runData.output || runData.message || '(无输出)'}`;
|
||||
const checkRes = await fetch(`/api/check?exercise_id=${encodeURIComponent(exerciseId)}&last_cmd=${encodeURIComponent(cmd)}&output=${encodeURIComponent(runData.output || '')}`);
|
||||
const checkData = await checkRes.json();
|
||||
feedback.className = `feedback show ${checkData.success ? 'success' : 'warn'}`;
|
||||
feedback.textContent = checkData.message + (checkData.next_suggestion ? `\n${checkData.next_suggestion}` : '');
|
||||
} catch (e) {
|
||||
outputEl.textContent = `❌ 执行失败:${e.message}`;
|
||||
showFeedback('error', '命令执行失败,请稍后重试。');
|
||||
output.textContent = `❌ 执行失败:${e.message}`;
|
||||
feedback.className = 'feedback show warn';
|
||||
feedback.textContent = '执行失败,请稍后重试。';
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAnswer(cmd, output) {
|
||||
const res = await fetch(`/api/check?task_id=${encodeURIComponent(currentTask.id)}&last_cmd=${encodeURIComponent(cmd)}&output=${encodeURIComponent(output)}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
if (!completedTasks.includes(currentTask.id)) {
|
||||
completedTasks.push(currentTask.id);
|
||||
localStorage.setItem('linux_completed', JSON.stringify(completedTasks));
|
||||
updateStats();
|
||||
}
|
||||
showFeedback('success', data.message + (data.next_suggestion ? `\n${data.next_suggestion}` : ''));
|
||||
renderCourseNav();
|
||||
} else {
|
||||
showFeedback('warn', data.message || '还没通过,再试一次。');
|
||||
}
|
||||
}
|
||||
|
||||
function showHint() {
|
||||
if (!currentTask) return;
|
||||
showFeedback('warn', '提示:' + (currentTask.hint || '从命令的最基础用法开始试。'));
|
||||
}
|
||||
|
||||
function showAnswer() {
|
||||
if (!currentTask || !currentTask.solution || !currentTask.solution.length) return;
|
||||
if (!confirm('查看答案会降低训练效果,确定继续?')) return;
|
||||
document.getElementById('cmdInput').value = currentTask.solution[0];
|
||||
showFeedback('warn', '标准答案已填入输入框,你可以先自己理解后再执行。');
|
||||
}
|
||||
|
||||
function nextTask() {
|
||||
const tasks = getAllTasks();
|
||||
if (!currentTask) return startLearning();
|
||||
const idx = tasks.findIndex(t => t.id === currentTask.id);
|
||||
if (idx >= 0 && idx + 1 < tasks.length) {
|
||||
const nxt = tasks[idx + 1];
|
||||
selectTask(nxt.levelId, nxt.id);
|
||||
} else {
|
||||
showFeedback('success', '🎉 你已经完成全部任务,接下来可以复盘和挑战更高难度命令组合。');
|
||||
}
|
||||
}
|
||||
|
||||
function startLearning() {
|
||||
const first = getAllTasks()[0];
|
||||
if (first) selectTask(first.levelId, first.id);
|
||||
}
|
||||
|
||||
function jumpToFirstUnfinished() {
|
||||
const tasks = getAllTasks();
|
||||
const first = tasks.find(t => !completedTasks.includes(t.id)) || tasks[0];
|
||||
if (first) selectTask(first.levelId, first.id);
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const total = getAllTasks().length || 0;
|
||||
const completed = completedTasks.length;
|
||||
const percent = total ? Math.round(completed / total * 100) : 0;
|
||||
document.getElementById('doneCount').textContent = completed;
|
||||
document.getElementById('progressFill').style.width = percent + '%';
|
||||
document.getElementById('progressText').textContent = `${completed} / ${total} 完成 (${percent}%)`;
|
||||
document.getElementById('taskCount').textContent = total;
|
||||
}
|
||||
|
||||
function updateCwd(cwd) {
|
||||
document.getElementById('cwdChip').textContent = `当前目录:${cwd}`;
|
||||
}
|
||||
|
||||
async function resetSandbox() {
|
||||
await fetch('/api/reset', { method: 'POST' });
|
||||
currentCwd = '/';
|
||||
updateCwd(currentCwd);
|
||||
document.getElementById('cmdOutput').textContent = '♻️ 沙盒环境已重置。你可以重新挑战当前任务。';
|
||||
showFeedback('warn', '沙盒环境已重置,目录、文件和权限状态都恢复到初始值。');
|
||||
alert('沙盒环境已重置。');
|
||||
}
|
||||
|
||||
function showFeedback(type, text) {
|
||||
const box = document.getElementById('feedbackBox');
|
||||
box.className = `feedback show ${type}`;
|
||||
box.textContent = text;
|
||||
function setTheme(theme, save = true) {
|
||||
currentTheme = theme;
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
if (save) localStorage.setItem('linux_course_theme', theme);
|
||||
}
|
||||
|
||||
function hideFeedback() {
|
||||
const box = document.getElementById('feedbackBox');
|
||||
box.className = 'feedback';
|
||||
box.textContent = '';
|
||||
function openFirstLesson() {
|
||||
const firstModule = COURSE.modules[0];
|
||||
const firstLesson = firstModule.lessons[0];
|
||||
openLesson(firstModule.id, firstLesson.id);
|
||||
}
|
||||
|
||||
window.setMode = setMode;
|
||||
window.toggleTheme = toggleTheme;
|
||||
window.selectTask = selectTask;
|
||||
window.toggleLevel = toggleLevel;
|
||||
window.executeCommand = executeCommand;
|
||||
window.showHint = showHint;
|
||||
window.showAnswer = showAnswer;
|
||||
window.nextTask = nextTask;
|
||||
window.startLearning = startLearning;
|
||||
function openFirstPracticeLesson() {
|
||||
for (const module of COURSE.modules) {
|
||||
for (const lesson of module.lessons) {
|
||||
if ((lesson.exercises || []).some(ex => ex.type === 'operation')) {
|
||||
openLesson(module.id, lesson.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
window.toggleModule = toggleModule;
|
||||
window.openLesson = openLesson;
|
||||
window.runExercise = runExercise;
|
||||
window.resetSandbox = resetSandbox;
|
||||
window.jumpToFirstUnfinished = jumpToFirstUnfinished;
|
||||
window.setTheme = setTheme;
|
||||
window.openFirstLesson = openFirstLesson;
|
||||
window.openFirstPracticeLesson = openFirstPracticeLesson;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
155
server.py
155
server.py
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user