feat: redesign linux course around learning-first structure

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

319
COURSE.md
View File

@@ -1,147 +1,274 @@
# 📚 Linux 命令学习课程体系(入门 → 高手 # Linux 学习平台课程设计(重构版
> 学习路径:**理论 → 演示 → 沙盒练习 → 测试 → 徽章认证** ## 设计原则
这次课程重构,目标不再是“闯关刷题”,而是建立一套**适合系统学习 Linux 的知识结构**。
核心原则:
- **先理解,再操作**
- **先场景,再命令**
- **先最小可用,再扩展参数**
- **练习服务于理解,不反客为主**
- **每个模块都能迁移到真实工作场景**
--- ---
## 🌱 Level 1入门新手村 ## 新课程结构
### 🎯 目标:熟悉终端、查看文件、当前目录、简单操作 课程分为 6 大模块,不再按简单等级推进,而按学习逻辑推进。
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 | ### 模块 1建立 Linux 基本认知
|------|------|---------|-------------|---------| 目标:先搞清楚“我在哪、我看到什么、我怎么移动”。
| 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 的内容 |
**徽章**` 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` | ⚔️⚔️⚔️⚔️ | - 能开始形成 Linux 使用习惯
| 📏 磁盘分析 | 写出 `/projects` 中最大的 3 个文件 | `du`, `sort`, `tail` | ⚔️⚔️ |
| 📝 生成报告 | 把所有 `.py` 文件的行数统计保存到 `stats.txt` | `wc`, `find`, `>` | ⚔️⚔️⚔️⚔️⚔️ |
| 🔐 权限检查 | 找出所有`.sh` 脚本并检查权限是否为 `755` | `find`, `stat`, `grep` | ⚔️⚔️⚔️⚔️⚔️ |
--- ---
## 🏆 完整通关徽章体系 ## 每个课时的新结构
```bash 每一课统一用下面 6 段结构:
beginner_1 ← 目录旅行者 pwd/ls/cd
beginner_2 ← 文件管理员 touch/cp/mv/mkdir ### 1. 学什么
intermediate_1 ← 情报专家 grep/find/du/sort/wc 一句话说清这个命令/主题在解决什么问题。
intermediate_2 ← 文字工匠 cat/echo/pipe/redirection
advanced_1 ← 系统法师 find-size/chmod/ps/history/man ### 2. 为什么重要
expert_1 ← 实战大师 (综合项目通关) 它在真实 Linux 使用、开发、运维里有什么价值。
legend ← Linux 大师 (所有课程 + 心得分享)
``` ### 3. 核心知识点
包括:
- 命令作用
- 常见参数
- 常见组合
- 输出怎么看
- 容易误解的点
### 4. 最小示例
给 2~4 个最有代表性的示例。
### 5. 常见场景
把命令放进真实场景里:
- 查配置
- 查日志
- 找文件
- 看资源
- 改权限
### 6. 练习题
练习题不再喧宾夺主,而是用于确认理解。
--- ---
## 📝 测验模式设计 ## 练习设计原则
每个课时结束后,自动弹出 练习题分成 3 类
```bash ### A. 理解题
🎯 当前任务_____________ 检验有没有理解命令用途。
💡 提示_________________ 例:
(stdin) > [输入命令] - 查看当前目录应该用什么命令?
- 为什么 `ls -a` 会比 `ls` 多看到东西?
✅ 回答正确!获得经验值 +100 ### B. 操作题
❌ 还未达标!提示:试试 `xxx` 检验能否正确写出命令。
``` 例:
- 进入 `/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 平台从“交互导向”改造成“知识导向”的系统学习平台。**
后续所有改动,以这个文档为准。

View File

@@ -1,191 +1,284 @@
{ {
"meta": { "meta": {
"version": "3.0", "version": "4.0",
"title": "Linux 运维工程师完整教程", "title": "Linux 系统学习课程(重构版)",
"author": "PMClaw", "author": "OpenClaw Dev",
"updated": "2026-03-06", "updated": "2026-03-10",
"description": "覆盖菜鸟教程 Linux 全部内容,从入门到精通", "description": "强调知识理解、场景迁移与轻量练习的 Linux 学习课程",
"total_levels": 12, "module_count": 6,
"total_challenges": 80 "total_lessons": 18,
"total_exercises": 54
}, },
"levels": [ "modules": [
{ {
"id": "level_1_basic", "id": "module_1_foundation",
"title": "🌱 Level 1: Linux 基础入门", "title": "模块 1建立 Linux 基本认知",
"description": "Linux 简介、目录结构、基本操作", "summary": "先理解终端、目录、路径和最基础命令,建立 Linux 使用的空间感。",
"challenges": [ "lessons": [
{"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": "m1_l1_pwd",
{"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": "📋 详细信息已获取!"}, "title": "认识当前目录pwd",
{"id": "l1_4_ls_a", "title": "显示隐藏文件", "description": "使用 ls -a 显示包括隐藏文件在内的所有文件", "hint": "ls -a", "success_test": "'.' in output and '..' in output", "solution": ["ls -a"], "success_msg": "👻 隐藏文件已显示!"}, "goal": "理解当前工作目录的意义,知道自己在文件系统中的位置。",
{"id": "l1_5_cd", "title": "切换目录", "description": "使用 cd 命令进入 /tmp 目录", "hint": "cd /tmp", "success_test": "cwd == '/tmp'", "solution": ["cd /tmp"], "success_msg": "🚶 目录切换成功!"}, "why_it_matters": "很多 Linux 操作依赖路径。如果不知道自己当前在哪,后续命令容易出错。",
{"id": "l1_6_cd_back", "title": "返回上级目录", "description": "使用 cd .. 返回上级目录", "hint": "cd ..", "success_test": "cwd != '/tmp'", "solution": ["cd .."], "success_msg": "⬆️ 返回成功!"}, "concepts": [
{"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": "🧹 屏幕已清空!"} "绝对路径与相对路径",
"为什么要先定位再操作"
],
"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": "level_2_file", "id": "m1_l2_ls",
"title": "📁 Level 2: 文件与目录管理", "title": "看见目录内容ls",
"description": "创建、删除、复制、移动文件和目录", "goal": "理解 ls 的作用,并掌握查看隐藏文件和详细信息的基本方式。",
"challenges": [ "why_it_matters": "Linux 下很多探索行为都从 ls 开始,它决定你如何观察目录结构。",
{"id": "l2_1_mkdir", "title": "创建目录", "description": "使用 mkdir 创建 /tmp/testdir 目录", "hint": "mkdir /tmp/testdir", "success_test": "exists('/tmp/testdir')", "solution": ["mkdir /tmp/testdir"], "success_msg": "📂 目录创建成功!"}, "concepts": [
{"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": "🚚 文件移动成功!"}, "command": "ls",
{"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": "✏️ 重命名成功!"}, "examples": [
{"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": "🗑️ 文件删除成功!"}, "ls",
{"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": "🗑️ 目录删除成功!"}, "ls -la",
{"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": "💥 强制删除成功!"} "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": "level_3_view", "id": "m1_l3_cd_cat_echo",
"title": "👁️ Level 3: 文件内容查看", "title": "移动、读文件、输出文本",
"description": "cat、more、less、head、tail 等查看命令", "goal": "掌握 cd、cat、echo 这些最基础但最常用的命令",
"challenges": [ "why_it_matters": "这三个命令几乎贯穿 Linux 入门阶段的所有练习。",
{"id": "l3_1_cat", "title": "查看文件内容", "description": "使用 cat 查看 /etc/passwd 文件内容", "hint": "cat /etc/passwd", "success_test": "'root' in output", "solution": ["cat /etc/passwd"], "success_msg": "📖 文件内容已显示!"}, "concepts": [
{"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": "👀 实时追踪模式!"}, "command": "cd / cat / echo",
{"id": "l3_7_more", "title": "分页查看", "description": "使用 more 分页查看 /etc/passwd", "hint": "more /etc/passwd", "success_test": "cmd.startswith('more')", "solution": ["more /etc/passwd"], "success_msg": "📄 分页查看模式!"}, "examples": [
{"id": "l3_8_less", "title": "可滚动查看", "description": "使用 less 查看 /etc/passwd支持上下滚动", "hint": "less /etc/passwd", "success_test": "cmd.startswith('less')", "solution": ["less /etc/passwd"], "success_msg": "📜 可滚动查看模式!"} "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_4_permission", "id": "module_2_filesystem",
"title": "🔐 Level 4: 文件权限管理", "title": "模块 2文件与目录操作",
"description": "chmod、chown、chgrp 权限控制", "summary": "围绕创建、复制、移动、删除和查看文件属性建立文件系统操作能力。",
"challenges": [ "lessons": [
{"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": "m2_l1_create",
{"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": "🔐 权限修改成功!"}, "title": "创建文件与目录mkdir / touch",
{"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": "⚡ 执行权限已添加!"}, "goal": "理解目录和文件的创建逻辑,学会递归创建多级目录。",
{"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": "🚫 读权限已移除!"}, "why_it_matters": "很多项目初始化、环境准备都从创建目录结构开始。",
{"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": "👤 所有者已修改!"}, "concepts": ["目录创建", "多级目录", "空文件创建"],
{"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": "👥 所属组已修改!"}, "command": "mkdir / touch",
{"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": "🔄 递归修改成功!"} "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": "level_5_search", "id": "m2_l2_move_copy_delete",
"title": "🔍 Level 5: 搜索与过滤", "title": "复制、移动与删除cp / mv / rm",
"description": "find、grep、which、whereis 搜索命令", "goal": "理解文件操作中的备份、迁移、重命名和清理。",
"challenges": [ "why_it_matters": "日常 Linux 使用里最常见的就是处理文件的生命周期。",
{"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": "🔍 文件已找到!"}, "concepts": ["复制与备份", "移动与重命名", "删除风险"],
{"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": "📁 目录已找到!"}, "command": "cp / mv / rm",
{"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": "📏 大文件已找到!"}, "examples": ["cp /etc/hosts /tmp/hosts.bak", "mv old.txt new.txt", "rm -r /tmp/testdir"],
{"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": "🗑️ 操作执行成功!"}, "pitfalls": ["把删除当成移动", "对目录使用 cp 却忘记 -r", "rm -rf 风险极高"],
{"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": "🎯 搜索成功!"}, "scenarios": ["做配置备份", "整理日志文件", "清理无用目录"],
{"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": "🔤 忽略大小写搜索成功!"}, "exercises": [
{"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": "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": "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": "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": "l5_9_which", "title": "查找命令位置", "description": "查找 ls 命令的位置", "hint": "which ls", "success_test": "'/bin' in output or '/usr' in output", "solution": ["which ls"], "success_msg": "📍 命令位置已找到!"}, {"id": "m2_l2_e3", "type": "understanding", "question": "为什么 rm -rf 是高风险命令?", "answer": "因为它会递归并强制删除文件和目录,执行错误会造成不可恢复的数据丢失"}
{"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", "id": "m2_l3_stat_permissions",
"title": "👥 Level 6: 用户和组管理", "title": "认识文件属性stat 与权限基础",
"description": "useradd、usermod、passwd、group 管理", "goal": "开始理解文件属性和权限表达。",
"challenges": [ "why_it_matters": "文件权限是 Linux 系统安全和协作的重要基础。",
{"id": "l6_1_whoami", "title": "查看当前用户", "description": "使用 whoami 查看当前登录用户名", "hint": "whoami", "success_test": "len(output.strip()) > 0", "solution": ["whoami"], "success_msg": "👤 当前用户已显示!"}, "concepts": ["文件元信息", "权限三元组", "目录与文件权限差异"],
{"id": "l6_2_id", "title": "查看用户ID", "description": "使用 id 查看当前用户的 UID、GID 和所属组", "hint": "id", "success_test": "'uid' in output.lower()", "solution": ["id"], "success_msg": "🆔 用户信息已显示!"}, "command": "stat / chmod",
{"id": "l6_3_w", "title": "查看登录用户", "description": "使用 w 查看当前登录系统的用户", "hint": "w", "success_test": "'USER' in output or 'TTY' in output", "solution": ["w"], "success_msg": "👥 登录用户已显示!"}, "examples": ["stat /etc/hosts", "chmod 755 script.sh", "chmod +x run.sh"],
{"id": "l6_4_last", "title": "查看登录历史", "description": "使用 last 查看最近的登录记录", "hint": "last", "success_test": "len(output) > 10", "solution": ["last"], "success_msg": "📜 登录历史已显示!"}, "pitfalls": ["不了解 755 / 644 的含义", "给不该执行的文件随意加执行权限"],
{"id": "l6_5_cat_passwd", "title": "查看用户列表", "description": "查看 /etc/passwd 文件了解系统用户", "hint": "cat /etc/passwd", "success_test": "':' in output", "solution": ["cat /etc/passwd"], "success_msg": "📋 用户列表已显示!"}, "scenarios": ["检查脚本是否可执行", "排查权限导致的运行失败"],
{"id": "l6_6_cat_group", "title": "查看组列表", "description": "查看 /etc/group 文件了解系统用户组", "hint": "cat /etc/group", "success_test": "':' in output", "solution": ["cat /etc/group"], "success_msg": "📋 组列表已显示!"}, "exercises": [
{"id": "l6_7_passwd", "title": "修改密码", "description": "使用 passwd 修改当前用户密码(输入当前密码和新密码)", "hint": "passwd", "success_test": "cmd == 'passwd'", "solution": ["passwd"], "success_msg": "🔑 密码修改命令已执行!"}, {"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": "l6_8_su", "title": "切换用户", "description": "使用 su - 切换到 root 用户", "hint": "su -", "success_test": "cmd.startswith('su')", "solution": ["su -", "su"], "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_7_disk", "id": "module_3_searching",
"title": "💾 Level 7: 磁盘管理", "title": "模块 3阅读与筛选信息",
"description": "df、du、fdisk、mount 磁盘操作", "summary": "把 Linux 当成信息检索工具来学,围绕日志、配置和统计建立阅读能力。",
"challenges": [ "lessons": [
{"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": "m3_l1_read_logs",
{"id": "l7_3_du", "title": "查看目录大小", "description": "使用 du 查看 /tmp 目录的大小", "hint": "du /tmp", "success_test": "len(output) > 0", "solution": ["du /tmp"], "success_msg": "📏 目录大小已显示!"}, "title": "看文件头尾head / tail",
{"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": "📊 总大小已显示!"}, "goal": "学会快速读取大文件的局部内容。",
{"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": "🏆 最大目录已排序!"}, "why_it_matters": "日志通常很大,不可能总是整份去看。",
{"id": "l7_6_mount", "title": "查看挂载点", "description": "使用 mount 查看已挂载的文件系统", "hint": "mount", "success_test": "'on' in output or 'type' in output", "solution": ["mount"], "success_msg": "🔗 挂载点已显示!"}, "concepts": ["查看前几行", "查看后几行", "实时追踪"],
{"id": "l7_7_fdisk", "title": "查看分区表", "description": "使用 fdisk -l 查看磁盘分区表(需要 root 权限)", "hint": "fdisk -l", "success_test": "cmd.startswith('fdisk')", "solution": ["fdisk -l"], "success_msg": "💿 分区表已显示!"} "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": "level_8_process", "id": "m3_l2_grep",
"title": "⚙️ Level 8: 进程管理", "title": "关键词搜索grep",
"description": "ps、top、kill、nohup 进程控制", "goal": "理解 grep 作为日志排障和文本定位核心工具的价值。",
"challenges": [ "why_it_matters": "没有 grep查日志和配置会慢很多。",
{"id": "l8_1_ps", "title": "查看进程", "description": "使用 ps 查看当前用户的进程", "hint": "ps", "success_test": "'PID' in output", "solution": ["ps"], "success_msg": "📋 进程列表已显示!"}, "concepts": ["大小写忽略", "显示行号", "反向匹配", "递归搜索"],
{"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": "📊 所有进程已显示!"}, "command": "grep",
{"id": "l8_3_ps_grep", "title": "查找特定进程", "description": "查找包含 ssh 的进程", "hint": "ps aux | grep ssh", "success_test": "'|' in cmd", "solution": ["ps aux | grep ssh"], "success_msg": "🎯 进程已找到!"}, "examples": ["grep error /var/log/syslog", "grep -in root /etc/passwd", "grep -v nologin /etc/passwd"],
{"id": "l8_4_top", "title": "实时进程监控", "description": "使用 top 实时查看进程(按 q 退出)", "hint": "top", "success_test": "cmd == 'top'", "solution": ["top"], "success_msg": "📈 实时监控已启动!"}, "pitfalls": ["不会结合 -n 定位行号", "不知道 -i 和 -v 的常见用途"],
{"id": "l8_5_kill", "title": "终止进程", "description": "使用 kill 命令(查看 PID 后终止)", "hint": "kill PID", "success_test": "cmd.startswith('kill')", "solution": ["kill 1234"], "success_msg": "💀 终止命令已执行!"}, "scenarios": ["查错误日志", "找配置项", "过滤无效行"],
{"id": "l8_6_kill9", "title": "强制终止", "description": "使用 kill -9 强制终止进程", "hint": "kill -9 PID", "success_test": "'-9' in cmd", "solution": ["kill -9 1234"], "success_msg": "💥 强制终止已执行!"}, "exercises": [
{"id": "l8_7_pkill", "title": "按名称终止", "description": "使用 pkill 按进程名终止", "hint": "pkill process_name", "success_test": "cmd.startswith('pkill')", "solution": ["pkill python"], "success_msg": "🎯 按名称终止已执行!"}, {"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": "l8_8_nohup", "title": "后台运行", "description": "使用 nohup 让命令在后台运行", "hint": "nohup command &", "success_test": "cmd.startswith('nohup')", "solution": ["nohup sleep 10 &"], "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": "level_9_network", "id": "m3_l3_find_wc_sort",
"title": "🌐 Level 9: 网络管理", "title": "查找与统计find / wc / sort",
"description": "ifconfig、ping、netstat、curl 网络命令", "goal": "建立查找文件和做基础统计的能力。",
"challenges": [ "why_it_matters": "Linux 的很多效率来自组合式查找与统计。",
{"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": "🎴 网卡信息已显示!"}, "concepts": ["按名称查找", "行数字数统计", "排序输出"],
{"id": "l9_2_ip_addr", "title": "现代方式查看", "description": "使用 ip addr 查看网络接口", "hint": "ip addr", "success_test": "cmd.startswith('ip addr')", "solution": ["ip addr"], "success_msg": "🎴 网络接口已显示!"}, "command": "find / wc / sort",
{"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 测试完成!"}, "examples": ["find /etc -name '*.conf'", "wc -l /var/log/syslog", "ls | sort"],
{"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": "🔗 网络连接已显示!"}, "pitfalls": ["把 find 和 grep 混淆", "不会根据任务选文件查找还是内容查找"],
{"id": "l9_5_ss", "title": "现代方式查看端口", "description": "使用 ss -tlnp 查看监听端口", "hint": "ss -tlnp", "success_test": "cmd.startswith('ss')", "solution": ["ss -tlnp"], "success_msg": "🔗 端口信息已显示!"}, "scenarios": ["找配置文件", "统计日志行数", "整理输出结果"],
{"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 请求已发送!"}, "exercises": [
{"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": "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": "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": "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因为这是文件定位问题不是文件内容搜索问题"}
}, ]
{ }
"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": "⏰ 定时任务已显示!"}
] ]
} }
] ]

View File

@@ -3,42 +3,39 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Linux 命令学习平台 - 从入门到精通</title> <title>Linux 系统学习平台</title>
<style> <style>
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
:root { :root {
--primary: #1677ff; --primary: #1677ff;
--primary-dark: #0f5ed7; --primary-dark: #0f5ed7;
--primary-soft: #eef5ff; --primary-soft: #eef5ff;
--secondary: #36a3ff; --accent: #36a3ff;
--accent: #22c55e; --success: #22c55e;
--warning: #f59e0b; --warning: #f59e0b;
--danger: #ef4444; --danger: #ef4444;
--bg: #f4f8ff; --bg: #f5f9ff;
--bg-2: #edf4ff; --bg-2: #edf4ff;
--card: rgba(255,255,255,0.95); --card: rgba(255,255,255,0.95);
--text: #0f172a; --text: #0f172a;
--text-2: #334155; --text-2: #334155;
--text-3: #64748b; --text-3: #64748b;
--border: #dbe7f5; --border: #dbe7f5;
--terminal: #0b1220;
--terminal-panel: #111b2e;
--terminal-text: #d8e7ff;
--shadow: 0 18px 45px rgba(15, 94, 215, 0.12); --shadow: 0 18px 45px rgba(15, 94, 215, 0.12);
--radius: 20px; --radius: 22px;
--terminal: #0b1220;
--terminal-soft: #101a2f;
} }
[data-theme="dark"] { [data-theme='dark'] {
--bg: #09111f; --bg: #08101d;
--bg-2: #0e1930; --bg-2: #101b31;
--card: rgba(17, 27, 46, 0.95); --card: rgba(16, 26, 47, 0.96);
--text: #e8f0ff; --text: #edf4ff;
--text-2: #cbd8f0; --text-2: #cfdbf4;
--text-3: #8ea2c8; --text-3: #90a5ca;
--border: #1d3358; --border: #1d3358;
--terminal: #050a13; --shadow: 0 18px 55px rgba(2, 8, 23, 0.42);
--terminal-panel: #0b1220; --primary-soft: rgba(22,119,255,.12);
--terminal-text: #d8e7ff;
--shadow: 0 20px 55px rgba(2, 8, 23, 0.45);
} }
body { body {
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif; font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
@@ -47,594 +44,439 @@
min-height: 100vh; min-height: 100vh;
} }
.header { .header {
background: linear-gradient(135deg, #0f5ed7 0%, #1677ff 48%, #36a3ff 100%); background: linear-gradient(135deg, #0f5ed7 0%, #1677ff 45%, #36a3ff 100%);
color: #fff; color: #fff;
padding: 18px 24px; 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 { .header-inner {
max-width: 1440px; max-width: 1480px;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
gap: 20px; gap: 20px;
align-items: center;
flex-wrap: wrap;
} }
.brand h1 { .brand h1 { font-size: 26px; font-weight: 800; }
font-size: 24px; .brand p { margin-top: 6px; opacity: .92; font-size: 13px; }
font-weight: 800; .header-actions { display:flex; gap:10px; flex-wrap:wrap; }
letter-spacing: 0.02em; .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 { .chip-btn:hover, .chip-btn.active { background:#fff; color:var(--primary); }
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); }
.layout { .layout {
max-width: 1440px; max-width: 1480px; margin: 0 auto; padding: 20px;
margin: 0 auto; display: grid; grid-template-columns: 320px minmax(0, 1fr) 340px; gap: 18px;
padding: 20px;
display: grid;
grid-template-columns: 300px minmax(0, 1fr) 360px;
gap: 18px;
min-height: calc(100vh - 92px);
} }
.card { .card {
background: var(--card); background: var(--card); border:1px solid var(--border); border-radius: var(--radius);
border: 1px solid var(--border); box-shadow: var(--shadow); backdrop-filter: blur(10px);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
} }
.sidebar, .learning-panel { padding: 18px; } .sidebar, .content, .aside { padding: 20px; }
.section-title { .title-row { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:14px; }
font-size: 16px; .title-row h2 { font-size: 18px; font-weight: 800; }
font-weight: 800; .muted { color: var(--text-3); }
margin-bottom: 14px; .course-meta {
color: var(--text); display:grid; grid-template-columns:repeat(2, 1fr); gap:10px; margin-bottom:16px;
display: flex;
align-items: center;
gap: 8px;
} }
.stats-grid { .meta-box {
display: grid; padding: 14px; border-radius: 16px; border:1px solid var(--border);
grid-template-columns: repeat(2, 1fr); background: linear-gradient(180deg, var(--primary-soft), rgba(255,255,255,.75));
gap: 10px;
margin-bottom: 16px;
} }
.stat-box { .meta-box .label { font-size:12px; color:var(--text-3); }
background: linear-gradient(180deg, var(--primary-soft) 0%, rgba(255,255,255,0.7) 100%); .meta-box .value { margin-top:6px; font-size:22px; font-weight:800; }
border: 1px solid var(--border); .module-item {
border-radius: 16px; border:1px solid var(--border); border-radius:18px; margin-bottom:14px; overflow:hidden;
padding: 14px; background: rgba(255,255,255,.55);
} }
.stat-box .label { font-size: 12px; color: var(--text-3); } .module-head {
.stat-box .value { font-size: 24px; font-weight: 800; margin-top: 6px; } padding: 14px 16px; cursor:pointer; display:flex; justify-content:space-between; gap:12px; align-items:center;
.progress-wrap { margin-bottom: 16px; } background: linear-gradient(90deg, #eef6ff 0%, #f8fbff 100%);
.progress-bar { color: var(--primary-dark); font-weight: 800;
height: 10px; background: rgba(148,163,184,0.15); border-radius: 999px; overflow: hidden;
} }
.progress-fill { .lesson-list { padding: 12px; }
height: 100%; background: linear-gradient(90deg, var(--primary), var(--secondary)); transition: width .3s ease; .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); } .lesson-item:hover { background: var(--primary-soft); }
.level-header { .lesson-item.active {
display: flex; justify-content: space-between; align-items: center; cursor: pointer; background: linear-gradient(90deg, var(--primary) 0%, var(--accent) 100%); color:#fff;
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;
} }
.task-list { list-style: none; margin-bottom: 12px; } .lesson-item .name { font-weight: 700; }
.task-item { .lesson-item .desc { font-size: 12px; margin-top: 5px; opacity: .85; }
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; }
.hero { .hero {
padding: 26px; border-radius: 24px; padding: 24px; border-radius: 24px; border:1px solid #d8e7fb;
background: linear-gradient(135deg, rgba(22,119,255,.12), rgba(54,163,255,.1)); background: linear-gradient(135deg, rgba(22,119,255,.12), rgba(54,163,255,.08));
border: 1px solid #d8e7fb; margin-bottom: 18px;
} }
.hero h2 { font-size: 30px; margin-bottom: 10px; } .hero h2 { font-size: 32px; margin-bottom: 10px; }
.hero p { color: var(--text-3); line-height: 1.8; } .hero p { line-height: 1.9; color: var(--text-3); }
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; } .hero-actions { display:flex; gap:12px; flex-wrap:wrap; margin-top:16px; }
.btn { .btn {
border: none; border-radius: 14px; padding: 12px 18px; cursor: pointer; font-size: 14px; font-weight: 700; border:none; border-radius:14px; padding: 12px 18px; cursor:pointer; font-weight:800;
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;
} }
.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 { .badge {
display: inline-flex; align-items: center; padding: 6px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; display:inline-flex; align-items:center; padding:6px 10px; border-radius:999px;
background: var(--primary-soft); color: var(--primary-dark); font-size:12px; font-weight:700; background: var(--primary-soft); color: var(--primary-dark);
} }
.task-desc { .lesson-title { font-size: 30px; font-weight: 800; margin-bottom: 10px; }
padding: 18px; border-radius: 18px; background: linear-gradient(180deg, #f8fbff 0%, #f1f7ff 100%); .panel {
border: 1px solid #dce9f8; line-height: 1.8; 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 { .panel h3 { font-size:16px; font-weight:800; margin-bottom:12px; color: var(--text-2); }
background: linear-gradient(180deg, var(--terminal) 0%, var(--terminal-panel) 100%); .panel p, .panel li { line-height: 1.9; color: var(--text-2); }
border-radius: 22px; padding: 16px; color: var(--terminal-text); border: 1px solid rgba(59,130,246,.18); .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; } .exercise-card {
.terminal-title { font-weight: 700; color: #b9d6ff; } border:1px solid var(--border); border-radius:16px; padding:14px; background: rgba(255,255,255,.85);
.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;
} }
.prompt-line { .exercise-type {
display: flex; align-items: center; gap: 10px; padding: 12px 14px; display:inline-flex; margin-bottom:8px; padding:4px 8px; border-radius:999px; font-size:12px; font-weight:700;
border-radius: 14px; background: rgba(255,255,255,.03); border: 1px solid rgba(96,165,250,.15); background: #eef4ff; color: var(--primary-dark);
} }
.prompt { color: #4ade80; font-weight: 700; font-family: Consolas, monospace; } .terminal-box {
.command-input { margin-top: 12px; border-radius: 18px; overflow:hidden; border:1px solid rgba(59,130,246,.18);
flex: 1; background: transparent; border: none; color: #fff; outline: none; font-family: Consolas, monospace; font-size: 15px; background: linear-gradient(180deg, var(--terminal) 0%, var(--terminal-soft) 100%);
} }
.terminal-output { .terminal-header { padding: 12px 14px; color:#b9d6ff; font-weight:700; border-bottom:1px solid rgba(148,163,184,.15); }
margin-top: 12px; min-height: 170px; max-height: 380px; overflow: auto; .terminal-input-row { display:flex; gap:10px; padding: 14px; align-items:center; }
border-radius: 14px; padding: 14px; background: rgba(2, 6, 23, 0.45); .prompt { color:#4ade80; font-family: Consolas, monospace; font-weight:700; }
font-family: Consolas, monospace; white-space: pre-wrap; line-height: 1.6; .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.show { display:block; }
.feedback.success { background: rgba(34,197,94,.1); color: #15803d; border: 1px solid rgba(34,197,94,.2); } .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); color: #b45309; border: 1px solid rgba(245,158,11,.2); } .feedback.warn { background: rgba(245,158,11,.1); border:1px solid rgba(245,158,11,.2); color:#b45309; }
.feedback.error { background: rgba(239,68,68,.1); color: #b91c1c; border: 1px solid rgba(239,68,68,.2); } .aside-card {
.action-row { display: flex; gap: 12px; flex-wrap: wrap; } border:1px solid var(--border); border-radius:18px; padding:16px; margin-bottom:14px;
.learning-panel .hint-card, background: linear-gradient(180deg, rgba(255,255,255,.88), rgba(247,250,255,.92));
.learning-panel .knowledge-card,
.learning-panel .milestone-card {
padding: 16px; border-radius: 16px; margin-bottom: 14px; border: 1px solid var(--border);
} }
.hint-card { background: linear-gradient(180deg, #f8fbff 0%, #f3f8ff 100%); } .aside-card h3 { font-size:15px; font-weight:800; margin-bottom:10px; }
.knowledge-card { background: linear-gradient(180deg, #eef6ff 0%, #f8fbff 100%); } .aside-card li { margin-left: 18px; line-height: 1.8; color: var(--text-2); }
.milestone-card { background: linear-gradient(180deg, rgba(34,197,94,.08), rgba(255,255,255,.9)); } .qa-box {
.small-title { font-size: 13px; font-weight: 800; color: var(--text-2); margin-bottom: 8px; } padding:12px; border-radius:14px; background: var(--primary-soft); margin-top:10px; color:var(--text-2);
.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) { @media (max-width: 1240px) {
.layout { grid-template-columns: 280px minmax(0,1fr); } .layout { grid-template-columns: 320px minmax(0, 1fr); }
.learning-panel { display: none; } .aside { display:none; }
} }
@media (max-width: 820px) { @media (max-width: 860px) {
.layout { grid-template-columns: 1fr; } .layout { grid-template-columns: 1fr; }
.sidebar { order: 2; } .header-inner { flex-direction: column; align-items: flex-start; }
.main-panel { order: 1; }
.header-content { flex-direction: column; align-items: start; }
} }
</style> </style>
</head> </head>
<body> <body>
<header class="header"> <header class="header">
<div class="header-content"> <div class="header-inner">
<div class="brand"> <div class="brand">
<h1>🐧 Linux 运维学习平台</h1> <h1>🐧 Linux 系统学习平台</h1>
<p>内容更系统 · 练习更真实 · 判题更可靠</p> <p>以知识理解为中心,练习为辅,帮助建立真正可迁移的 Linux 能力。</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button class="pill-btn active" id="learnMode" onclick="setMode('learn')">📖 学习模式</button> <button class="chip-btn active" onclick="setTheme('light')">浅色</button>
<button class="pill-btn" id="practiceMode" onclick="setMode('practice')">⚔️ 实战模式</button> <button class="chip-btn" onclick="setTheme('dark')">深色</button>
<button class="pill-btn" onclick="resetSandbox()">♻️ 重置环境</button> <button class="chip-btn" onclick="resetSandbox()">重置沙盒</button>
<button class="pill-btn" onclick="toggleTheme()">🌓 主题</button>
</div> </div>
</div> </div>
</header> </header>
<div class="layout"> <div class="layout">
<aside class="sidebar card"> <aside class="sidebar card">
<div class="section-title">📚 学习地图</div> <div class="title-row">
<div class="stats-grid"> <h2>课程地图</h2>
<div class="stat-box"> <span class="muted" id="courseVersion">v4.0</span>
<div class="label">课程关卡</div>
<div class="value" id="levelCount">12</div>
</div> </div>
<div class="stat-box"> <div class="course-meta">
<div class="label">总任务数</div> <div class="meta-box"><div class="label">模块数</div><div class="value" id="moduleCount">6</div></div>
<div class="value" id="taskCount">80</div> <div class="meta-box"><div class="label">课时数</div><div class="value" id="lessonCount">18</div></div>
</div> <div class="meta-box"><div class="label">练习数</div><div class="value" id="exerciseCount">54</div></div>
<div class="stat-box"> <div class="meta-box"><div class="label">学习方向</div><div class="value" style="font-size:18px">知识优先</div></div>
<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>
<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> </div>
<div id="courseNav"></div> <div id="courseNav"></div>
</aside> </aside>
<main class="main-panel card"> <main class="content card">
<section class="hero" id="heroPanel"> <section class="hero" id="heroPanel">
<h2>从命令入门,到运维思维</h2> <h2>先把 Linux 学明白,再去练</h2>
<p> <p>
这个版本不再只是“输入答案过题”。你现在会得到更真实的沙盒环境、状态可变的文件系统、 这个版本不再以“闯关感”作为核心,而是把 Linux 当成一门真正要学懂的技能来组织:
更合理的后端判题,以及更清晰的学习建议 每个课时会先说明为什么重要、核心知识点是什么、常见误区在哪里,再给最小示例和少量练习
</p> </p>
<div class="hero-actions"> <div class="hero-actions">
<button class="btn btn-primary" onclick="startLearning()">开始第一关</button> <button class="btn btn-primary" onclick="openFirstLesson()">从第一课开始</button>
<button class="btn btn-secondary" onclick="jumpToFirstUnfinished()">继续上次进度</button> <button class="btn btn-soft" onclick="openFirstPracticeLesson()">直接看第一组练习</button>
</div> </div>
</section> </section>
<section class="task-shell" id="taskShell"> <section class="lesson-shell" id="lessonShell">
<div> <div class="badge-row">
<div class="task-meta"> <span class="badge" id="moduleBadge">模块 1</span>
<span class="badge" id="taskLevel">Level 1</span> <span class="badge" id="commandBadge">命令</span>
<span class="badge" id="taskIndex">1 / 80</span>
<span class="badge" id="taskModeBadge">当前:学习模式</span>
</div> </div>
<h2 style="margin-top:12px; font-size:28px;" id="taskTitle">任务标题</h2> <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>
<div class="task-desc" id="taskDescription"></div> <div class="panel">
<h3>为什么重要</h3>
<div class="terminal"> <p id="lessonWhy"></p>
<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> </div>
<div class="feedback" id="feedbackBox"></div> <div class="panel">
<h3>核心知识点</h3>
<ul id="conceptList"></ul>
</div>
<div class="action-row"> <div class="panel">
<button class="btn btn-primary" onclick="executeCommand()">▶ 执行命令</button> <h3>最小示例</h3>
<button class="btn btn-secondary" onclick="showHint()">💡 提示</button> <div class="example-list" id="exampleList"></div>
<button class="btn btn-warning" onclick="showAnswer()">👀 查看答案</button> </div>
<button class="btn btn-secondary" onclick="nextTask()">下一题 →</button>
<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> </div>
</section> </section>
</main> </main>
<aside class="learning-panel card"> <aside class="aside card">
<div class="section-title">🧠 学习辅助</div> <div class="aside-card">
<div class="hint-card"> <h3>学习建议</h3>
<div class="small-title">本题提示</div> <ul>
<div id="hintBox" class="empty-state">选择一题后,这里会显示更聚焦的提示</div> <li>先理解“命令解决什么问题”,再记参数</li>
<li>不要一开始背太多选项,先掌握最常用组合。</li>
<li>日志、配置、目录、进程,是 Linux 学习的四大核心场景。</li>
</ul>
</div> </div>
<div class="knowledge-card"> <div class="aside-card">
<div class="small-title">命令讲解</div> <h3>当前课时提示</h3>
<div id="knowledgeBox" class="empty-state">选择任务后,这里会提供命令用途、示例与注意点</div> <div id="asideHint" class="muted">打开课程后,这里会显示与当前课时相关的补充提醒</div>
</div> </div>
<div class="milestone-card"> <div class="aside-card">
<div class="small-title">当前目标</div> <h3>理解型问题</h3>
<div id="milestoneBox" class="empty-state">开始第一关,逐步建立 Linux 运维基本功</div> <div id="qaBox" class="muted">选择课时后,这里会显示理解题和场景题,帮助你形成真正的命令认识</div>
</div> </div>
</aside> </aside>
</div> </div>
<script> <script>
let COURSE_DATA = null; let COURSE = null;
let currentTask = null; let currentLesson = null;
let currentMode = localStorage.getItem('linux_mode') || 'learn'; let currentTheme = localStorage.getItem('linux_course_theme') || 'light';
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 进程名 是经典组合。'
}
};
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
applyTheme(currentTheme); setTheme(currentTheme, false);
setMode(currentMode, false); await loadCourse();
await loadCourseData();
renderCourseNav(); renderCourseNav();
updateStats();
}); });
async function loadCourseData() { async function loadCourse() {
const res = await fetch('/api/tasks'); const res = await fetch('/api/course');
COURSE_DATA = await res.json(); COURSE = await res.json();
document.getElementById('levelCount').textContent = COURSE_DATA.meta.total_levels || COURSE_DATA.levels.length; document.getElementById('courseVersion').textContent = 'v' + (COURSE.meta.version || '4.0');
document.getElementById('taskCount').textContent = getAllTasks().length; 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() { function getAllLessons() {
return COURSE_DATA ? COURSE_DATA.levels.flatMap(level => level.challenges.map(task => ({...task, levelTitle: level.title, levelId: level.id}))) : []; return COURSE.modules.flatMap(module => module.lessons.map(lesson => ({ ...lesson, moduleId: module.id, moduleTitle: module.title, moduleSummary: module.summary })));
} }
function setMode(mode, save = true) { function getAllExercises() {
currentMode = mode; return getAllLessons().flatMap(lesson => lesson.exercises.map(ex => ({ ...ex, lessonTitle: lesson.title, moduleTitle: lesson.moduleTitle })));
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 renderCourseNav() { function renderCourseNav() {
if (!COURSE_DATA) return;
const nav = document.getElementById('courseNav'); const nav = document.getElementById('courseNav');
nav.innerHTML = COURSE_DATA.levels.map((level, levelIndex) => { nav.innerHTML = COURSE.modules.map((module, index) => {
const items = level.challenges.map(task => { const lessonHtml = module.lessons.map(lesson => {
const active = currentTask && currentTask.id === task.id; const active = currentLesson && currentLesson.id === lesson.id;
const completed = completedTasks.includes(task.id);
return ` return `
<li class="task-item ${active ? 'active' : ''} ${completed ? 'completed' : ''}" onclick="selectTask('${level.id}','${task.id}')"> <div class="lesson-item ${active ? 'active' : ''}" onclick="openLesson('${module.id}', '${lesson.id}')">
<span class="task-status">${completed ? '✅' : (active ? '▶' : '○')}</span> <div class="name">${lesson.title}</div>
<div> <div class="desc">${lesson.goal}</div>
<div style="font-weight:700;">${task.title}</div> </div>`;
<div style="font-size:12px; opacity:.85; margin-top:3px;">${(task.description || task.desc || '').slice(0, 28)}...</div>
</div>
</li>`;
}).join(''); }).join('');
return ` return `
<div style="margin-bottom:12px;"> <div class="module-item">
<div class="level-header" onclick="toggleLevel(${levelIndex})"> <div class="module-head" onclick="toggleModule(${index})">
<span>${level.title}</span> <span>${module.title}</span>
<span>${level.challenges.length} </span> <span>${module.lessons.length} </span>
</div> </div>
<ul class="task-list" id="level-${levelIndex}">${items}</ul> <div class="lesson-list" id="module-${index}">${lessonHtml}</div>
</div>`; </div>`;
}).join(''); }).join('');
} }
function toggleLevel(index) { function toggleModule(index) {
const el = document.getElementById(`level-${index}`); const el = document.getElementById(`module-${index}`);
el.style.display = el.style.display === 'none' ? 'block' : 'none'; el.style.display = el.style.display === 'none' ? 'block' : 'block';
} }
function selectTask(levelId, taskId) { function openLesson(moduleId, lessonId) {
const level = COURSE_DATA.levels.find(l => l.id === levelId); const module = COURSE.modules.find(m => m.id === moduleId);
const flat = getAllTasks(); const lesson = module.lessons.find(l => l.id === lessonId);
const index = flat.findIndex(t => t.id === taskId); currentLesson = { ...lesson, moduleTitle: module.title, moduleSummary: module.summary };
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);
renderCourseNav(); renderCourseNav();
renderLesson(currentLesson);
} }
function renderTask(task) { function renderLesson(lesson) {
document.getElementById('heroPanel').style.display = 'none'; document.getElementById('heroPanel').style.display = 'none';
document.getElementById('taskShell').classList.add('show'); document.getElementById('lessonShell').classList.add('show');
document.getElementById('taskTitle').textContent = task.title; document.getElementById('moduleBadge').textContent = lesson.moduleTitle;
document.getElementById('taskLevel').textContent = task.levelTitle; document.getElementById('commandBadge').textContent = lesson.command || '综合命令';
document.getElementById('taskIndex').textContent = `${task.globalIndex} / ${task.totalCount}`; document.getElementById('lessonTitle').textContent = lesson.title;
document.getElementById('taskModeBadge').textContent = `当前:${currentMode === 'learn' ? '学习模式' : '实战模式'}`; document.getElementById('lessonSummary').textContent = lesson.moduleSummary || '';
document.getElementById('hintBox').textContent = task.hint || '先思考命令目标,再尝试最短的正确命令。'; document.getElementById('lessonGoal').textContent = lesson.goal || '';
document.getElementById('milestoneBox').innerHTML = `当前目标:<strong>${task.title}</strong><br/>完成后将解锁后续任务。`; document.getElementById('lessonWhy').textContent = lesson.why_it_matters || '';
currentCwd = '/'; renderList('conceptList', lesson.concepts || []);
updateCwd(task.cwd || currentCwd); renderList('pitfallList', lesson.pitfalls || []);
const desc = task.description || task.desc || '完成本题要求。'; renderList('scenarioList', lesson.scenarios || []);
document.getElementById('taskDescription').innerHTML = ` document.getElementById('exampleList').innerHTML = (lesson.examples || []).map(ex => `<div class="code-block">${escapeHtml(ex)}</div>`).join('');
<div style="font-size:15px; color:var(--text-2);">${desc}</div> document.getElementById('exerciseList').innerHTML = (lesson.exercises || []).map(ex => renderExercise(ex)).join('');
${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('asideHint').textContent = (lesson.pitfalls || [])[0] || '这一课没有额外提示。';
`; document.getElementById('qaBox').innerHTML = buildQaBox(lesson.exercises || []);
document.getElementById('cmdInput').value = '';
document.getElementById('cmdOutput').textContent = '准备好了就开始输入命令。';
hideFeedback();
updateKnowledgePanel(task);
} }
function updateKnowledgePanel(task) { function renderList(targetId, list) {
const text = task.description || task.desc || ''; const el = document.getElementById(targetId);
const match = text.match(/<code>([\w-]+)<\/code>/) || text.match(/使用\s+([\w-]+)/); el.innerHTML = list.map(item => `<li>${escapeHtml(item)}</li>`).join('');
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>
`;
} else {
box.innerHTML = '<div class="empty-state">这题更偏向综合操作。先理解目标,再尝试一步步拆分命令。</div>';
}
} }
async function executeCommand() { function renderExercise(ex) {
const input = document.getElementById('cmdInput'); 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>
`;
return `
<div class="exercise-card">
<div class="exercise-type">${typeLabel}</div>
${body}
</div>`;
}
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(); const cmd = input.value.trim();
if (!cmd || !currentTask) return; if (!cmd) return;
const outputEl = document.getElementById('cmdOutput'); output.textContent = `$ ${cmd}\n\n执行中...`;
outputEl.textContent = `$ ${cmd}\n\n执行中...`; feedback.className = 'feedback';
hideFeedback(); feedback.textContent = '';
try { try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd)); const runRes = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json(); const runData = await runRes.json();
currentCwd = data.cwd || currentCwd; output.textContent = `$ ${cmd}\n\n${runData.output || runData.message || '(无输出)'}`;
updateCwd(currentCwd); const checkRes = await fetch(`/api/check?exercise_id=${encodeURIComponent(exerciseId)}&last_cmd=${encodeURIComponent(cmd)}&output=${encodeURIComponent(runData.output || '')}`);
outputEl.textContent = `$ ${cmd}\n\n${data.output || data.message || '(无输出)'}`; const checkData = await checkRes.json();
await checkAnswer(cmd, data.output || ''); feedback.className = `feedback show ${checkData.success ? 'success' : 'warn'}`;
feedback.textContent = checkData.message + (checkData.next_suggestion ? `\n${checkData.next_suggestion}` : '');
} catch (e) { } catch (e) {
outputEl.textContent = `❌ 执行失败:${e.message}`; output.textContent = `❌ 执行失败:${e.message}`;
showFeedback('error', '命令执行失败,请稍后重试。'); 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() { async function resetSandbox() {
await fetch('/api/reset', { method: 'POST' }); await fetch('/api/reset', { method: 'POST' });
currentCwd = '/'; alert('沙盒环境已重置。');
updateCwd(currentCwd);
document.getElementById('cmdOutput').textContent = '♻️ 沙盒环境已重置。你可以重新挑战当前任务。';
showFeedback('warn', '沙盒环境已重置,目录、文件和权限状态都恢复到初始值。');
} }
function showFeedback(type, text) { function setTheme(theme, save = true) {
const box = document.getElementById('feedbackBox'); currentTheme = theme;
box.className = `feedback show ${type}`; document.documentElement.setAttribute('data-theme', theme);
box.textContent = text; if (save) localStorage.setItem('linux_course_theme', theme);
} }
function hideFeedback() { function openFirstLesson() {
const box = document.getElementById('feedbackBox'); const firstModule = COURSE.modules[0];
box.className = 'feedback'; const firstLesson = firstModule.lessons[0];
box.textContent = ''; openLesson(firstModule.id, firstLesson.id);
} }
window.setMode = setMode; function openFirstPracticeLesson() {
window.toggleTheme = toggleTheme; for (const module of COURSE.modules) {
window.selectTask = selectTask; for (const lesson of module.lessons) {
window.toggleLevel = toggleLevel; if ((lesson.exercises || []).some(ex => ex.type === 'operation')) {
window.executeCommand = executeCommand; openLesson(module.id, lesson.id);
window.showHint = showHint; return;
window.showAnswer = showAnswer; }
window.nextTask = nextTask; }
window.startLearning = startLearning; }
}
function escapeHtml(str) {
return String(str)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
window.toggleModule = toggleModule;
window.openLesson = openLesson;
window.runExercise = runExercise;
window.resetSandbox = resetSandbox; window.resetSandbox = resetSandbox;
window.jumpToFirstUnfinished = jumpToFirstUnfinished; window.setTheme = setTheme;
window.openFirstLesson = openFirstLesson;
window.openFirstPracticeLesson = openFirstPracticeLesson;
</script> </script>
</body> </body>
</html> </html>

155
server.py
View File

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