feat: Linux练习平台

- Web界面Linux命令练习
- Python后端 + sandbox安全沙箱
- 课程和任务管理
This commit is contained in:
likingcode
2026-03-07 05:43:51 +00:00
commit 5686831d9a
22 changed files with 8816 additions and 0 deletions

147
COURSE.md Normal file
View File

@@ -0,0 +1,147 @@
# 📚 Linux 命令学习课程体系(入门 → 高手)
> 学习路径:**理论 → 演示 → 沙盒练习 → 测试 → 徽章认证**
---
## 🌱 Level 1入门新手村
### 🎯 目标:熟悉终端、查看文件、当前目录、简单操作
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|------|------|---------|-------------|---------|
| 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 ← 目录旅行者`
---
## 🚀 Level 2文件操作手艺人
### 🎯 目标:创建、复制、移动、删除(安全版)、查看
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|------|------|---------|-------------|---------|
| 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搜索高手情报员
### 🎯 目标:快速定位、查找、筛选内容
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|------|------|---------|-------------|---------|
| 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文本编辑文字工作者
### 🎯 目标:预览/编辑文本(只读模式)
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|------|------|---------|-------------|---------|
| 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高级技巧小黑客
### 🎯 目标:权限、查找大文件、进程、自动化
| 课时 | 主题 | 学习内容 | 沙盒练习目标 | 测试场景 |
|------|------|---------|-------------|---------|
| 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实战项目通关玩家
### 🎯 综合应用:解决真实场景
| 场景 | 任务 | 所需命令 | 难度 |
|------|------|----------|------|
| 📁 备份项目 | 将 `/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` | ⚔️⚔️⚔️⚔️⚔️ |
---
## 🏆 完整通关徽章体系
```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 大师 (所有课程 + 心得分享)
```
---
## 📝 测验模式设计
每个课时结束后,自动弹出:
```bash
🎯 当前任务_____________
💡 提示_________________
(stdin) > [输入命令]
✅ 回答正确!获得经验值 +100
❌ 还未达标!提示:试试 `xxx`
```
答对 3 次 → 解锁下一关
---
## 🎯 课程特色
-**零风险沙盒**:所有命令在虚拟环境中执行,不会影响真实系统
-**闯关式学习**:从入门到高手,逐步解锁新技能
-**即时反馈**:答对/错都有针对性提示
-**实战导向**:每个级别都有真实业务场景
-**徽章认证**:每完成一个阶段获得专属徽章
---
需要我根据这个课程体系开始实现吗?包括:
1. `COURSE_TASKS.json`(所有练习题)
2. 沙盒模拟器 `sandbox.py`
3. 熟悉 `server.py` 重构
4. UI 改造(闯关式界面)
5. `README.md` 使用文档
还是先调整下课程内容?😄

192
COURSE_FULL.json Normal file
View File

@@ -0,0 +1,192 @@
{
"meta": {
"version": "3.0",
"title": "Linux 运维工程师完整教程",
"author": "PMClaw",
"updated": "2026-03-06",
"description": "覆盖菜鸟教程 Linux 全部内容,从入门到精通",
"total_levels": 12,
"total_challenges": 80
},
"levels": [
{
"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": "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": "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": "⏰ 定时任务已显示!"}
]
}
]
}

192
COURSE_TASKS.json Normal file
View File

@@ -0,0 +1,192 @@
{
"meta": {
"version": "3.0",
"title": "Linux 运维工程师完整教程",
"author": "PMClaw",
"updated": "2026-03-06",
"description": "覆盖菜鸟教程 Linux 全部内容,从入门到精通",
"total_levels": 12,
"total_challenges": 80
},
"levels": [
{
"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": "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": "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": "⏰ 定时任务已显示!"}
]
}
]
}

View File

@@ -0,0 +1,121 @@
{
"meta": {
"version": "1.0",
"title": "Linux 命令学习课程 - 沙盒练习题",
"author": "Dev Agent",
"created": "2026-03-04"
},
"levels": [
{
"id": "level_1_intro",
"title": "入门篇:熟悉终端",
"description": "前5个命令让你快速上手 Linux 终端",
"challenges": [
{
"id": "pwd_1",
"title": "我在哪?",
"description": "使用 `pwd` 命令查看你当前所在的目录",
"hint": "直接输入 pwd 回车",
"success_test": "if output == '/' then pass",
"solution": ["pwd"],
"fail_message": "❌ 还不对!试试 `pwd` 命令"
},
{
"id": "ls_1",
"title": "看看周围",
"description": "使用 `ls` 命令列出 `/sandbox` 下的所有子目录和文件",
"hint": "输入 ls /sandbox",
"success_test": "if output contains 'users' and 'projects' and 'logs' then pass",
"solution": ["ls /sandbox", "ls /"],
"fail_message": "❌ 还差一点!试试 `ls /sandbox` 或 `ls /`"
},
{
"id": "cd_1",
"title": "进入用户区",
"description": "使用 `cd` 命令进入 /users 目录",
"hint": "先 cd 进去,再用 pwd 确认",
"success_test": "if cwd == '/users' then pass",
"solution": ["cd /users", "cd /"],
"fail_message": "❌ 还没到 /users试试 `cd /users`"
},
{
"id": "cat_1",
"title": "读取文件",
"description": "使用 `cat` 命令读取 /users/alice.txt 的内容",
"hint": "cat /users/alice.txt",
"success_test": "if output contains 'Alice' and 'Linux' then pass",
"solution": ["cat /users/alice.txt"],
"fail_message": "❌ 记得完整路径!试试 `cat /users/alice.txt`"
},
{
"id": "echo_1",
"title": "打招呼",
"description": "使用 `echo` 打印一句话Hello Linux!",
"hint": "echo 黑客996",
"success_test": "if output contains 'Hello Linux' then pass",
"solution": ["echo Hello Linux!", "echo 'Hello Linux!'"],
"fail_message": "❌ 别忘了空格!试试 `echo Hello Linux!`"
}
]
},
{
"id": "level_2_files",
"title": "文件操作",
"description": "创建、复制、移动、查看文件内容(只读沙盒)",
"challenges": [
{
"id": "tail_1",
"title": "看文件末尾",
"description": "使用 `tail -n 3` 查看 /projects/backend/app.py 的最后 3 行",
"hint": "tail -n 3 /projects/backend/app.py",
"success_test": "if output contains 'main' then pass",
"solution": ["tail -n 3 /projects/backend/app.py"],
"fail_message": "❌ 记得 `-n` 参数!试试 `tail -n 3`"
},
{
"id": "head_1",
"title": "看文件开头",
"description": "使用 `head -n 2` 查看 /logs/access.log 的前 2 行",
"hint": "head -n 2 /logs/access.log",
"success_test": "if output contains '2024-01-01' then pass",
"solution": ["head -n 2 /logs/access.log"],
"fail_message": "❌ 别忘了 `-n 2`!试试 `head -n 2`"
},
{
"id": "mkdir_1",
"title": "创建目录",
"description": "使用 `mkdir` 在 /projects 下创建一个叫 mycode 的目录",
"hint": "mkdir /projects/mycode",
"success_test": "if output contains 'mycode' then pass",
"solution": ["mkdir /projects/mycode"],
"fail_message": "❌ 路径别写错!试试 `mkdir /projects/mycode`"
}
]
},
{
"id": "level_3_search",
"title": "搜索高手",
"description": "grep/fun/wc/sort 组合技",
"challenges": [
{
"id": "grep_1",
"title": "查找关键词",
"description": "使用 `grep` 在 /users/alice.txt 中查找包含 'Linux' 的行",
"hint": "grep Linux /users/alice.txt",
"success_test": "if output contains 'Linux' then pass",
"solution": ["grep Linux /users/alice.txt"],
"fail_message": "❌ 格式是 `grep pattern file`!试试 `grep Linux`"
},
{
"id": "wc_1",
"title": "统计行数",
"description": "统计 /projects/backend/app.py 的总行数",
"hint": "wc -l /projects/backend/app.py",
"success_test": "if output contains '4' then pass",
"solution": ["wc -l /projects/backend/app.py"],
"fail_message": "❌ `-l` 是行数参数!试试 `wc -l`"
}
]
}
]
}

314
COURSE_TASKS.json.bak Normal file
View File

@@ -0,0 +1,314 @@
{
"meta": {
"version": "2.0",
"title": "Linux 运维工程师成长路径",
"author": "PMClaw",
"updated": "2026-03-06",
"description": "从新手到高手,系统学习 Linux 运维技能"
},
"levels": [
{
"id": "level_1_intro",
"title": "🌱 Level 1: 入门篇 - 熟悉终端",
"description": "掌握最基本的 5 个命令,快速上手 Linux 终端",
"challenges": [
{
"id": "pwd_1",
"title": "我在哪?",
"desc": "使用 <code>pwd</code> 命令查看你当前所在的目录",
"hint": "直接输入 pwd 回车",
"success_test": "output.strip() == '/'",
"solution": ["pwd"],
"success_msg": "🎉 恭喜你!你学会了第一个 Linux 命令!"
},
{
"id": "ls_1",
"title": "看看周围",
"desc": "使用 <code>ls</code> 命令列出 <code>/sandbox</code> 下的所有子目录和文件",
"hint": "输入 ls /sandbox",
"success_test": "'users' in output and 'projects' in output",
"solution": ["ls /sandbox", "ls /"],
"success_msg": "👀 很好!你能看到周围的文件了"
},
{
"id": "cd_1",
"title": "进入用户区",
"desc": "使用 <code>cd</code> 命令进入 /users 目录",
"hint": "先 cd 进去,再用 pwd 确认",
"success_test": "cwd == '/users'",
"solution": ["cd /users"],
"success_msg": "🚶 移动成功!你学会了在目录间穿梭"
},
{
"id": "cat_1",
"title": "读取文件",
"desc": "使用 <code>cat</code> 命令读取 /users/alice.txt 的内容",
"hint": "cat /users/alice.txt",
"success_test": "'Alice' in output and 'Linux' in output",
"solution": ["cat /users/alice.txt"],
"success_msg": "📖 阅读成功!文件内容已掌握"
},
{
"id": "echo_1",
"title": "输出文字",
"desc": "使用 <code>echo</code> 命令输出 'Hello Linux'",
"hint": "echo Hello Linux",
"success_test": "'Hello Linux' in output",
"solution": ["echo Hello Linux"],
"success_msg": "📢 声音洪亮echo 命令已掌握"
}
]
},
{
"id": "level_2_file",
"title": "📁 Level 2: 文件操作 - 管理文件系统",
"description": "学习创建、删除、复制、移动文件和目录",
"challenges": [
{
"id": "mkdir_1",
"title": "创建目录",
"desc": "在 /tmp 下创建一个名为 'mydir' 的目录",
"hint": "mkdir /tmp/mydir",
"success_test": "exists('/tmp/mydir')",
"solution": ["mkdir /tmp/mydir"],
"success_msg": "📂 目录创建成功!"
},
{
"id": "touch_1",
"title": "创建文件",
"desc": "在 /tmp/mydir 下创建一个名为 'test.txt' 的空文件",
"hint": "touch /tmp/mydir/test.txt",
"success_test": "exists('/tmp/mydir/test.txt')",
"solution": ["touch /tmp/mydir/test.txt"],
"success_msg": "📄 文件创建成功!"
},
{
"id": "cp_1",
"title": "复制文件",
"desc": "将 /users/alice.txt 复制到 /tmp/mydir/",
"hint": "cp /users/alice.txt /tmp/mydir/",
"success_test": "exists('/tmp/mydir/alice.txt')",
"solution": ["cp /users/alice.txt /tmp/mydir/"],
"success_msg": "📋 复制成功!文件已备份"
},
{
"id": "mv_1",
"title": "移动文件",
"desc": "将 /tmp/mydir/alice.txt 重命名为 bob.txt",
"hint": "mv /tmp/mydir/alice.txt /tmp/mydir/bob.txt",
"success_test": "exists('/tmp/mydir/bob.txt') and not exists('/tmp/mydir/alice.txt')",
"solution": ["mv /tmp/mydir/alice.txt /tmp/mydir/bob.txt"],
"success_msg": "🚚 移动成功!文件已改名"
},
{
"id": "rm_1",
"title": "删除文件",
"desc": "删除 /tmp/mydir/test.txt 文件",
"hint": "rm /tmp/mydir/test.txt",
"success_test": "not exists('/tmp/mydir/test.txt')",
"solution": ["rm /tmp/mydir/test.txt"],
"success_msg": "🗑️ 删除成功!文件已清理"
},
{
"id": "chmod_1",
"title": "修改权限",
"desc": "给 /tmp/mydir/bob.txt 添加可执行权限",
"hint": "chmod +x /tmp/mydir/bob.txt",
"success_test": "is_executable('/tmp/mydir/bob.txt')",
"solution": ["chmod +x /tmp/mydir/bob.txt"],
"success_msg": "🔐 权限修改成功!"
}
]
},
{
"id": "level_3_text",
"title": "🔍 Level 3: 文本处理 - 数据分析师",
"description": "掌握 grep、sed、awk 等强大的文本处理工具",
"challenges": [
{
"id": "grep_1",
"title": "搜索文本",
"desc": "在 /var/log/syslog 中搜索包含 'error' 的行",
"hint": "grep error /var/log/syslog",
"success_test": "'error' in output.lower()",
"solution": ["grep error /var/log/syslog", "grep -i error /var/log/syslog"],
"success_msg": "🎯 搜索成功!找到了错误日志"
},
{
"id": "head_1",
"title": "查看开头",
"desc": "查看 /var/log/syslog 的前 5 行",
"hint": "head -n 5 /var/log/syslog",
"success_test": "len(output.split('\\n')) >= 5",
"solution": ["head -5 /var/log/syslog", "head -n 5 /var/log/syslog"],
"success_msg": "👀 看到了开头!"
},
{
"id": "tail_1",
"title": "查看结尾",
"desc": "查看 /var/log/syslog 的最后 3 行",
"hint": "tail -n 3 /var/log/syslog",
"success_test": "len(output.split('\\n')) >= 3",
"solution": ["tail -3 /var/log/syslog", "tail -n 3 /var/log/syslog"],
"success_msg": "🔚 看到了结尾!"
},
{
"id": "wc_1",
"title": "统计行数",
"desc": "统计 /var/log/syslog 有多少行",
"hint": "wc -l /var/log/syslog",
"success_test": "output.strip().isdigit() or ' ' in output",
"solution": ["wc -l /var/log/syslog"],
"success_msg": "🔢 统计完成!"
},
{
"id": "find_1",
"title": "查找文件",
"desc": "在 /sandbox 下查找所有 .txt 文件",
"hint": "find /sandbox -name '*.txt'",
"success_test": "'.txt' in output",
"solution": ["find /sandbox -name '*.txt'"],
"success_msg": "🔍 查找成功!"
}
]
},
{
"id": "level_4_system",
"title": "⚙️ Level 4: 系统管理 - 运维工程师",
"description": "学习进程管理、磁盘监控、系统服务控制",
"challenges": [
{
"id": "ps_1",
"title": "查看进程",
"desc": "查看当前运行的所有进程",
"hint": "ps aux",
"success_test": "'PID' in output and 'COMMAND' in output",
"solution": ["ps aux", "ps -ef"],
"success_msg": "👥 进程列表已显示!"
},
{
"id": "df_1",
"title": "磁盘空间",
"desc": "查看磁盘使用情况",
"hint": "df -h",
"success_test": "'Filesystem' in output and 'Size' in output",
"solution": ["df -h"],
"success_msg": "💾 磁盘信息已获取!"
},
{
"id": "du_1",
"title": "目录大小",
"desc": "查看 /sandbox 目录的总大小",
"hint": "du -sh /sandbox",
"success_test": "'sandbox' in output or 'M' in output or 'K' in output",
"solution": ["du -sh /sandbox"],
"success_msg": "📊 大小统计完成!"
},
{
"id": "free_1",
"title": "内存使用",
"desc": "查看内存使用情况",
"hint": "free -h",
"success_test": "'Mem' in output and 'total' in output.lower()",
"solution": ["free -h"],
"success_msg": "🧠 内存信息已获取!"
},
{
"id": "uptime_1",
"title": "系统运行时间",
"desc": "查看系统已运行多久",
"hint": "uptime",
"success_test": "'load average' in output.lower() or 'day' in output or 'up' in output.lower()",
"solution": ["uptime"],
"success_msg": "⏱️ 运行时间已显示!"
}
]
},
{
"id": "level_5_network",
"title": "🌐 Level 5: 网络运维 - 网络管理员",
"description": "掌握网络诊断、连接监控、服务配置",
"challenges": [
{
"id": "ping_1",
"title": "测试连通性",
"desc": "ping 127.0.0.1 测试本地网络",
"hint": "ping -c 3 127.0.0.1",
"success_test": "'127.0.0.1' in output or 'bytes' in output or 'icmp' in output.lower()",
"solution": ["ping -c 3 127.0.0.1"],
"success_msg": "📡 网络连通!"
},
{
"id": "netstat_1",
"title": "查看端口",
"desc": "查看所有监听的端口",
"hint": "netstat -tlnp",
"success_test": "'LISTEN' in output or 'tcp' in output.lower()",
"solution": ["netstat -tlnp", "ss -tlnp"],
"success_msg": "🔌 端口列表已显示!"
},
{
"id": "curl_1",
"title": "HTTP 请求",
"desc": "使用 curl 访问 http://localhost:8080",
"hint": "curl http://localhost:8080",
"success_test": "'html' in output.lower() or 'http' in output.lower() or len(output) > 10",
"solution": ["curl http://localhost:8080", "curl -I http://localhost:8080"],
"success_msg": "🌐 HTTP 请求成功!"
},
{
"id": "ifconfig_1",
"title": "查看网卡",
"desc": "查看网络接口配置",
"hint": "ifconfig 或 ip addr",
"success_test": "'inet' in output or 'eth0' in output or 'lo' in output",
"solution": ["ifconfig", "ip addr"],
"success_msg": "🎴 网卡信息已获取!"
}
]
},
{
"id": "level_6_advanced",
"title": "🚀 Level 6: 高级运维 - 架构师",
"description": "Shell 脚本、日志分析、性能优化",
"challenges": [
{
"id": "pipe_1",
"title": "管道操作",
"desc": "使用管道 | 将 ps aux 的输出传给 grep 搜索 ssh",
"hint": "ps aux | grep ssh",
"success_test": "'|' in cmd and ('ssh' in output or 'grep' in output)",
"solution": ["ps aux | grep ssh"],
"success_msg": "🔀 管道操作成功!"
},
{
"id": "redirect_1",
"title": "重定向输出",
"desc": "将 echo 'test' 的输出重定向到 /tmp/test.txt",
"hint": "echo 'test' > /tmp/test.txt",
"success_test": "'>' in cmd and exists('/tmp/test.txt')",
"solution": ["echo 'test' > /tmp/test.txt"],
"success_msg": "📤 重定向成功!"
},
{
"id": "tar_1",
"title": "压缩文件",
"desc": "将 /sandbox 目录打包压缩为 /tmp/sandbox.tar.gz",
"hint": "tar -czf /tmp/sandbox.tar.gz /sandbox",
"success_test": "exists('/tmp/sandbox.tar.gz')",
"solution": ["tar -czf /tmp/sandbox.tar.gz /sandbox"],
"success_msg": "📦 压缩成功!"
},
{
"id": "cron_1",
"title": "定时任务",
"desc": "查看当前用户的定时任务",
"hint": "crontab -l",
"success_test": "'crontab' in cmd",
"solution": ["crontab -l"],
"success_msg": "⏰ 定时任务已查看!"
}
]
}
]
}

314
COURSE_TASKS_NEW.json Normal file
View File

@@ -0,0 +1,314 @@
{
"meta": {
"version": "2.0",
"title": "Linux 运维工程师成长路径",
"author": "PMClaw",
"updated": "2026-03-06",
"description": "从新手到高手,系统学习 Linux 运维技能"
},
"levels": [
{
"id": "level_1_intro",
"title": "🌱 Level 1: 入门篇 - 熟悉终端",
"description": "掌握最基本的 5 个命令,快速上手 Linux 终端",
"challenges": [
{
"id": "pwd_1",
"title": "我在哪?",
"desc": "使用 <code>pwd</code> 命令查看你当前所在的目录",
"hint": "直接输入 pwd 回车",
"success_test": "output.strip() == '/'",
"solution": ["pwd"],
"success_msg": "🎉 恭喜你!你学会了第一个 Linux 命令!"
},
{
"id": "ls_1",
"title": "看看周围",
"desc": "使用 <code>ls</code> 命令列出 <code>/sandbox</code> 下的所有子目录和文件",
"hint": "输入 ls /sandbox",
"success_test": "'users' in output and 'projects' in output",
"solution": ["ls /sandbox", "ls /"],
"success_msg": "👀 很好!你能看到周围的文件了"
},
{
"id": "cd_1",
"title": "进入用户区",
"desc": "使用 <code>cd</code> 命令进入 /users 目录",
"hint": "先 cd 进去,再用 pwd 确认",
"success_test": "cwd == '/users'",
"solution": ["cd /users"],
"success_msg": "🚶 移动成功!你学会了在目录间穿梭"
},
{
"id": "cat_1",
"title": "读取文件",
"desc": "使用 <code>cat</code> 命令读取 /users/alice.txt 的内容",
"hint": "cat /users/alice.txt",
"success_test": "'Alice' in output and 'Linux' in output",
"solution": ["cat /users/alice.txt"],
"success_msg": "📖 阅读成功!文件内容已掌握"
},
{
"id": "echo_1",
"title": "输出文字",
"desc": "使用 <code>echo</code> 命令输出 'Hello Linux'",
"hint": "echo Hello Linux",
"success_test": "'Hello Linux' in output",
"solution": ["echo Hello Linux"],
"success_msg": "📢 声音洪亮echo 命令已掌握"
}
]
},
{
"id": "level_2_file",
"title": "📁 Level 2: 文件操作 - 管理文件系统",
"description": "学习创建、删除、复制、移动文件和目录",
"challenges": [
{
"id": "mkdir_1",
"title": "创建目录",
"desc": "在 /tmp 下创建一个名为 'mydir' 的目录",
"hint": "mkdir /tmp/mydir",
"success_test": "exists('/tmp/mydir')",
"solution": ["mkdir /tmp/mydir"],
"success_msg": "📂 目录创建成功!"
},
{
"id": "touch_1",
"title": "创建文件",
"desc": "在 /tmp/mydir 下创建一个名为 'test.txt' 的空文件",
"hint": "touch /tmp/mydir/test.txt",
"success_test": "exists('/tmp/mydir/test.txt')",
"solution": ["touch /tmp/mydir/test.txt"],
"success_msg": "📄 文件创建成功!"
},
{
"id": "cp_1",
"title": "复制文件",
"desc": "将 /users/alice.txt 复制到 /tmp/mydir/",
"hint": "cp /users/alice.txt /tmp/mydir/",
"success_test": "exists('/tmp/mydir/alice.txt')",
"solution": ["cp /users/alice.txt /tmp/mydir/"],
"success_msg": "📋 复制成功!文件已备份"
},
{
"id": "mv_1",
"title": "移动文件",
"desc": "将 /tmp/mydir/alice.txt 重命名为 bob.txt",
"hint": "mv /tmp/mydir/alice.txt /tmp/mydir/bob.txt",
"success_test": "exists('/tmp/mydir/bob.txt') and not exists('/tmp/mydir/alice.txt')",
"solution": ["mv /tmp/mydir/alice.txt /tmp/mydir/bob.txt"],
"success_msg": "🚚 移动成功!文件已改名"
},
{
"id": "rm_1",
"title": "删除文件",
"desc": "删除 /tmp/mydir/test.txt 文件",
"hint": "rm /tmp/mydir/test.txt",
"success_test": "not exists('/tmp/mydir/test.txt')",
"solution": ["rm /tmp/mydir/test.txt"],
"success_msg": "🗑️ 删除成功!文件已清理"
},
{
"id": "chmod_1",
"title": "修改权限",
"desc": "给 /tmp/mydir/bob.txt 添加可执行权限",
"hint": "chmod +x /tmp/mydir/bob.txt",
"success_test": "is_executable('/tmp/mydir/bob.txt')",
"solution": ["chmod +x /tmp/mydir/bob.txt"],
"success_msg": "🔐 权限修改成功!"
}
]
},
{
"id": "level_3_text",
"title": "🔍 Level 3: 文本处理 - 数据分析师",
"description": "掌握 grep、sed、awk 等强大的文本处理工具",
"challenges": [
{
"id": "grep_1",
"title": "搜索文本",
"desc": "在 /var/log/syslog 中搜索包含 'error' 的行",
"hint": "grep error /var/log/syslog",
"success_test": "'error' in output.lower()",
"solution": ["grep error /var/log/syslog", "grep -i error /var/log/syslog"],
"success_msg": "🎯 搜索成功!找到了错误日志"
},
{
"id": "head_1",
"title": "查看开头",
"desc": "查看 /var/log/syslog 的前 5 行",
"hint": "head -n 5 /var/log/syslog",
"success_test": "len(output.split('\\n')) >= 5",
"solution": ["head -5 /var/log/syslog", "head -n 5 /var/log/syslog"],
"success_msg": "👀 看到了开头!"
},
{
"id": "tail_1",
"title": "查看结尾",
"desc": "查看 /var/log/syslog 的最后 3 行",
"hint": "tail -n 3 /var/log/syslog",
"success_test": "len(output.split('\\n')) >= 3",
"solution": ["tail -3 /var/log/syslog", "tail -n 3 /var/log/syslog"],
"success_msg": "🔚 看到了结尾!"
},
{
"id": "wc_1",
"title": "统计行数",
"desc": "统计 /var/log/syslog 有多少行",
"hint": "wc -l /var/log/syslog",
"success_test": "output.strip().isdigit() or ' ' in output",
"solution": ["wc -l /var/log/syslog"],
"success_msg": "🔢 统计完成!"
},
{
"id": "find_1",
"title": "查找文件",
"desc": "在 /sandbox 下查找所有 .txt 文件",
"hint": "find /sandbox -name '*.txt'",
"success_test": "'.txt' in output",
"solution": ["find /sandbox -name '*.txt'"],
"success_msg": "🔍 查找成功!"
}
]
},
{
"id": "level_4_system",
"title": "⚙️ Level 4: 系统管理 - 运维工程师",
"description": "学习进程管理、磁盘监控、系统服务控制",
"challenges": [
{
"id": "ps_1",
"title": "查看进程",
"desc": "查看当前运行的所有进程",
"hint": "ps aux",
"success_test": "'PID' in output and 'COMMAND' in output",
"solution": ["ps aux", "ps -ef"],
"success_msg": "👥 进程列表已显示!"
},
{
"id": "df_1",
"title": "磁盘空间",
"desc": "查看磁盘使用情况",
"hint": "df -h",
"success_test": "'Filesystem' in output and 'Size' in output",
"solution": ["df -h"],
"success_msg": "💾 磁盘信息已获取!"
},
{
"id": "du_1",
"title": "目录大小",
"desc": "查看 /sandbox 目录的总大小",
"hint": "du -sh /sandbox",
"success_test": "'sandbox' in output or 'M' in output or 'K' in output",
"solution": ["du -sh /sandbox"],
"success_msg": "📊 大小统计完成!"
},
{
"id": "free_1",
"title": "内存使用",
"desc": "查看内存使用情况",
"hint": "free -h",
"success_test": "'Mem' in output and 'total' in output.lower()",
"solution": ["free -h"],
"success_msg": "🧠 内存信息已获取!"
},
{
"id": "uptime_1",
"title": "系统运行时间",
"desc": "查看系统已运行多久",
"hint": "uptime",
"success_test": "'load average' in output.lower() or 'day' in output or 'up' in output.lower()",
"solution": ["uptime"],
"success_msg": "⏱️ 运行时间已显示!"
}
]
},
{
"id": "level_5_network",
"title": "🌐 Level 5: 网络运维 - 网络管理员",
"description": "掌握网络诊断、连接监控、服务配置",
"challenges": [
{
"id": "ping_1",
"title": "测试连通性",
"desc": "ping 127.0.0.1 测试本地网络",
"hint": "ping -c 3 127.0.0.1",
"success_test": "'127.0.0.1' in output or 'bytes' in output or 'icmp' in output.lower()",
"solution": ["ping -c 3 127.0.0.1"],
"success_msg": "📡 网络连通!"
},
{
"id": "netstat_1",
"title": "查看端口",
"desc": "查看所有监听的端口",
"hint": "netstat -tlnp",
"success_test": "'LISTEN' in output or 'tcp' in output.lower()",
"solution": ["netstat -tlnp", "ss -tlnp"],
"success_msg": "🔌 端口列表已显示!"
},
{
"id": "curl_1",
"title": "HTTP 请求",
"desc": "使用 curl 访问 http://localhost:8080",
"hint": "curl http://localhost:8080",
"success_test": "'html' in output.lower() or 'http' in output.lower() or len(output) > 10",
"solution": ["curl http://localhost:8080", "curl -I http://localhost:8080"],
"success_msg": "🌐 HTTP 请求成功!"
},
{
"id": "ifconfig_1",
"title": "查看网卡",
"desc": "查看网络接口配置",
"hint": "ifconfig 或 ip addr",
"success_test": "'inet' in output or 'eth0' in output or 'lo' in output",
"solution": ["ifconfig", "ip addr"],
"success_msg": "🎴 网卡信息已获取!"
}
]
},
{
"id": "level_6_advanced",
"title": "🚀 Level 6: 高级运维 - 架构师",
"description": "Shell 脚本、日志分析、性能优化",
"challenges": [
{
"id": "pipe_1",
"title": "管道操作",
"desc": "使用管道 | 将 ps aux 的输出传给 grep 搜索 ssh",
"hint": "ps aux | grep ssh",
"success_test": "'|' in cmd and ('ssh' in output or 'grep' in output)",
"solution": ["ps aux | grep ssh"],
"success_msg": "🔀 管道操作成功!"
},
{
"id": "redirect_1",
"title": "重定向输出",
"desc": "将 echo 'test' 的输出重定向到 /tmp/test.txt",
"hint": "echo 'test' > /tmp/test.txt",
"success_test": "'>' in cmd and exists('/tmp/test.txt')",
"solution": ["echo 'test' > /tmp/test.txt"],
"success_msg": "📤 重定向成功!"
},
{
"id": "tar_1",
"title": "压缩文件",
"desc": "将 /sandbox 目录打包压缩为 /tmp/sandbox.tar.gz",
"hint": "tar -czf /tmp/sandbox.tar.gz /sandbox",
"success_test": "exists('/tmp/sandbox.tar.gz')",
"solution": ["tar -czf /tmp/sandbox.tar.gz /sandbox"],
"success_msg": "📦 压缩成功!"
},
{
"id": "cron_1",
"title": "定时任务",
"desc": "查看当前用户的定时任务",
"hint": "crontab -l",
"success_test": "'crontab' in cmd",
"solution": ["crontab -l"],
"success_msg": "⏰ 定时任务已查看!"
}
]
}
]
}

314
COURSE_TASKS_v2.json Normal file
View File

@@ -0,0 +1,314 @@
{
"meta": {
"version": "2.0",
"title": "Linux 运维工程师成长路径",
"author": "PMClaw",
"updated": "2026-03-06",
"description": "从新手到高手,系统学习 Linux 运维技能"
},
"levels": [
{
"id": "level_1_intro",
"title": "🌱 Level 1: 入门篇 - 熟悉终端",
"description": "掌握最基本的 5 个命令,快速上手 Linux 终端",
"challenges": [
{
"id": "pwd_1",
"title": "我在哪?",
"description": "使用 <code>pwd</code> 命令查看你当前所在的目录",
"hint": "直接输入 pwd 回车",
"success_test": "output.strip() == '/'",
"solution": ["pwd"],
"success_msg": "🎉 恭喜你!你学会了第一个 Linux 命令!"
},
{
"id": "ls_1",
"title": "看看周围",
"description": "使用 <code>ls</code> 命令列出 <code>/sandbox</code> 下的所有子目录和文件",
"hint": "输入 ls /sandbox",
"success_test": "'users' in output and 'projects' in output",
"solution": ["ls /sandbox", "ls /"],
"success_msg": "👀 很好!你能看到周围的文件了"
},
{
"id": "cd_1",
"title": "进入用户区",
"description": "使用 <code>cd</code> 命令进入 /users 目录",
"hint": "先 cd 进去,再用 pwd 确认",
"success_test": "cwd == '/users'",
"solution": ["cd /users"],
"success_msg": "🚶 移动成功!你学会了在目录间穿梭"
},
{
"id": "cat_1",
"title": "读取文件",
"description": "使用 <code>cat</code> 命令读取 /users/alice.txt 的内容",
"hint": "cat /users/alice.txt",
"success_test": "'Alice' in output and 'Linux' in output",
"solution": ["cat /users/alice.txt"],
"success_msg": "📖 阅读成功!文件内容已掌握"
},
{
"id": "echo_1",
"title": "输出文字",
"description": "使用 <code>echo</code> 命令输出 'Hello Linux'",
"hint": "echo Hello Linux",
"success_test": "'Hello Linux' in output",
"solution": ["echo Hello Linux"],
"success_msg": "📢 声音洪亮echo 命令已掌握"
}
]
},
{
"id": "level_2_file",
"title": "📁 Level 2: 文件操作 - 管理文件系统",
"description": "学习创建、删除、复制、移动文件和目录",
"challenges": [
{
"id": "mkdir_1",
"title": "创建目录",
"description": "在 /tmp 下创建一个名为 'mydir' 的目录",
"hint": "mkdir /tmp/mydir",
"success_test": "exists('/tmp/mydir')",
"solution": ["mkdir /tmp/mydir"],
"success_msg": "📂 目录创建成功!"
},
{
"id": "touch_1",
"title": "创建文件",
"description": "在 /tmp/mydir 下创建一个名为 'test.txt' 的空文件",
"hint": "touch /tmp/mydir/test.txt",
"success_test": "exists('/tmp/mydir/test.txt')",
"solution": ["touch /tmp/mydir/test.txt"],
"success_msg": "📄 文件创建成功!"
},
{
"id": "cp_1",
"title": "复制文件",
"description": "将 /users/alice.txt 复制到 /tmp/mydir/",
"hint": "cp /users/alice.txt /tmp/mydir/",
"success_test": "exists('/tmp/mydir/alice.txt')",
"solution": ["cp /users/alice.txt /tmp/mydir/"],
"success_msg": "📋 复制成功!文件已备份"
},
{
"id": "mv_1",
"title": "移动文件",
"description": "将 /tmp/mydir/alice.txt 重命名为 bob.txt",
"hint": "mv /tmp/mydir/alice.txt /tmp/mydir/bob.txt",
"success_test": "exists('/tmp/mydir/bob.txt') and not exists('/tmp/mydir/alice.txt')",
"solution": ["mv /tmp/mydir/alice.txt /tmp/mydir/bob.txt"],
"success_msg": "🚚 移动成功!文件已改名"
},
{
"id": "rm_1",
"title": "删除文件",
"description": "删除 /tmp/mydir/test.txt 文件",
"hint": "rm /tmp/mydir/test.txt",
"success_test": "not exists('/tmp/mydir/test.txt')",
"solution": ["rm /tmp/mydir/test.txt"],
"success_msg": "🗑️ 删除成功!文件已清理"
},
{
"id": "chmod_1",
"title": "修改权限",
"description": "给 /tmp/mydir/bob.txt 添加可执行权限",
"hint": "chmod +x /tmp/mydir/bob.txt",
"success_test": "is_executable('/tmp/mydir/bob.txt')",
"solution": ["chmod +x /tmp/mydir/bob.txt"],
"success_msg": "🔐 权限修改成功!"
}
]
},
{
"id": "level_3_text",
"title": "🔍 Level 3: 文本处理 - 数据分析师",
"description": "掌握 grep、sed、awk 等强大的文本处理工具",
"challenges": [
{
"id": "grep_1",
"title": "搜索文本",
"description": "在 /var/log/syslog 中搜索包含 'error' 的行",
"hint": "grep error /var/log/syslog",
"success_test": "'error' in output.lower()",
"solution": ["grep error /var/log/syslog", "grep -i error /var/log/syslog"],
"success_msg": "🎯 搜索成功!找到了错误日志"
},
{
"id": "head_1",
"title": "查看开头",
"description": "查看 /var/log/syslog 的前 5 行",
"hint": "head -n 5 /var/log/syslog",
"success_test": "len(output.split('\\n')) >= 5",
"solution": ["head -5 /var/log/syslog", "head -n 5 /var/log/syslog"],
"success_msg": "👀 看到了开头!"
},
{
"id": "tail_1",
"title": "查看结尾",
"description": "查看 /var/log/syslog 的最后 3 行",
"hint": "tail -n 3 /var/log/syslog",
"success_test": "len(output.split('\\n')) >= 3",
"solution": ["tail -3 /var/log/syslog", "tail -n 3 /var/log/syslog"],
"success_msg": "🔚 看到了结尾!"
},
{
"id": "wc_1",
"title": "统计行数",
"description": "统计 /var/log/syslog 有多少行",
"hint": "wc -l /var/log/syslog",
"success_test": "output.strip().isdigit() or ' ' in output",
"solution": ["wc -l /var/log/syslog"],
"success_msg": "🔢 统计完成!"
},
{
"id": "find_1",
"title": "查找文件",
"description": "在 /sandbox 下查找所有 .txt 文件",
"hint": "find /sandbox -name '*.txt'",
"success_test": "'.txt' in output",
"solution": ["find /sandbox -name '*.txt'"],
"success_msg": "🔍 查找成功!"
}
]
},
{
"id": "level_4_system",
"title": "⚙️ Level 4: 系统管理 - 运维工程师",
"description": "学习进程管理、磁盘监控、系统服务控制",
"challenges": [
{
"id": "ps_1",
"title": "查看进程",
"description": "查看当前运行的所有进程",
"hint": "ps aux",
"success_test": "'PID' in output and 'COMMAND' in output",
"solution": ["ps aux", "ps -ef"],
"success_msg": "👥 进程列表已显示!"
},
{
"id": "df_1",
"title": "磁盘空间",
"description": "查看磁盘使用情况",
"hint": "df -h",
"success_test": "'Filesystem' in output and 'Size' in output",
"solution": ["df -h"],
"success_msg": "💾 磁盘信息已获取!"
},
{
"id": "du_1",
"title": "目录大小",
"description": "查看 /sandbox 目录的总大小",
"hint": "du -sh /sandbox",
"success_test": "'sandbox' in output or 'M' in output or 'K' in output",
"solution": ["du -sh /sandbox"],
"success_msg": "📊 大小统计完成!"
},
{
"id": "free_1",
"title": "内存使用",
"description": "查看内存使用情况",
"hint": "free -h",
"success_test": "'Mem' in output and 'total' in output.lower()",
"solution": ["free -h"],
"success_msg": "🧠 内存信息已获取!"
},
{
"id": "uptime_1",
"title": "系统运行时间",
"description": "查看系统已运行多久",
"hint": "uptime",
"success_test": "'load average' in output.lower() or 'day' in output or 'up' in output.lower()",
"solution": ["uptime"],
"success_msg": "⏱️ 运行时间已显示!"
}
]
},
{
"id": "level_5_network",
"title": "🌐 Level 5: 网络运维 - 网络管理员",
"description": "掌握网络诊断、连接监控、服务配置",
"challenges": [
{
"id": "ping_1",
"title": "测试连通性",
"description": "ping 127.0.0.1 测试本地网络",
"hint": "ping -c 3 127.0.0.1",
"success_test": "'127.0.0.1' in output or 'bytes' in output or 'icmp' in output.lower()",
"solution": ["ping -c 3 127.0.0.1"],
"success_msg": "📡 网络连通!"
},
{
"id": "netstat_1",
"title": "查看端口",
"description": "查看所有监听的端口",
"hint": "netstat -tlnp",
"success_test": "'LISTEN' in output or 'tcp' in output.lower()",
"solution": ["netstat -tlnp", "ss -tlnp"],
"success_msg": "🔌 端口列表已显示!"
},
{
"id": "curl_1",
"title": "HTTP 请求",
"description": "使用 curl 访问 http://localhost:8080",
"hint": "curl http://localhost:8080",
"success_test": "'html' in output.lower() or 'http' in output.lower() or len(output) > 10",
"solution": ["curl http://localhost:8080", "curl -I http://localhost:8080"],
"success_msg": "🌐 HTTP 请求成功!"
},
{
"id": "ifconfig_1",
"title": "查看网卡",
"description": "查看网络接口配置",
"hint": "ifconfig 或 ip addr",
"success_test": "'inet' in output or 'eth0' in output or 'lo' in output",
"solution": ["ifconfig", "ip addr"],
"success_msg": "🎴 网卡信息已获取!"
}
]
},
{
"id": "level_6_advanced",
"title": "🚀 Level 6: 高级运维 - 架构师",
"description": "Shell 脚本、日志分析、性能优化",
"challenges": [
{
"id": "pipe_1",
"title": "管道操作",
"description": "使用管道 | 将 ps aux 的输出传给 grep 搜索 ssh",
"hint": "ps aux | grep ssh",
"success_test": "'|' in cmd and ('ssh' in output or 'grep' in output)",
"solution": ["ps aux | grep ssh"],
"success_msg": "🔀 管道操作成功!"
},
{
"id": "redirect_1",
"title": "重定向输出",
"description": "将 echo 'test' 的输出重定向到 /tmp/test.txt",
"hint": "echo 'test' > /tmp/test.txt",
"success_test": "'>' in cmd and exists('/tmp/test.txt')",
"solution": ["echo 'test' > /tmp/test.txt"],
"success_msg": "📤 重定向成功!"
},
{
"id": "tar_1",
"title": "压缩文件",
"description": "将 /sandbox 目录打包压缩为 /tmp/sandbox.tar.gz",
"hint": "tar -czf /tmp/sandbox.tar.gz /sandbox",
"success_test": "exists('/tmp/sandbox.tar.gz')",
"solution": ["tar -czf /tmp/sandbox.tar.gz /sandbox"],
"success_msg": "📦 压缩成功!"
},
{
"id": "cron_1",
"title": "定时任务",
"description": "查看当前用户的定时任务",
"hint": "crontab -l",
"success_test": "'crontab' in cmd",
"solution": ["crontab -l"],
"success_msg": "⏰ 定时任务已查看!"
}
]
}
]
}

Binary file not shown.

Binary file not shown.

926
index.html Normal file
View File

@@ -0,0 +1,926 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linux 命令学习平台 - 从入门到精通</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #4CAF50;
--primary-dark: #388E3C;
--secondary: #2196F3;
--accent: #FF9800;
--bg: #f5f7fa;
--card-bg: #ffffff;
--text: #333333;
--text-light: #666666;
--border: #e0e0e0;
--success: #4CAF50;
--error: #f44336;
--warning: #ff9800;
}
[data-theme="dark"] {
--bg: #1a1a2e;
--card-bg: #16213e;
--text: #eaeaea;
--text-light: #a0a0a0;
--border: #0f3460;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
/* Header */
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
}
.header-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.mode-toggle {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
}
.mode-toggle:hover {
background: rgba(255,255,255,0.3);
}
.mode-toggle.active {
background: white;
color: var(--primary);
}
/* Main Layout */
.container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 280px 1fr 350px;
gap: 1.5rem;
padding: 1.5rem;
min-height: calc(100vh - 70px);
}
/* Sidebar */
.sidebar {
background: var(--card-bg);
border-radius: 12px;
padding: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
height: fit-content;
}
.sidebar-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--primary);
}
.level-section {
margin-bottom: 1rem;
}
.level-header {
font-weight: 600;
color: var(--primary);
padding: 0.5rem;
background: rgba(76,175,80,0.1);
border-radius: 6px;
margin-bottom: 0.5rem;
cursor: pointer;
}
.task-list {
list-style: none;
}
.task-item {
padding: 0.6rem 0.8rem;
margin: 0.3rem 0;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
font-size: 0.9rem;
}
.task-item:hover {
background: var(--bg);
}
.task-item.active {
background: var(--primary);
color: white;
}
.task-item.completed {
opacity: 0.7;
}
.task-status {
font-size: 1rem;
}
/* Main Content */
.main-content {
background: var(--card-bg);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.welcome-panel {
text-align: center;
padding: 3rem 2rem;
}
.welcome-title {
font-size: 2rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.welcome-desc {
color: var(--text-light);
font-size: 1.1rem;
margin-bottom: 2rem;
}
.start-btn {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
border: none;
padding: 1rem 3rem;
font-size: 1.1rem;
border-radius: 30px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(76,175,80,0.4);
}
/* Task Panel */
.task-panel {
display: none;
}
.task-header {
margin-bottom: 1.5rem;
}
.task-title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.task-meta {
display: flex;
gap: 1rem;
color: var(--text-light);
font-size: 0.9rem;
}
.task-description {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 1.5rem;
border-radius: 10px;
margin-bottom: 1.5rem;
border-left: 4px solid var(--primary);
}
.task-description h3 {
color: var(--primary);
margin-bottom: 0.8rem;
}
.task-description code {
background: rgba(76,175,80,0.1);
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: var(--primary-dark);
font-family: 'Consolas', monospace;
}
/* Command Input Area */
.command-area {
background: #1e1e1e;
border-radius: 10px;
padding: 1rem;
margin-bottom: 1rem;
}
.command-input-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.prompt {
color: #4CAF50;
font-family: 'Consolas', monospace;
font-weight: bold;
}
.command-input {
flex: 1;
background: transparent;
border: none;
color: #fff;
font-family: 'Consolas', monospace;
font-size: 1rem;
outline: none;
}
.command-output {
background: #2d2d2d;
padding: 1rem;
border-radius: 6px;
min-height: 100px;
max-height: 300px;
overflow-y: auto;
font-family: 'Consolas', monospace;
color: #ddd;
white-space: pre-wrap;
margin-top: 0.5rem;
}
.action-buttons {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.btn {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--border);
color: var(--text);
}
.btn-hint {
background: var(--accent);
color: white;
}
/* Learning Panel (Right Sidebar) */
.learning-panel {
background: var(--card-bg);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
height: fit-content;
}
.panel-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.panel-title::before {
content: "📚";
}
.command-detail {
display: none;
}
.command-detail.active {
display: block;
}
.cmd-name {
font-size: 1.3rem;
font-weight: bold;
color: var(--primary);
margin-bottom: 0.5rem;
font-family: 'Consolas', monospace;
}
.cmd-desc {
color: var(--text-light);
margin-bottom: 1rem;
line-height: 1.8;
}
.cmd-section {
margin-bottom: 1.2rem;
}
.cmd-section-title {
font-weight: 600;
color: var(--secondary);
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.param-list {
list-style: none;
}
.param-item {
padding: 0.5rem;
background: var(--bg);
border-radius: 6px;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.param-name {
font-family: 'Consolas', monospace;
color: var(--accent);
font-weight: 600;
}
.example-box {
background: #f8f9fa;
border-left: 3px solid var(--secondary);
padding: 0.8rem;
border-radius: 0 6px 6px 0;
margin: 0.5rem 0;
}
.example-box code {
font-family: 'Consolas', monospace;
color: var(--primary-dark);
}
/* Success Panel */
.success-panel {
display: none;
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border: 1px solid var(--success);
border-radius: 10px;
padding: 1.5rem;
margin-top: 1rem;
}
.success-panel.show {
display: block;
}
.success-title {
color: var(--success);
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.deep-learning-btn {
background: var(--secondary);
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 6px;
cursor: pointer;
margin-top: 1rem;
}
/* Progress Bar */
.progress-section {
margin-bottom: 1.5rem;
}
.progress-bar {
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--secondary));
transition: width 0.3s;
}
.progress-text {
text-align: center;
margin-top: 0.5rem;
color: var(--text-light);
font-size: 0.9rem;
}
/* Responsive */
@media (max-width: 1200px) {
.container {
grid-template-columns: 260px 1fr;
}
.learning-panel {
display: none;
}
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo">🐧 Linux 学习平台</div>
<div class="header-actions">
<button class="mode-toggle active" id="learnMode" onclick="setMode('learn')">📖 学习模式</button>
<button class="mode-toggle" id="practiceMode" onclick="setMode('practice')">✏️ 练习模式</button>
<button class="mode-toggle" onclick="toggleTheme()">🌓</button>
</div>
</div>
</header>
<div class="container">
<!-- Left Sidebar: Course Navigation -->
<aside class="sidebar">
<div class="sidebar-title">📚 课程目录</div>
<div class="progress-section">
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
</div>
<div class="progress-text" id="progressText">0 / 95 完成</div>
</div>
<div id="courseNav"></div>
</aside>
<!-- Main Content: Task Area -->
<main class="main-content">
<!-- Welcome Panel -->
<div class="welcome-panel" id="welcomePanel">
<h1 class="welcome-title">🚀 开启 Linux 学习之旅</h1>
<p class="welcome-desc">
从入门到精通,系统学习 Linux 运维技能<br>
<strong>12 个级别 · 95 道题目 · 循序渐进</strong>
</p>
<button class="start-btn" onclick="startLearning()">开始学习</button>
</div>
<!-- Task Panel -->
<div class="task-panel" id="taskPanel">
<div class="task-header">
<h2 class="task-title" id="taskTitle">任务标题</h2>
<div class="task-meta">
<span id="taskLevel">Level 1</span>
<span>·</span>
<span id="taskNumber">1 / 95</span>
</div>
</div>
<div class="task-description" id="taskDescription">
<!-- 任务描述 -->
</div>
<!-- Command Input -->
<div class="command-area">
<div class="command-input-wrapper">
<span class="prompt">$</span>
<input type="text" class="command-input" id="cmdInput"
placeholder="输入 Linux 命令..."
onkeypress="if(event.key==='Enter')executeCommand()">
</div>
<div class="command-output" id="cmdOutput">输出将显示在这里...</div>
</div>
<div class="action-buttons">
<button class="btn btn-primary" onclick="executeCommand()">▶ 执行命令</button>
<button class="btn btn-hint" onclick="showHint()">💡 提示</button>
<button class="btn btn-secondary" onclick="showAnswer()">👀 查看答案</button>
</div>
<!-- Success Panel -->
<div class="success-panel" id="successPanel">
<div class="success-title">🎉 回答正确!</div>
<p id="successMessage">恭喜你完成了这个任务!</p>
<button class="deep-learning-btn" onclick="showDeepLearning()">
📚 查看深入学习资料
</button>
<button class="btn btn-primary" onclick="nextTask()" style="margin-left: 1rem;">
下一题 →
</button>
</div>
</div>
</main>
<!-- Right Sidebar: Learning Panel -->
<aside class="learning-panel" id="learningPanel">
<div class="panel-title">命令详解</div>
<div id="commandDetail">
<p style="color: var(--text-light); text-align: center; padding: 2rem 0;">
选择一个任务开始学习<br>
每个命令都有详细讲解
</p>
</div>
</aside>
</div>
<script>
// ====================
// 全局状态
// ====================
let COURSE_DATA = null;
let currentMode = 'learn'; // 'learn' 或 'practice'
let currentTask = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_completed') || '[]');
let currentTheme = localStorage.getItem('linux_theme') || 'light';
// ====================
// 命令知识库
// ====================
const COMMAND_KNOWLEDGE = {
'pwd': {
name: 'pwd',
fullName: 'Print Working Directory',
description: '显示当前工作目录的完整路径。这是你在文件系统中的"当前位置"。',
commonParams: [
{ param: '-L', desc: '显示逻辑路径(包括符号链接)' },
{ param: '-P', desc: '显示物理路径(解析所有符号链接)' }
],
examples: [
{ cmd: 'pwd', desc: '显示当前目录' },
{ cmd: 'pwd -P', desc: '显示物理路径' }
],
tips: '当你不确定自己在哪个目录时,随时使用 pwd 确认位置。',
related: ['cd', 'ls']
},
'ls': {
name: 'ls',
fullName: 'List Directory Contents',
description: '列出目录中的文件和子目录。这是最常用的文件查看命令。',
commonParams: [
{ param: '-l', desc: '长格式显示,包含权限、所有者、大小等详细信息' },
{ param: '-a', desc: '显示所有文件,包括以点开头的隐藏文件' },
{ param: '-h', desc: '人类可读格式,文件大小显示为 K、M、G' },
{ param: '-t', desc: '按修改时间排序' },
{ param: '-r', desc: '反向排序' },
{ param: '-S', desc: '按文件大小排序' }
],
examples: [
{ cmd: 'ls', desc: '列出当前目录文件' },
{ cmd: 'ls -la', desc: '详细列出所有文件' },
{ cmd: 'ls -lh', desc: '详细列出,人类可读大小' },
{ cmd: 'ls -lt', desc: '按时间排序列出' }
],
tips: 'ls -la 是查看目录内容最常用的组合命令。',
related: ['pwd', 'cd', 'll']
},
'cd': {
name: 'cd',
fullName: 'Change Directory',
description: '切换当前工作目录。这是文件系统导航的基础命令。',
commonParams: [
{ param: '..', desc: '切换到上级目录' },
{ param: '~', desc: '切换到用户主目录' },
{ param: '-', desc: '切换到刚才所在的目录' }
],
examples: [
{ cmd: 'cd /home', desc: '切换到 /home 目录' },
{ cmd: 'cd ..', desc: '切换到上级目录' },
{ cmd: 'cd ~', desc: '切换到主目录' },
{ cmd: 'cd -', desc: '返回刚才的目录' }
],
tips: 'cd - 可以快速在两个目录之间切换。',
related: ['pwd', 'ls']
},
'cat': {
name: 'cat',
fullName: 'Concatenate',
description: '连接文件并输出内容。常用于查看小文件的内容。',
commonParams: [
{ param: '-n', desc: '显示行号' },
{ param: '-b', desc: '显示行号,但空行不编号' },
{ param: '-s', desc: '压缩多个空行为一个' }
],
examples: [
{ cmd: 'cat file.txt', desc: '显示文件内容' },
{ cmd: 'cat -n file.txt', desc: '显示文件内容并带行号' },
{ cmd: 'cat file1 file2', desc: '连接多个文件' }
],
tips: '对于大文件,建议使用 less 或 more 而不是 cat。',
related: ['less', 'more', 'head', 'tail']
}
};
// ====================
// 初始化
// ====================
document.addEventListener('DOMContentLoaded', async () => {
applyTheme(currentTheme);
await loadCourseData();
renderCourseNav();
updateProgress();
});
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功');
}
} catch (e) {
console.error('❌ 加载课程数据失败:', e);
}
}
// ====================
// 模式切换
// ====================
function setMode(mode) {
currentMode = mode;
document.getElementById('learnMode').classList.toggle('active', mode === 'learn');
document.getElementById('practiceMode').classList.toggle('active', mode === 'practice');
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() {
if (!COURSE_DATA) return;
const nav = document.getElementById('courseNav');
let html = '';
COURSE_DATA.levels.forEach((level, levelIdx) => {
html += `
<div class="level-section">
<div class="level-header" onclick="toggleLevel(${levelIdx})">
${level.title}
</div>
<ul class="task-list" id="level-${levelIdx}">
`;
level.challenges.forEach((task, taskIdx) => {
const isCompleted = completedTasks.includes(task.id);
const isActive = currentTask && currentTask.id === task.id;
html += `
<li class="task-item ${isActive ? 'active' : ''} ${isCompleted ? 'completed' : ''}"
onclick="selectTask('${level.id}', '${task.id}')">
<span class="task-status">${isCompleted ? '✅' : '○'}</span>
<span>${taskIdx + 1}. ${task.title}</span>
</li>
`;
});
html += '</ul></div>';
});
nav.innerHTML = html;
}
function toggleLevel(levelIdx) {
const list = document.getElementById(`level-${levelIdx}`);
list.style.display = list.style.display === 'none' ? 'block' : 'none';
}
function selectTask(levelId, taskId) {
if (!COURSE_DATA) return;
const level = COURSE_DATA.levels.find(l => l.id === levelId);
const task = level.challenges.find(t => t.id === taskId);
currentTask = { ...task, level: level.title, levelNum: COURSE_DATA.levels.indexOf(level) + 1 };
renderTask(currentTask);
}
// ====================
// 任务渲染
// ====================
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('successPanel').classList.remove('show');
// 基本信息
document.getElementById('taskTitle').textContent = task.title;
document.getElementById('taskLevel').textContent = task.level;
document.getElementById('taskNumber').textContent = `${task.levelNum} / 95`;
// 任务描述
let descHtml = `<h3>📝 任务描述</h3><p>${task.description}</p>`;
// 学习模式:显示更多学习资料
if (currentMode === 'learn') {
descHtml += `
<div style="margin-top: 1rem; padding: 1rem; background: rgba(33,150,243,0.1); border-radius: 8px;">
<strong>💡 学习提示:</strong> ${task.hint}
</div>
`;
}
document.getElementById('taskDescription').innerHTML = descHtml;
// 清空输出
document.getElementById('cmdOutput').textContent = '输出将显示在这里...';
document.getElementById('cmdInput').value = '';
// 更新学习面板
updateLearningPanel(task);
// 更新导航高亮
renderCourseNav();
}
function updateLearningPanel(task) {
const panel = document.getElementById('commandDetail');
// 从任务描述中提取命令
const cmdMatch = task.description.match(/<code>(\w+)<\/code>/);
const cmdName = cmdMatch ? cmdMatch[1] : null;
if (cmdName && COMMAND_KNOWLEDGE[cmdName]) {
const cmd = COMMAND_KNOWLEDGE[cmdName];
let html = `
<div class="command-detail active">
<div class="cmd-name">${cmd.name}</div>
<div style="color: var(--text-light); font-size: 0.9rem; margin-bottom: 1rem;">
${cmd.fullName}
</div>
<div class="cmd-desc">${cmd.description}</div>
<div class="cmd-section">
<div class="cmd-section-title">🔧 常用参数</div>
<ul class="param-list">
`;
cmd.commonParams.forEach(param => {
html += `
<li class="param-item">
<span class="param-name">${param.param}</span>
<span style="color: var(--text-light);"> - ${param.desc}</span>
</li>
`;
});
html += `</ul></div>
<div class="cmd-section">
<div class="cmd-section-title">📝 使用示例</div>
`;
cmd.examples.forEach(ex => {
html += `
<div class="example-box">
<code>${ex.cmd}</code>
<div style="color: var(--text-light); margin-top: 0.3rem; font-size: 0.85rem;">
${ex.desc}
</div>
</div>
`;
});
html += `
</div>
<div class="cmd-section">
<div class="cmd-section-title">💡 小贴士</div>
<p style="font-size: 0.9rem; color: var(--text-light);">${cmd.tips}</p>
</div>
</div>
`;
panel.innerHTML = html;
} else {
panel.innerHTML = `
<div class="command-detail active">
<p style="color: var(--text-light);">
本任务涉及多个命令组合使用<br>
完成任务后可查看详细解析
</p>
</div>
`;
}
}
// ====================
// 命令执行
// ====================
async function executeCommand() {
const cmd = document.getElementById('cmdInput').value.trim();
if (!cmd) return;
const outputEl = document.getElementById('cmdOutput');
outputEl.textContent = `执行: ${cmd}\n正在运行...`;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
outputEl.textContent = data.output || data.message || '(无输出)';
// 检查答案
checkAnswer(cmd, data.output || '');
} catch (e) {
outputEl.textContent = `❌ 错误: ${e.message}`;
}
}
function checkAnswer(cmd, output) {
if (!currentTask) return;
// 简化检查:命令是否包含在 solution 中
const isCorrect = currentTask.solution && currentTask.solution.some(s =>
cmd.toLowerCase().includes(s.toLowerCase())
);
if (isCorrect) {
showSuccess();
}
}
function showSuccess() {
if (!completedTasks.includes(currentTask.id)) {
completedTasks.push(currentTask.id);
localStorage.setItem('linux_completed', JSON.stringify(completedTasks));
updateProgress();
}
document.getElementById('successPanel').classList.add('show');
document.getElementById('successMessage').textContent =
currentTask.success_msg || '恭喜你完成了这个任务!';
renderCourseNav();
}
function showDeepLearning() {
// 显示深入学习资料
alert('📚 深入学习资料功能开发中...\n\n这里将显示\n- 命令原理解析\n- 实际应用场景\n- 常见错误分析\n- 相关命令对比');
}
// ====================
// 辅助功能
// ====================
function showHint() {
if (!currentTask) return;
alert('💡 提示:\n\n' + currentTask.hint);
}
function showAnswer() {
if (!currentTask) return;
const answer = currentTask.solution ? currentTask.solution[0] : '暂无答案';
if (confirm('查看答案将标记此题为"已学习",确定要继续吗?')) {
document.getElementById('cmdInput').value = answer;
showSuccess();
}
}
function nextTask() {
if (!COURSE_DATA || !currentTask) return;
// 找到当前任务的下一个
let found = false;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (found) {
selectTask(level.id, task.id);
return;
}
if (task.id === currentTask.id) found = true;
}
}
alert('🎉 恭喜!你已完成所有课程!');
}
function startLearning() {
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
selectTask(COURSE_DATA.levels[0].id, COURSE_DATA.levels[0].challenges[0].id);
}
function updateProgress() {
const total = 95;
const completed = completedTasks.length;
const percent = Math.round((completed / total) * 100);
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressText').textContent = `${completed} / ${total} 完成 (${percent}%)`;
}
// ====================
// 全局函数暴露
// ====================
window.setMode = setMode;
window.toggleTheme = toggleTheme;
window.toggleLevel = toggleLevel;
window.selectTask = selectTask;
window.executeCommand = executeCommand;
window.showHint = showHint;
window.showAnswer = showAnswer;
window.showDeepLearning = showDeepLearning;
window.nextTask = nextTask;
window.startLearning = startLearning;
</script>
</body>
</html>

View File

@@ -0,0 +1,582 @@
<!DOCTYPE html>
<html>
<head>
<title>Linux 命令沙盒练习 - 菜鸟式学习平台</title>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--sidebar-bg: #fff;
--sidebar-border: #ddd;
--main-bg: #fff;
--cmd-bg: #f0f0f0;
--cmd-border: #ccc;
--cmd-text: #333;
--success-color: #4caf50;
--error-color: #f44336;
--highlight-color: #ff9800;
}
[data-theme="dark"] {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--sidebar-bg: #2d2d2d;
--sidebar-border: #444;
--main-bg: #1e1e1e;
--cmd-bg: #2d2d2d;
--cmd-border: #444;
--cmd-text: #fff;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
/* Header */
header {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--sidebar-border);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 20px; color: var(--success-color); }
.header-actions { display: flex; gap: 10px; align-items: center; }
.theme-toggle, .logout-btn {
background: transparent;
border: 1px solid var(--sidebar-border);
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
color: var(--text-color);
}
.theme-toggle:hover, .logout-btn:hover { background: var(--cmd-bg); }
/* Layout */
.container {
display: flex;
height: calc(100vh - 50px);
}
/* Sidebar */
.sidebar {
width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
overflow-y: auto;
padding: 15px;
}
.sidebar h2 {
font-size: 16px;
margin: 10px 0 5px;
color: var(--success-color);
border-bottom: 2px solid var(--highlight-color);
padding-bottom: 5px;
}
.lesson-list {
list-style: none;
padding: 0;
margin: 0;
}
.lesson-item {
padding: 8px 12px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.lesson-item:hover { background: var(--cmd-bg); }
.lesson-item.active {
background: var(--highlight-color);
color: #fff;
}
.lesson-item.locked { opacity: 0.6; cursor: not-allowed; }
.lesson-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: #ccc;
color: #fff;
}
.lesson-item.active .lesson-badge { background: #fff; color: var(--highlight-color); }
.lesson-item.locked .lesson-badge { background: var(--highlight-color); }
/* Main Content */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.task-card {
background: var(--main-bg);
border: 1px solid var(--sidebar-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.task-title {
font-size: 22px;
color: var(--success-color);
margin: 0 0 10px;
}
.task-desc {
background: var(--cmd-bg);
padding: 15px;
border-radius: 6px;
border-left: 4px solid var(--highlight-color);
margin: 10px 0;
line-height: 1.6;
}
.task-hint {
color: var(--highlight-color);
font-weight: bold;
margin: 10px 0;
}
.task-success { display: none; background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0; border:1px solid var(--success-color); }
.task-success.show { display: block; }
.task-success h4 { margin: 0 0 5px; color: var(--success-color); }
/* Command Area */
.cmd-area {
margin-top: 20px;
}
.cmd-bar {
background: var(--cmd-bg);
border: 1px solid var(--cmd-border);
border-radius: 6px;
padding: 10px;
display: flex;
align-items: center;
}
.prompt { margin-right: 10px; color: var(--success-color); font-weight: bold; }
#cmd {
flex: 1;
background: transparent;
border: none;
color: var(--cmd-text);
font-family: 'Courier New', monospace;
font-size: 16px;
padding: 8px;
outline: none;
}
#output {
margin-top: 15px;
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 6px;
padding: 15px;
min-height: 60px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
color: var(--text-color);
}
.btn {
background: var(--success-color);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background 0.2s;
}
.btn:hover { background: #45a049; }
.btn:disabled { background: #ccc; cursor: not-allowed; }
.btn-next { background: var(--highlight-color); }
.btn-next:hover { background: #f57c00; }
/* Footer */
footer {
background: var(--sidebar-bg);
border-top: 1px solid var(--sidebar-border);
padding: 10px 20px;
text-align: right;
font-size: 12px;
color: #888;
}
.progress-bar {
display: flex;
gap: 2px;
margin: 10px 0;
}
.progress-step {
width: 30px;
height: 4px;
background: var(--cmd-bg);
border-radius: 2px;
}
.progress-step.done { background: var(--success-color); }
.progress-step.current { background: var(--highlight-color); }
</style>
</head>
<body>
<header>
<h1>💻 Linux 命令沙盒练习</h1>
<div class="header-actions">
<button class="theme-toggle" onclick="toggleTheme()" title="切换深色/浅色模式">
<span id="themeLabel">☀️</span> <span id="themeLabelText">浅色</span>
</button>
<span id="userStatus">未登录</span>
<button id="logoutBtn" class="logout-btn" onclick="logout()" style="display:none">👋 退出</button>
</div>
</header>
<div class="container">
<aside class="sidebar">
<div id="sidebarContent">
<!-- 课程目录将由 JS 动态生成 -->
<p style="text-align:center; color:#888; margin-top:20px;">加载课程中...</p>
</div>
</aside>
<main class="main-content">
<div id="taskPanel" class="task-card" style="display:none">
<h2 class="task-title" id="taskTitle"></h2>
<div class="progress-bar" id="progressBar"></div>
<p class="task-hint" id="taskHint"></p>
<div class="task-desc" id="taskDesc"></div>
<div class="task-success" id="taskSuccess">
<h4>✅ 回答正确!🎉</h4>
<p id="taskSuccessMsg"></p>
</div>
<div class="cmd-area">
<div class="cmd-bar">
<span class="prompt">$</span>
<input type="text" id="cmd" placeholder="输入 Linux 命令..." autocomplete="off" autofocus>
</div>
<div id="output">等待输入命令...</div>
<button id="submitBtn" class="btn" onclick="runCommand()">执行命令</button>
<button id="nextBtn" class="btn btn-next" onclick="nextLevel()" style="display:none">下一关 →</button>
</div>
</div>
<div id="welcomePanel" class="task-card" style="text-align:center; padding:40px 20px;">
<h2>欢迎来到 Linux 命令沙盒练习平台!</h2>
<p style="font-size:18px; margin:20px 0;">这是一个零风险的 Linux 命令学习环境,你可以安全地练习常用命令。</p>
<p><strong>📌 使用说明:</strong></p>
<ul style="text-align:left; display:inline-block;">
<li>左侧是课程目录,按顺序学习</li>
<li>阅读任务描述,输入对应命令</li>
<li>答对后自动解锁下一关</li>
<li>支持深色/浅色主题切换</li>
</ul>
<button class="btn" style="margin-top:30px;" onclick="startFirstLesson()">开始学习 →</button>
</div>
</main>
</div>
<footer>
<span id="userBadge">🔒 未认证</span> |
<span id="currentLevel">Lv.1</span> |
<a href="https://github.com/" target="_blank">Powered by Linux Sandbox</a>
</footer>
<script>
// ==============================
// 全局状态
// ==============================
let currentToken = localStorage.getItem('linux_sandbox_token') || '';
let currentUser = localStorage.getItem('linux_sandbox_user') || '';
let currentTheme = localStorage.getItem('linux_sandbox_theme') || 'light';
let currentTaskId = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_sandbox_completed') || '[]');
let COURSE_DATA = null; // 将从 /api/tasks 动态加载
// ==============================
// 主题切换
// ==============================
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('linux_sandbox_theme', theme);
document.getElementById('themeLabel').textContent = theme === 'dark' ? '🌙' : '☀️';
document.getElementById('themeLabelText').textContent = theme === 'dark' ? '深色' : '浅色';
}
function toggleTheme() {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
currentTheme = newTheme;
}
applyTheme(currentTheme);
// ==============================
// 登录/登出
// ==============================
async function login() {
// 建议:实际从登录弹窗调用,这里简化直接用 token
if (currentToken && currentUser) {
updateUI();
return;
}
alert('请先登录!当前简化版支持直接使用内置 Token 执行命令。');
// 临时允许
currentToken = 'safe_linux_2026';
currentUser = 'sandbox_user';
localStorage.setItem('linux_sandbox_token', currentToken);
localStorage.setItem('linux_sandbox_user', currentUser);
logoutBtn.style.display = 'inline-block';
updateUI();
}
function logout() {
localStorage.removeItem('linux_sandbox_token');
localStorage.removeItem('linux_sandbox_user');
currentToken = '';
currentUser = '';
logoutBtn.style.display = 'none';
updateUI();
}
function updateUI() {
const userStatus = document.getElementById('userStatus');
const userBadge = document.getElementById('userBadge');
if (currentToken && currentUser) {
userStatus.textContent = `👤 ${currentUser}`;
userBadge.textContent = '✅ 已认证';
document.getElementById('logoutBtn').style.display = 'inline-block';
} else {
userStatus.textContent = '未登录';
userBadge.textContent = '🔒 未认证';
}
}
// ==============================
// 课程渲染
// ==============================
function renderSidebar() {
const sidebar = document.getElementById('sidebarContent');
let html = '<ul class="lesson-list">';
COURSE_DATA.levels.forEach(level => {
html += `<h2>${level.title}</h2>`;
level.challenges.forEach((task, idx) => {
const done = completedTasks.includes(task.id);
const active = task.id === currentTaskId ? 'active' : '';
const locked = !done && idx > 0 && !completedTasks.includes(level.challenges[idx-1].id);
html += `<li class="lesson-item ${active} ${locked ? 'locked' : ''}" onclick="loadTask('${task.id}')">
<span>${idx+1}. ${task.title}</span>
<span class="lesson-badge">${done ? '✅' : '🔒'}</span>
</li>`;
});
});
html += '</ul>';
sidebar.innerHTML = html;
}
// ==============================
// 任务加载
// ==============================
function loadTask(taskId) {
// 检查前序任务是否完成
let prevTaskId = null;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (task.id === taskId) {
if (prevTaskId && !completedTasks.includes(prevTaskId)) {
alert('请先完成前一关!');
return;
}
currentTaskId = taskId;
renderTask(task);
renderSidebar(); // 更新高亮
return;
}
prevTaskId = task.id;
}
}
alert('任务未找到');
}
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('taskTitle').innerHTML = task.title;
document.getElementById('taskDesc').innerHTML = task.desc;
document.getElementById('taskHint').textContent = `💡 ${task.hint}`;
document.getElementById('taskSuccess').classList.remove('show');
document.getElementById('taskSuccessMsg').textContent = task.success_msg || '恭喜你,任务完成!';
document.getElementById('submitBtn').style.display = 'inline-block';
document.getElementById('nextBtn').style.display = 'none';
// 渲染进度条
const progressBar = document.getElementById('progressBar');
let progressHtml = '';
for (let i=0; i<5; i++) {
progressHtml += `<div class="progress-step ${i === 0 ? 'current' : ''} ${i < 0 ? 'done' : ''}"></div>`;
}
progressBar.innerHTML = progressHtml;
// 聚焦输入框
document.getElementById('cmd').value = '';
document.getElementById('cmd').focus();
}
function startFirstLesson() {
if (!currentToken) {
login();
}
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
loadTask(COURSE_DATA.levels[0].challenges[0].id);
}
// ==============================
// 命令执行
// ==============================
async function runCommand() {
const cmd = document.getElementById('cmd').value.trim();
if (!cmd) return;
const output = document.getElementById('output');
output.textContent = `执行: ${cmd}\n正在运行...`;
document.getElementById('submitBtn').disabled = true;
document.getElementById('cmd').disabled = true;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
output.textContent = data.output || data.message || '(无输出)';
// 检查任务是否完成
checkTaskCompletion(cmd, data.output || '');
} catch (e) {
output.textContent = `❌ 错误: ${e.message}`;
} finally {
document.getElementById('submitBtn').disabled = false;
document.getElementById('cmd').disabled = false;
}
}
function checkTaskCompletion(cmd, output) {
const task = findTaskById(currentTaskId);
if (!task) return;
// 检查 solution 匹配
const solutionMatch = task.success_test || (task.solution && task.solution.includes(cmd));
if (solutionMatch || task.solution?.includes(cmd)) {
handleTaskSuccess(task);
} else {
// 简单启发式检查(未实现 full success_test 解析)
console.log('需要完成任务检查逻辑');
}
}
function handleTaskSuccess(task) {
if (!completedTasks.includes(task.id)) {
completedTasks.push(task.id);
localStorage.setItem('linux_sandbox_completed', JSON.stringify(completedTasks));
alert('✅ 回答正确!\n奖励经验值 +100');
}
document.getElementById('submitBtn').style.display = 'none';
const successBox = document.getElementById('taskSuccess');
successBox.querySelector('h4').innerHTML = '🎉 回答正确!';
successBox.querySelector('p').textContent = task.success_msg || '你已经掌握这个命令的基本用法!';
successBox.classList.add('show');
document.getElementById('nextBtn').style.display = 'inline-block';
}
function nextLevel() {
const task = findTaskById(currentTaskId);
if (!task) return;
// 找到下一关
let found = false;
for (const level of COURSE_DATA.levels) {
for (const t of level.challenges) {
if (found) {
loadTask(t.id);
return;
}
if (t.id === task.id) found = true;
}
}
alert('🎉 所有课程已完成!你已经是 Linux 大师了!🏆');
}
function findTaskById(id) {
for (const level of COURSE_DATA.levels) {
const task = level.challenges.find(t => t.id === id);
if (task) return task;
}
return null;
}
// ==============================
// 事件监听
// ==============================
document.getElementById('cmd').addEventListener('keypress', function(e) {
if (e.key === 'Enter') runCommand();
});
// ==============================
// 初始化
// ==============================
function init() {
updateUI();
// 渲染侧边栏(课程数据加载后调用 renderSidebar
if (COURSE_DATA) renderSidebar();
// 检查是否已有学习进度
if (currentToken && !currentTaskId && !completedTasks.length) {
// 从第一关开始(延迟加载)
setTimeout(startFirstLesson, 1000);
} else if (currentTaskId) {
const task = findTaskById(currentTaskId);
if (task) renderTask(task);
}
}
// 等待课程数据加载完成后初始化 UI
let dataLoaded = false;
setInterval(() => {
if (COURSE_DATA && !dataLoaded) {
init();
dataLoaded = true;
}
}, 500);
// ==============================
// 加载课程数据API
// ==============================
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功', COURSE_DATA);
renderSidebar();
} else {
console.error('❌ 课程数据加载失败:', res.statusText);
}
} catch (e) {
console.error('❌ 课程数据获取异常:', e);
}
}
// 页面全局函数(供 onclick 调用)
window.loadTask = loadTask;
window.runCommand = runCommand;
window.nextLevel = nextLevel;
window.startFirstLesson = startFirstLesson;
// 立即加载课程数据
document.addEventListener('DOMContentLoaded', loadCourseData);
</script>
</body>
</html>

582
index.html.backup.test Normal file
View File

@@ -0,0 +1,582 @@
<!DOCTYPE html>
<html>
<head>
<title>Linux 命令沙盒练习 - 菜鸟式学习平台</title>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--sidebar-bg: #fff;
--sidebar-border: #ddd;
--main-bg: #fff;
--cmd-bg: #f0f0f0;
--cmd-border: #ccc;
--cmd-text: #333;
--success-color: #4caf50;
--error-color: #f44336;
--highlight-color: #ff9800;
}
[data-theme="dark"] {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--sidebar-bg: #2d2d2d;
--sidebar-border: #444;
--main-bg: #1e1e1e;
--cmd-bg: #2d2d2d;
--cmd-border: #444;
--cmd-text: #fff;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
/* Header */
header {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--sidebar-border);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 20px; color: var(--success-color); }
.header-actions { display: flex; gap: 10px; align-items: center; }
.theme-toggle, .logout-btn {
background: transparent;
border: 1px solid var(--sidebar-border);
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
color: var(--text-color);
}
.theme-toggle:hover, .logout-btn:hover { background: var(--cmd-bg); }
/* Layout */
.container {
display: flex;
height: calc(100vh - 50px);
}
/* Sidebar */
.sidebar {
width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
overflow-y: auto;
padding: 15px;
}
.sidebar h2 {
font-size: 16px;
margin: 10px 0 5px;
color: var(--success-color);
border-bottom: 2px solid var(--highlight-color);
padding-bottom: 5px;
}
.lesson-list {
list-style: none;
padding: 0;
margin: 0;
}
.lesson-item {
padding: 8px 12px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.lesson-item:hover { background: var(--cmd-bg); }
.lesson-item.active {
background: var(--highlight-color);
color: #fff;
}
.lesson-item.locked { opacity: 0.6; cursor: not-allowed; }
.lesson-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: #ccc;
color: #fff;
}
.lesson-item.active .lesson-badge { background: #fff; color: var(--highlight-color); }
.lesson-item.locked .lesson-badge { background: var(--highlight-color); }
/* Main Content */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.task-card {
background: var(--main-bg);
border: 1px solid var(--sidebar-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.task-title {
font-size: 22px;
color: var(--success-color);
margin: 0 0 10px;
}
.task-desc {
background: var(--cmd-bg);
padding: 15px;
border-radius: 6px;
border-left: 4px solid var(--highlight-color);
margin: 10px 0;
line-height: 1.6;
}
.task-hint {
color: var(--highlight-color);
font-weight: bold;
margin: 10px 0;
}
.task-success { display: none; background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0; border:1px solid var(--success-color); }
.task-success.show { display: block; }
.task-success h4 { margin: 0 0 5px; color: var(--success-color); }
/* Command Area */
.cmd-area {
margin-top: 20px;
}
.cmd-bar {
background: var(--cmd-bg);
border: 1px solid var(--cmd-border);
border-radius: 6px;
padding: 10px;
display: flex;
align-items: center;
}
.prompt { margin-right: 10px; color: var(--success-color); font-weight: bold; }
#cmd {
flex: 1;
background: transparent;
border: none;
color: var(--cmd-text);
font-family: 'Courier New', monospace;
font-size: 16px;
padding: 8px;
outline: none;
}
#output {
margin-top: 15px;
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 6px;
padding: 15px;
min-height: 60px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
color: var(--text-color);
}
.btn {
background: var(--success-color);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background 0.2s;
}
.btn:hover { background: #45a049; }
.btn:disabled { background: #ccc; cursor: not-allowed; }
.btn-next { background: var(--highlight-color); }
.btn-next:hover { background: #f57c00; }
/* Footer */
footer {
background: var(--sidebar-bg);
border-top: 1px solid var(--sidebar-border);
padding: 10px 20px;
text-align: right;
font-size: 12px;
color: #888;
}
.progress-bar {
display: flex;
gap: 2px;
margin: 10px 0;
}
.progress-step {
width: 30px;
height: 4px;
background: var(--cmd-bg);
border-radius: 2px;
}
.progress-step.done { background: var(--success-color); }
.progress-step.current { background: var(--highlight-color); }
</style>
</head>
<body>
<header>
<h1>💻 Linux 命令沙盒练习</h1>
<div class="header-actions">
<button class="theme-toggle" onclick="toggleTheme()" title="切换深色/浅色模式">
<span id="themeLabel">☀️</span> <span id="themeLabelText">浅色</span>
</button>
<span id="userStatus">未登录</span>
<button id="logoutBtn" class="logout-btn" onclick="logout()" style="display:none">👋 退出</button>
</div>
</header>
<div class="container">
<aside class="sidebar">
<div id="sidebarContent">
<!-- 课程目录将由 JS 动态生成 -->
<p style="text-align:center; color:#888; margin-top:20px;">加载课程中...</p>
</div>
</aside>
<main class="main-content">
<div id="taskPanel" class="task-card" style="display:none">
<h2 class="task-title" id="taskTitle"></h2>
<div class="progress-bar" id="progressBar"></div>
<p class="task-hint" id="taskHint"></p>
<div class="task-desc" id="taskDesc"></div>
<div class="task-success" id="taskSuccess">
<h4>✅ 回答正确!🎉</h4>
<p id="taskSuccessMsg"></p>
</div>
<div class="cmd-area">
<div class="cmd-bar">
<span class="prompt">$</span>
<input type="text" id="cmd" placeholder="输入 Linux 命令..." autocomplete="off" autofocus>
</div>
<div id="output">等待输入命令...</div>
<button id="submitBtn" class="btn" onclick="runCommand()">执行命令</button>
<button id="nextBtn" class="btn btn-next" onclick="nextLevel()" style="display:none">下一关 →</button>
</div>
</div>
<div id="welcomePanel" class="task-card" style="text-align:center; padding:40px 20px;">
<h2>欢迎来到 Linux 命令沙盒练习平台!</h2>
<p style="font-size:18px; margin:20px 0;">这是一个零风险的 Linux 命令学习环境,你可以安全地练习常用命令。</p>
<p><strong>📌 使用说明:</strong></p>
<ul style="text-align:left; display:inline-block;">
<li>左侧是课程目录,按顺序学习</li>
<li>阅读任务描述,输入对应命令</li>
<li>答对后自动解锁下一关</li>
<li>支持深色/浅色主题切换</li>
</ul>
<button class="btn" style="margin-top:30px;" onclick="startFirstLesson()">开始学习 →</button>
</div>
</main>
</div>
<footer>
<span id="userBadge">🔒 未认证</span> |
<span id="currentLevel">Lv.1</span> |
<a href="https://github.com/" target="_blank">Powered by Linux Sandbox</a>
</footer>
<script>
// ==============================
// 全局状态
// ==============================
let currentToken = localStorage.getItem('linux_sandbox_token') || '';
let currentUser = localStorage.getItem('linux_sandbox_user') || '';
let currentTheme = localStorage.getItem('linux_sandbox_theme') || 'light';
let currentTaskId = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_sandbox_completed') || '[]');
let COURSE_DATA = null; // 将从 /api/tasks 动态加载
// ==============================
// 主题切换
// ==============================
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('linux_sandbox_theme', theme);
document.getElementById('themeLabel').textContent = theme === 'dark' ? '🌙' : '☀️';
document.getElementById('themeLabelText').textContent = theme === 'dark' ? '深色' : '浅色';
}
function toggleTheme() {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
currentTheme = newTheme;
}
applyTheme(currentTheme);
// ==============================
// 登录/登出
// ==============================
async function login() {
// 建议:实际从登录弹窗调用,这里简化直接用 token
if (currentToken && currentUser) {
updateUI();
return;
}
alert('请先登录!当前简化版支持直接使用内置 Token 执行命令。');
// 临时允许
currentToken = 'safe_linux_2026';
currentUser = 'sandbox_user';
localStorage.setItem('linux_sandbox_token', currentToken);
localStorage.setItem('linux_sandbox_user', currentUser);
logoutBtn.style.display = 'inline-block';
updateUI();
}
function logout() {
localStorage.removeItem('linux_sandbox_token');
localStorage.removeItem('linux_sandbox_user');
currentToken = '';
currentUser = '';
logoutBtn.style.display = 'none';
updateUI();
}
function updateUI() {
const userStatus = document.getElementById('userStatus');
const userBadge = document.getElementById('userBadge');
if (currentToken && currentUser) {
userStatus.textContent = `👤 ${currentUser}`;
userBadge.textContent = '✅ 已认证';
document.getElementById('logoutBtn').style.display = 'inline-block';
} else {
userStatus.textContent = '未登录';
userBadge.textContent = '🔒 未认证';
}
}
// ==============================
// 课程渲染
// ==============================
function renderSidebar() {
const sidebar = document.getElementById('sidebarContent');
let html = '<ul class="lesson-list">';
COURSE_DATA.levels.forEach(level => {
html += `<h2>${level.title}</h2>`;
level.challenges.forEach((task, idx) => {
const done = completedTasks.includes(task.id);
const active = task.id === currentTaskId ? 'active' : '';
const locked = !done && idx > 0 && !completedTasks.includes(level.challenges[idx-1].id);
html += `<li class="lesson-item ${active} ${locked ? 'locked' : ''}" onclick="loadTask('${task.id}')">
<span>${idx+1}. ${task.title}</span>
<span class="lesson-badge">${done ? '✅' : '🔒'}</span>
</li>`;
});
});
html += '</ul>';
sidebar.innerHTML = html;
}
// ==============================
// 任务加载
// ==============================
function loadTask(taskId) {
// 检查前序任务是否完成
let prevTaskId = null;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (task.id === taskId) {
if (prevTaskId && !completedTasks.includes(prevTaskId)) {
alert('请先完成前一关!');
return;
}
currentTaskId = taskId;
renderTask(task);
renderSidebar(); // 更新高亮
return;
}
prevTaskId = task.id;
}
}
alert('任务未找到');
}
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('taskTitle').innerHTML = task.title;
document.getElementById('taskDesc').innerHTML = task.desc;
document.getElementById('taskHint').textContent = `💡 ${task.hint}`;
document.getElementById('taskSuccess').classList.remove('show');
document.getElementById('taskSuccessMsg').textContent = task.success_msg || '恭喜你,任务完成!';
document.getElementById('submitBtn').style.display = 'inline-block';
document.getElementById('nextBtn').style.display = 'none';
// 渲染进度条
const progressBar = document.getElementById('progressBar');
let progressHtml = '';
for (let i=0; i<5; i++) {
progressHtml += `<div class="progress-step ${i === 0 ? 'current' : ''} ${i < 0 ? 'done' : ''}"></div>`;
}
progressBar.innerHTML = progressHtml;
// 聚焦输入框
document.getElementById('cmd').value = '';
document.getElementById('cmd').focus();
}
function startFirstLesson() {
if (!currentToken) {
login();
}
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
loadTask(COURSE_DATA.levels[0].challenges[0].id);
}
// ==============================
// 命令执行
// ==============================
async function runCommand() {
const cmd = document.getElementById('cmd').value.trim();
if (!cmd) return;
const output = document.getElementById('output');
output.textContent = `执行: ${cmd}\n正在运行...`;
document.getElementById('submitBtn').disabled = true;
document.getElementById('cmd').disabled = true;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
output.textContent = data.output || data.message || '(无输出)';
// 检查任务是否完成
checkTaskCompletion(cmd, data.output || '');
} catch (e) {
output.textContent = `❌ 错误: ${e.message}`;
} finally {
document.getElementById('submitBtn').disabled = false;
document.getElementById('cmd').disabled = false;
}
}
function checkTaskCompletion(cmd, output) {
const task = findTaskById(currentTaskId);
if (!task) return;
// 检查 solution 匹配
const solutionMatch = task.success_test || (task.solution && task.solution.includes(cmd));
if (solutionMatch || task.solution?.includes(cmd)) {
handleTaskSuccess(task);
} else {
// 简单启发式检查(未实现 full success_test 解析)
console.log('需要完成任务检查逻辑');
}
}
function handleTaskSuccess(task) {
if (!completedTasks.includes(task.id)) {
completedTasks.push(task.id);
localStorage.setItem('linux_sandbox_completed', JSON.stringify(completedTasks));
alert('✅ 回答正确!\n奖励经验值 +100');
}
document.getElementById('submitBtn').style.display = 'none';
const successBox = document.getElementById('taskSuccess');
successBox.querySelector('h4').innerHTML = '🎉 回答正确!';
successBox.querySelector('p').textContent = task.success_msg || '你已经掌握这个命令的基本用法!';
successBox.classList.add('show');
document.getElementById('nextBtn').style.display = 'inline-block';
}
function nextLevel() {
const task = findTaskById(currentTaskId);
if (!task) return;
// 找到下一关
let found = false;
for (const level of COURSE_DATA.levels) {
for (const t of level.challenges) {
if (found) {
loadTask(t.id);
return;
}
if (t.id === task.id) found = true;
}
}
alert('🎉 所有课程已完成!你已经是 Linux 大师了!🏆');
}
function findTaskById(id) {
for (const level of COURSE_DATA.levels) {
const task = level.challenges.find(t => t.id === id);
if (task) return task;
}
return null;
}
// ==============================
// 事件监听
// ==============================
document.getElementById('cmd').addEventListener('keypress', function(e) {
if (e.key === 'Enter') runCommand();
});
// ==============================
// 初始化
// ==============================
function init() {
updateUI();
// 渲染侧边栏(课程数据加载后调用 renderSidebar
if (COURSE_DATA) renderSidebar();
// 检查是否已有学习进度
if (currentToken && !currentTaskId && !completedTasks.length) {
// 从第一关开始(延迟加载)
setTimeout(startFirstLesson, 1000);
} else if (currentTaskId) {
const task = findTaskById(currentTaskId);
if (task) renderTask(task);
}
}
// 等待课程数据加载完成后初始化 UI
let dataLoaded = false;
setInterval(() => {
if (COURSE_DATA && !dataLoaded) {
init();
dataLoaded = true;
}
}, 500);
// ==============================
// 加载课程数据API
// ==============================
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功', COURSE_DATA);
renderSidebar();
} else {
console.error('❌ 课程数据加载失败:', res.statusText);
}
} catch (e) {
console.error('❌ 课程数据获取异常:', e);
}
}
// 页面全局函数(供 onclick 调用)
window.loadTask = loadTask;
window.runCommand = runCommand;
window.nextLevel = nextLevel;
window.startFirstLesson = startFirstLesson;
// 立即加载课程数据
document.addEventListener('DOMContentLoaded', loadCourseData);
</script>
</body>
</html>

582
index.html.bak Normal file
View File

@@ -0,0 +1,582 @@
<!DOCTYPE html>
<html>
<head>
<title>Linux 命令沙盒练习 - 菜鸟式学习平台</title>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--sidebar-bg: #fff;
--sidebar-border: #ddd;
--main-bg: #fff;
--cmd-bg: #f0f0f0;
--cmd-border: #ccc;
--cmd-text: #333;
--success-color: #4caf50;
--error-color: #f44336;
--highlight-color: #ff9800;
}
[data-theme="dark"] {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--sidebar-bg: #2d2d2d;
--sidebar-border: #444;
--main-bg: #1e1e1e;
--cmd-bg: #2d2d2d;
--cmd-border: #444;
--cmd-text: #fff;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
/* Header */
header {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--sidebar-border);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 20px; color: var(--success-color); }
.header-actions { display: flex; gap: 10px; align-items: center; }
.theme-toggle, .logout-btn {
background: transparent;
border: 1px solid var(--sidebar-border);
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
color: var(--text-color);
}
.theme-toggle:hover, .logout-btn:hover { background: var(--cmd-bg); }
/* Layout */
.container {
display: flex;
height: calc(100vh - 50px);
}
/* Sidebar */
.sidebar {
width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
overflow-y: auto;
padding: 15px;
}
.sidebar h2 {
font-size: 16px;
margin: 10px 0 5px;
color: var(--success-color);
border-bottom: 2px solid var(--highlight-color);
padding-bottom: 5px;
}
.lesson-list {
list-style: none;
padding: 0;
margin: 0;
}
.lesson-item {
padding: 8px 12px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.lesson-item:hover { background: var(--cmd-bg); }
.lesson-item.active {
background: var(--highlight-color);
color: #fff;
}
.lesson-item.locked { opacity: 0.6; cursor: not-allowed; }
.lesson-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: #ccc;
color: #fff;
}
.lesson-item.active .lesson-badge { background: #fff; color: var(--highlight-color); }
.lesson-item.locked .lesson-badge { background: var(--highlight-color); }
/* Main Content */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.task-card {
background: var(--main-bg);
border: 1px solid var(--sidebar-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.task-title {
font-size: 22px;
color: var(--success-color);
margin: 0 0 10px;
}
.task-desc {
background: var(--cmd-bg);
padding: 15px;
border-radius: 6px;
border-left: 4px solid var(--highlight-color);
margin: 10px 0;
line-height: 1.6;
}
.task-hint {
color: var(--highlight-color);
font-weight: bold;
margin: 10px 0;
}
.task-success { display: none; background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0; border:1px solid var(--success-color); }
.task-success.show { display: block; }
.task-success h4 { margin: 0 0 5px; color: var(--success-color); }
/* Command Area */
.cmd-area {
margin-top: 20px;
}
.cmd-bar {
background: var(--cmd-bg);
border: 1px solid var(--cmd-border);
border-radius: 6px;
padding: 10px;
display: flex;
align-items: center;
}
.prompt { margin-right: 10px; color: var(--success-color); font-weight: bold; }
#cmd {
flex: 1;
background: transparent;
border: none;
color: var(--cmd-text);
font-family: 'Courier New', monospace;
font-size: 16px;
padding: 8px;
outline: none;
}
#output {
margin-top: 15px;
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 6px;
padding: 15px;
min-height: 60px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
color: var(--text-color);
}
.btn {
background: var(--success-color);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background 0.2s;
}
.btn:hover { background: #45a049; }
.btn:disabled { background: #ccc; cursor: not-allowed; }
.btn-next { background: var(--highlight-color); }
.btn-next:hover { background: #f57c00; }
/* Footer */
footer {
background: var(--sidebar-bg);
border-top: 1px solid var(--sidebar-border);
padding: 10px 20px;
text-align: right;
font-size: 12px;
color: #888;
}
.progress-bar {
display: flex;
gap: 2px;
margin: 10px 0;
}
.progress-step {
width: 30px;
height: 4px;
background: var(--cmd-bg);
border-radius: 2px;
}
.progress-step.done { background: var(--success-color); }
.progress-step.current { background: var(--highlight-color); }
</style>
</head>
<body>
<header>
<h1>💻 Linux 命令沙盒练习</h1>
<div class="header-actions">
<button class="theme-toggle" onclick="toggleTheme()" title="切换深色/浅色模式">
<span id="themeLabel">☀️</span> <span id="themeLabelText">浅色</span>
</button>
<span id="userStatus">未登录</span>
<button id="logoutBtn" class="logout-btn" onclick="logout()" style="display:none">👋 退出</button>
</div>
</header>
<div class="container">
<aside class="sidebar">
<div id="sidebarContent">
<!-- 课程目录将由 JS 动态生成 -->
<p style="text-align:center; color:#888; margin-top:20px;">加载课程中...</p>
</div>
</aside>
<main class="main-content">
<div id="taskPanel" class="task-card" style="display:none">
<h2 class="task-title" id="taskTitle"></h2>
<div class="progress-bar" id="progressBar"></div>
<p class="task-hint" id="taskHint"></p>
<div class="task-desc" id="taskDesc"></div>
<div class="task-success" id="taskSuccess">
<h4>✅ 回答正确!🎉</h4>
<p id="taskSuccessMsg"></p>
</div>
<div class="cmd-area">
<div class="cmd-bar">
<span class="prompt">$</span>
<input type="text" id="cmd" placeholder="输入 Linux 命令..." autocomplete="off" autofocus>
</div>
<div id="output">等待输入命令...</div>
<button id="submitBtn" class="btn" onclick="runCommand()">执行命令</button>
<button id="nextBtn" class="btn btn-next" onclick="nextLevel()" style="display:none">下一关 →</button>
</div>
</div>
<div id="welcomePanel" class="task-card" style="text-align:center; padding:40px 20px;">
<h2>欢迎来到 Linux 命令沙盒练习平台!</h2>
<p style="font-size:18px; margin:20px 0;">这是一个零风险的 Linux 命令学习环境,你可以安全地练习常用命令。</p>
<p><strong>📌 使用说明:</strong></p>
<ul style="text-align:left; display:inline-block;">
<li>左侧是课程目录,按顺序学习</li>
<li>阅读任务描述,输入对应命令</li>
<li>答对后自动解锁下一关</li>
<li>支持深色/浅色主题切换</li>
</ul>
<button class="btn" style="margin-top:30px;" onclick="startFirstLesson()">开始学习 →</button>
</div>
</main>
</div>
<footer>
<span id="userBadge">🔒 未认证</span> |
<span id="currentLevel">Lv.1</span> |
<a href="https://github.com/" target="_blank">Powered by Linux Sandbox</a>
</footer>
<script>
// ==============================
// 全局状态
// ==============================
let currentToken = localStorage.getItem('linux_sandbox_token') || '';
let currentUser = localStorage.getItem('linux_sandbox_user') || '';
let currentTheme = localStorage.getItem('linux_sandbox_theme') || 'light';
let currentTaskId = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_sandbox_completed') || '[]');
let COURSE_DATA = null; // 将从 /api/tasks 动态加载
// ==============================
// 主题切换
// ==============================
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('linux_sandbox_theme', theme);
document.getElementById('themeLabel').textContent = theme === 'dark' ? '🌙' : '☀️';
document.getElementById('themeLabelText').textContent = theme === 'dark' ? '深色' : '浅色';
}
function toggleTheme() {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
currentTheme = newTheme;
}
applyTheme(currentTheme);
// ==============================
// 登录/登出
// ==============================
async function login() {
// 建议:实际从登录弹窗调用,这里简化直接用 token
if (currentToken && currentUser) {
updateUI();
return;
}
alert('请先登录!当前简化版支持直接使用内置 Token 执行命令。');
// 临时允许
currentToken = 'safe_linux_2026';
currentUser = 'sandbox_user';
localStorage.setItem('linux_sandbox_token', currentToken);
localStorage.setItem('linux_sandbox_user', currentUser);
logoutBtn.style.display = 'inline-block';
updateUI();
}
function logout() {
localStorage.removeItem('linux_sandbox_token');
localStorage.removeItem('linux_sandbox_user');
currentToken = '';
currentUser = '';
logoutBtn.style.display = 'none';
updateUI();
}
function updateUI() {
const userStatus = document.getElementById('userStatus');
const userBadge = document.getElementById('userBadge');
if (currentToken && currentUser) {
userStatus.textContent = `👤 ${currentUser}`;
userBadge.textContent = '✅ 已认证';
document.getElementById('logoutBtn').style.display = 'inline-block';
} else {
userStatus.textContent = '未登录';
userBadge.textContent = '🔒 未认证';
}
}
// ==============================
// 课程渲染
// ==============================
function renderSidebar() {
const sidebar = document.getElementById('sidebarContent');
let html = '<ul class="lesson-list">';
COURSE_DATA.levels.forEach(level => {
html += `<h2>${level.title}</h2>`;
level.challenges.forEach((task, idx) => {
const done = completedTasks.includes(task.id);
const active = task.id === currentTaskId ? 'active' : '';
const locked = !done && idx > 0 && !completedTasks.includes(level.challenges[idx-1].id);
html += `<li class="lesson-item ${active} ${locked ? 'locked' : ''}" onclick="loadTask('${task.id}')">
<span>${idx+1}. ${task.title}</span>
<span class="lesson-badge">${done ? '✅' : '🔒'}</span>
</li>`;
});
});
html += '</ul>';
sidebar.innerHTML = html;
}
// ==============================
// 任务加载
// ==============================
function loadTask(taskId) {
// 检查前序任务是否完成
let prevTaskId = null;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (task.id === taskId) {
if (prevTaskId && !completedTasks.includes(prevTaskId)) {
alert('请先完成前一关!');
return;
}
currentTaskId = taskId;
renderTask(task);
renderSidebar(); // 更新高亮
return;
}
prevTaskId = task.id;
}
}
alert('任务未找到');
}
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('taskTitle').innerHTML = task.title;
document.getElementById('taskDesc').innerHTML = task.desc;
document.getElementById('taskHint').textContent = `💡 ${task.hint}`;
document.getElementById('taskSuccess').classList.remove('show');
document.getElementById('taskSuccessMsg').textContent = task.success_msg || '恭喜你,任务完成!';
document.getElementById('submitBtn').style.display = 'inline-block';
document.getElementById('nextBtn').style.display = 'none';
// 渲染进度条
const progressBar = document.getElementById('progressBar');
let progressHtml = '';
for (let i=0; i<5; i++) {
progressHtml += `<div class="progress-step ${i === 0 ? 'current' : ''} ${i < 0 ? 'done' : ''}"></div>`;
}
progressBar.innerHTML = progressHtml;
// 聚焦输入框
document.getElementById('cmd').value = '';
document.getElementById('cmd').focus();
}
function startFirstLesson() {
if (!currentToken) {
login();
}
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
loadTask(COURSE_DATA.levels[0].challenges[0].id);
}
// ==============================
// 命令执行
// ==============================
async function runCommand() {
const cmd = document.getElementById('cmd').value.trim();
if (!cmd) return;
const output = document.getElementById('output');
output.textContent = `执行: ${cmd}\n正在运行...`;
document.getElementById('submitBtn').disabled = true;
document.getElementById('cmd').disabled = true;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
output.textContent = data.output || data.message || '(无输出)';
// 检查任务是否完成
checkTaskCompletion(cmd, data.output || '');
} catch (e) {
output.textContent = `❌ 错误: ${e.message}`;
} finally {
document.getElementById('submitBtn').disabled = false;
document.getElementById('cmd').disabled = false;
}
}
function checkTaskCompletion(cmd, output) {
const task = findTaskById(currentTaskId);
if (!task) return;
// 检查 solution 匹配
const solutionMatch = task.success_test || (task.solution && task.solution.includes(cmd));
if (solutionMatch || task.solution?.includes(cmd)) {
handleTaskSuccess(task);
} else {
// 简单启发式检查(未实现 full success_test 解析)
console.log('需要完成任务检查逻辑');
}
}
function handleTaskSuccess(task) {
if (!completedTasks.includes(task.id)) {
completedTasks.push(task.id);
localStorage.setItem('linux_sandbox_completed', JSON.stringify(completedTasks));
alert('✅ 回答正确!\n奖励经验值 +100');
}
document.getElementById('submitBtn').style.display = 'none';
const successBox = document.getElementById('taskSuccess');
successBox.querySelector('h4').innerHTML = '🎉 回答正确!';
successBox.querySelector('p').textContent = task.success_msg || '你已经掌握这个命令的基本用法!';
successBox.classList.add('show');
document.getElementById('nextBtn').style.display = 'inline-block';
}
function nextLevel() {
const task = findTaskById(currentTaskId);
if (!task) return;
// 找到下一关
let found = false;
for (const level of COURSE_DATA.levels) {
for (const t of level.challenges) {
if (found) {
loadTask(t.id);
return;
}
if (t.id === task.id) found = true;
}
}
alert('🎉 所有课程已完成!你已经是 Linux 大师了!🏆');
}
function findTaskById(id) {
for (const level of COURSE_DATA.levels) {
const task = level.challenges.find(t => t.id === id);
if (task) return task;
}
return null;
}
// ==============================
// 事件监听
// ==============================
document.getElementById('cmd').addEventListener('keypress', function(e) {
if (e.key === 'Enter') runCommand();
});
// ==============================
// 初始化
// ==============================
function init() {
updateUI();
// 渲染侧边栏(课程数据加载后调用 renderSidebar
if (COURSE_DATA) renderSidebar();
// 检查是否已有学习进度
if (currentToken && !currentTaskId && !completedTasks.length) {
// 从第一关开始(延迟加载)
setTimeout(startFirstLesson, 1000);
} else if (currentTaskId) {
const task = findTaskById(currentTaskId);
if (task) renderTask(task);
}
}
// 等待课程数据加载完成后初始化 UI
let dataLoaded = false;
setInterval(() => {
if (COURSE_DATA && !dataLoaded) {
init();
dataLoaded = true;
}
}, 500);
// ==============================
// 加载课程数据API
// ==============================
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功', COURSE_DATA);
renderSidebar();
} else {
console.error('❌ 课程数据加载失败:', res.statusText);
}
} catch (e) {
console.error('❌ 课程数据获取异常:', e);
}
}
// 页面全局函数(供 onclick 调用)
window.loadTask = loadTask;
window.runCommand = runCommand;
window.nextLevel = nextLevel;
window.startFirstLesson = startFirstLesson;
// 立即加载课程数据
document.addEventListener('DOMContentLoaded', loadCourseData);
</script>
</body>
</html>

644
index.html.bak.1772815479 Normal file
View File

@@ -0,0 +1,644 @@
<!DOCTYPE html>
<html>
<head>
<title>Linux 命令沙盒练习 - 菜鸟式学习平台</title>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--sidebar-bg: #fff;
--sidebar-border: #ddd;
--main-bg: #fff;
--cmd-bg: #f0f0f0;
--cmd-border: #ccc;
--cmd-text: #333;
--success-color: #4caf50;
--error-color: #f44336;
--highlight-color: #ff9800;
}
[data-theme="dark"] {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--sidebar-bg: #2d2d2d;
--sidebar-border: #444;
--main-bg: #1e1e1e;
--cmd-bg: #2d2d2d;
--cmd-border: #444;
--cmd-text: #fff;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
/* Header */
header {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--sidebar-border);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 20px; color: var(--success-color); }
.header-actions { display: flex; gap: 10px; align-items: center; }
.theme-toggle, .logout-btn {
background: transparent;
border: 1px solid var(--sidebar-border);
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
color: var(--text-color);
}
.theme-toggle:hover, .logout-btn:hover { background: var(--cmd-bg); }
/* Layout */
.container {
display: flex;
height: calc(100vh - 50px);
}
/* Sidebar */
.sidebar {
width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
overflow-y: auto;
padding: 15px;
}
.sidebar h2 {
font-size: 16px;
margin: 10px 0 5px;
color: var(--success-color);
border-bottom: 2px solid var(--highlight-color);
padding-bottom: 5px;
}
.lesson-list {
list-style: none;
padding: 0;
margin: 0;
}
.lesson-item {
padding: 8px 12px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.lesson-item:hover { background: var(--cmd-bg); }
.lesson-item.active {
background: var(--highlight-color);
color: #fff;
}
.lesson-item.locked { opacity: 0.6; cursor: not-allowed; }
.lesson-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: #ccc;
color: #fff;
}
.lesson-item.active .lesson-badge { background: #fff; color: var(--highlight-color); }
.lesson-item.locked .lesson-badge { background: var(--highlight-color); }
/* Main Content */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.task-card {
background: var(--main-bg);
border: 1px solid var(--sidebar-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.task-title {
font-size: 22px;
color: var(--success-color);
margin: 0 0 10px;
}
.task-desc {
background: var(--cmd-bg);
padding: 15px;
border-radius: 6px;
border-left: 4px solid var(--highlight-color);
margin: 10px 0;
line-height: 1.6;
}
.task-hint {
color: var(--highlight-color);
font-weight: bold;
margin: 10px 0;
}
.task-success { display: none; background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0; border:1px solid var(--success-color); }
.task-success.show { display: block; }
.task-success h4 { margin: 0 0 5px; color: var(--success-color); }
/* Command Area */
.cmd-area {
margin-top: 20px;
}
.cmd-bar {
background: var(--cmd-bg);
border: 1px solid var(--cmd-border);
border-radius: 6px;
padding: 10px;
display: flex;
align-items: center;
}
.prompt { margin-right: 10px; color: var(--success-color); font-weight: bold; }
#cmd {
flex: 1;
background: transparent;
border: none;
color: var(--cmd-text);
font-family: 'Courier New', monospace;
font-size: 16px;
padding: 8px;
outline: none;
}
#output {
margin-top: 15px;
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 6px;
padding: 15px;
min-height: 60px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
color: var(--text-color);
}
.btn {
background: var(--success-color);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background 0.2s;
}
.btn:hover { background: #45a049; }
.btn:disabled { background: #ccc; cursor: not-allowed; }
.btn-next { background: var(--highlight-color); }
.btn-next:hover { background: #f57c00; }
/* Footer */
footer {
background: var(--sidebar-bg);
border-top: 1px solid var(--sidebar-border);
padding: 10px 20px;
text-align: right;
font-size: 12px;
color: #888;
}
.progress-bar {
display: flex;
gap: 2px;
margin: 10px 0;
}
.progress-step {
width: 30px;
height: 4px;
background: var(--cmd-bg);
border-radius: 2px;
}
.progress-step.done { background: var(--success-color); }
.progress-step.current { background: var(--highlight-color); }
</style>
</head>
<body>
<header>
<h1>💻 Linux 命令沙盒练习</h1>
<div class="header-actions">
<button class="theme-toggle" onclick="showWrongAnswers()" title="错题本">📚 错题本</button>
<button class="theme-toggle" onclick="toggleTheme()" title="切换深色/浅色模式">
<span id="themeLabel">☀️</span> <span id="themeLabelText">浅色</span>
</button>
<span id="userStatus">未登录</span>
<button id="logoutBtn" class="logout-btn" onclick="logout()" style="display:none">👋 退出</button>
</div>
</header>
<div class="container">
<aside class="sidebar">
<div id="sidebarContent">
<!-- 课程目录将由 JS 动态生成 -->
<p style="text-align:center; color:#888; margin-top:20px;">加载课程中...</p>
</div>
</aside>
<main class="main-content">
<div id="taskPanel" class="task-card" style="display:none">
<h2 class="task-title" id="taskTitle"></h2>
<div class="progress-bar" id="progressBar"></div>
<p class="task-hint" id="taskHint"></p>
<div class="task-desc" id="taskDesc"></div>
<div class="task-success" id="taskSuccess">
<h4>✅ 回答正确!🎉</h4>
<p id="taskSuccessMsg"></p>
</div>
<div class="cmd-area">
<div class="cmd-bar">
<span class="prompt">$</span>
<input type="text" id="cmd" placeholder="输入 Linux 命令..." autocomplete="off" autofocus>
</div>
<div id="output">等待输入命令...</div>
<button id="submitBtn" class="btn" onclick="runCommand()">执行命令</button>
<button id="nextBtn" class="btn btn-next" onclick="nextLevel()" style="display:none">下一关 →</button>
</div>
</div>
<div id="welcomePanel" class="task-card" style="text-align:center; padding:40px 20px;">
<h2>欢迎来到 Linux 命令沙盒练习平台!</h2>
<p style="font-size:18px; margin:20px 0;">这是一个零风险的 Linux 命令学习环境,你可以安全地练习常用命令。</p>
<p><strong>📌 使用说明:</strong></p>
<ul style="text-align:left; display:inline-block;">
<li>左侧是课程目录,按顺序学习</li>
<li>阅读任务描述,输入对应命令</li>
<li>答对后自动解锁下一关</li>
<li>支持深色/浅色主题切换</li>
</ul>
<button class="btn" style="margin-top:30px;" onclick="startFirstLesson()">开始学习 →</button>
</div>
</main>
</div>
<footer>
<span id="userBadge">🔒 未认证</span> |
<span id="currentLevel">Lv.1</span> |
<a href="https://github.com/" target="_blank">Powered by Linux Sandbox</a>
</footer>
<script>
// ==============================
// 全局状态
// ==============================
let currentToken = localStorage.getItem('linux_sandbox_token') || '';
let currentUser = localStorage.getItem('linux_sandbox_user') || '';
let currentTheme = localStorage.getItem('linux_sandbox_theme') || 'light';
let currentTaskId = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_sandbox_completed') || '[]');
let wrongAnswers = JSON.parse(localStorage.getItem('linux_sandbox_wrong' || '[]');
let COURSE_DATA = null; // 将从 /api/tasks 动态加载
// ==============================
// 主题切换
// ==============================
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('linux_sandbox_theme', theme);
document.getElementById('themeLabel').textContent = theme === 'dark' ? '🌙' : '☀️';
document.getElementById('themeLabelText').textContent = theme === 'dark' ? '深色' : '浅色';
}
function toggleTheme() {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
currentTheme = newTheme;
}
applyTheme(currentTheme);
// ==============================
// 登录/登出
// ==============================
async function login() {
// 建议:实际从登录弹窗调用,这里简化直接用 token
if (currentToken && currentUser) {
updateUI();
return;
}
alert('请先登录!当前简化版支持直接使用内置 Token 执行命令。');
// 临时允许
currentToken = 'safe_linux_2026';
currentUser = 'sandbox_user';
localStorage.setItem('linux_sandbox_token', currentToken);
localStorage.setItem('linux_sandbox_user', currentUser);
logoutBtn.style.display = 'inline-block';
updateUI();
}
function logout() {
localStorage.removeItem('linux_sandbox_token');
localStorage.removeItem('linux_sandbox_user');
currentToken = '';
currentUser = '';
logoutBtn.style.display = 'none';
updateUI();
}
function updateUI() {
const userStatus = document.getElementById('userStatus');
const userBadge = document.getElementById('userBadge');
if (currentToken && currentUser) {
userStatus.textContent = `👤 ${currentUser}`;
userBadge.textContent = '✅ 已认证';
document.getElementById('logoutBtn').style.display = 'inline-block';
} else {
userStatus.textContent = '未登录';
userBadge.textContent = '🔒 未认证';
}
}
// ==============================
// 课程渲染
// ==============================
function renderSidebar() {
const sidebar = document.getElementById('sidebarContent');
let html = '<ul class="lesson-list">';
COURSE_DATA.levels.forEach(level => {
html += `<h2>${level.title}</h2>`;
level.challenges.forEach((task, idx) => {
const done = completedTasks.includes(task.id);
const active = task.id === currentTaskId ? 'active' : '';
const locked = !done && idx > 0 && !completedTasks.includes(level.challenges[idx-1].id);
html += `<li class="lesson-item ${active} ${locked ? 'locked' : ''}" onclick="loadTask('${task.id}')">
<span>${idx+1}. ${task.title}</span>
<span class="lesson-badge">${done ? '✅' : '🔒'}</span>
</li>`;
});
});
html += '</ul>';
sidebar.innerHTML = html;
}
// ==============================
// 任务加载
// ==============================
function loadTask(taskId) {
// 检查前序任务是否完成
let prevTaskId = null;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (task.id === taskId) {
if (prevTaskId && !completedTasks.includes(prevTaskId)) {
alert('请先完成前一关!');
return;
}
currentTaskId = taskId;
renderTask(task);
renderSidebar(); // 更新高亮
return;
}
prevTaskId = task.id;
}
}
alert('任务未找到');
}
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('taskTitle').innerHTML = task.title;
document.getElementById('taskDesc').innerHTML = task.description;
document.getElementById('taskHint').textContent = `💡 ${task.hint}`;
document.getElementById('taskSuccess').classList.remove('show');
document.getElementById('taskSuccessMsg').textContent = task.success_msg || '恭喜你,任务完成!';
document.getElementById('submitBtn').style.display = 'inline-block';
document.getElementById('nextBtn').style.display = 'none';
// 渲染进度条
const progressBar = document.getElementById('progressBar');
let progressHtml = '';
for (let i=0; i<5; i++) {
progressHtml += `<div class="progress-step ${i === 0 ? 'current' : ''} ${i < 0 ? 'done' : ''}"></div>`;
}
progressBar.innerHTML = progressHtml;
// 聚焦输入框
document.getElementById('cmd').value = '';
document.getElementById('cmd').focus();
}
function startFirstLesson() {
if (!currentToken) {
login();
}
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
loadTask(COURSE_DATA.levels[0].challenges[0].id);
}
// ==============================
// 命令执行
// ==============================
async function runCommand() {
const cmd = document.getElementById('cmd').value.trim();
if (!cmd) return;
const output = document.getElementById('output');
output.textContent = `执行: ${cmd}\n正在运行...`;
document.getElementById('submitBtn').disabled = true;
document.getElementById('cmd').disabled = true;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
output.textContent = data.output || data.message || '(无输出)';
// 检查任务是否完成
checkTaskCompletion(cmd, data.output || '');
} catch (e) {
output.textContent = `❌ 错误: ${e.message}`;
} finally {
document.getElementById('submitBtn').disabled = false;
document.getElementById('cmd').disabled = false;
}
}
function checkTaskCompletion(cmd, output) {
const task = findTaskById(currentTaskId);
if (!task) return;
// 检查 solution 匹配
const solutionMatch = task.success_test || (task.solution && task.solution.includes(cmd));
if (solutionMatch || task.solution?.includes(cmd)) {
handleTaskSuccess(task);
} else {
// 简单启发式检查(未实现 full success_test 解析)
console.log('需要完成任务检查逻辑');
recordWrongAnswer(currentTaskId, cmd, output);
}
}
function handleTaskSuccess(task) {
if (!completedTasks.includes(task.id)) {
completedTasks.push(task.id);
localStorage.setItem('linux_sandbox_completed', JSON.stringify(completedTasks));
alert('✅ 回答正确!\n奖励经验值 +100');
}
document.getElementById('submitBtn').style.display = 'none';
const successBox = document.getElementById('taskSuccess');
successBox.querySelector('h4').innerHTML = '🎉 回答正确!';
successBox.querySelector('p').textContent = task.success_msg || '你已经掌握这个命令的基本用法!';
successBox.classList.add('show');
document.getElementById('nextBtn').style.display = 'inline-block';
}
function nextLevel() {
const task = findTaskById(currentTaskId);
if (!task) return;
// 找到下一关
let found = false;
for (const level of COURSE_DATA.levels) {
for (const t of level.challenges) {
if (found) {
loadTask(t.id);
return;
}
if (t.id === task.id) found = true;
}
}
alert('🎉 所有课程已完成!你已经是 Linux 大师了!🏆');
}
function findTaskById(id) {
for (const level of COURSE_DATA.levels) {
const task = level.challenges.find(t => t.id === id);
if (task) return task;
}
return null;
}
// ==============================
// 事件监听
// ==============================
document.getElementById('cmd').addEventListener('keypress', function(e) {
if (e.key === 'Enter') runCommand();
});
// ==============================
// 初始化
// ==============================
function init() {
updateUI();
// 渲染侧边栏(课程数据加载后调用 renderSidebar
if (COURSE_DATA) renderSidebar();
// 检查是否已有学习进度
if (currentToken && !currentTaskId && !completedTasks.length) {
// 从第一关开始(延迟加载)
setTimeout(startFirstLesson, 1000);
} else if (currentTaskId) {
const task = findTaskById(currentTaskId);
if (task) renderTask(task);
}
}
// 等待课程数据加载完成后初始化 UI
let dataLoaded = false;
setInterval(() => {
if (COURSE_DATA && !dataLoaded) {
init();
dataLoaded = true;
}
}, 500);
// ==============================
// 加载课程数据API
// ==============================
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功', COURSE_DATA);
renderSidebar();
} else {
console.error('❌ 课程数据加载失败:', res.statusText);
}
} catch (e) {
console.error('❌ 课程数据获取异常:', e);
}
}
// 页面全局函数(供 onclick 调用)
window.loadTask = loadTask;
window.runCommand = runCommand;
window.nextLevel = nextLevel;
window.startFirstLesson = startFirstLesson;
// ==============================
// 错题记录功能
// ==============================
function recordWrongAnswer(taskId, cmd, output) {
const wrongItem = {
taskId: taskId,
cmd: cmd,
output: output,
timestamp: new Date().toISOString(),
count: 1
};
// 检查是否已存在
const existing = wrongAnswers.find(w => w.taskId === taskId);
if (existing) {
existing.count++;
existing.cmd = cmd;
existing.output = output;
existing.timestamp = wrongItem.timestamp;
} else {
wrongAnswers.push(wrongItem);
}
localStorage.setItem('linux_sandbox_wrong', JSON.stringify(wrongAnswers));
}
function showWrongAnswers() {
if (wrongAnswers.length === 0) {
alert('🎉 还没有错题记录!继续保持!');
return;
}
let html = '<h3>📚 错题本</h3><ul>';
wrongAnswers.forEach(w => {
const task = findTaskById(w.taskId);
const title = task ? task.title : w.taskId;
html += `<li style="margin:10px 0;padding:10px;background:#fff3e0;border-radius:4px;">
<strong>${title}</strong> (错误 ${w.count} 次)<br>
<code>${w.cmd}</code><br>
<small>${new Date(w.timestamp).toLocaleString()}</small>
</li>`;
});
html += '</ul>';
// 创建弹窗显示
const modal = document.createElement('div');
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;';
modal.innerHTML = `<div style="background:white;padding:20px;border-radius:8px;max-width:600px;max-height:80vh;overflow:auto;">${html}<br><button onclick="this.parentElement.parentElement.remove()" style="padding:10px 20px;background:#4caf50;color:white;border:none;border-radius:4px;cursor:pointer;">关闭</button></div>`;
document.body.appendChild(modal);
}
function clearWrongAnswers() {
if (confirm('确定要清空所有错题记录吗?')) {
wrongAnswers = [];
localStorage.removeItem('linux_sandbox_wrong');
alert('✅ 错题记录已清空');
}
}
// 立即加载课程数据
document.addEventListener('DOMContentLoaded', loadCourseData);
</script>
</body>
</html>

648
index.html.debug Normal file
View File

@@ -0,0 +1,648 @@
<!DOCTYPE html>
<html>
<head>
<title>Linux 命令沙盒练习 - 菜鸟式学习平台</title>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--sidebar-bg: #fff;
--sidebar-border: #ddd;
--main-bg: #fff;
--cmd-bg: #f0f0f0;
--cmd-border: #ccc;
--cmd-text: #333;
--success-color: #4caf50;
--error-color: #f44336;
--highlight-color: #ff9800;
}
[data-theme="dark"] {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--sidebar-bg: #2d2d2d;
--sidebar-border: #444;
--main-bg: #1e1e1e;
--cmd-bg: #2d2d2d;
--cmd-border: #444;
--cmd-text: #fff;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
/* Header */
header {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--sidebar-border);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 20px; color: var(--success-color); }
.header-actions { display: flex; gap: 10px; align-items: center; }
.theme-toggle, .logout-btn {
background: transparent;
border: 1px solid var(--sidebar-border);
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
color: var(--text-color);
}
.theme-toggle:hover, .logout-btn:hover { background: var(--cmd-bg); }
/* Layout */
.container {
display: flex;
height: calc(100vh - 50px);
}
/* Sidebar */
.sidebar {
width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
overflow-y: auto;
padding: 15px;
}
.sidebar h2 {
font-size: 16px;
margin: 10px 0 5px;
color: var(--success-color);
border-bottom: 2px solid var(--highlight-color);
padding-bottom: 5px;
}
.lesson-list {
list-style: none;
padding: 0;
margin: 0;
}
.lesson-item {
padding: 8px 12px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.lesson-item:hover { background: var(--cmd-bg); }
.lesson-item.active {
background: var(--highlight-color);
color: #fff;
}
.lesson-item.locked { opacity: 0.6; cursor: not-allowed; }
.lesson-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: #ccc;
color: #fff;
}
.lesson-item.active .lesson-badge { background: #fff; color: var(--highlight-color); }
.lesson-item.locked .lesson-badge { background: var(--highlight-color); }
/* Main Content */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.task-card {
background: var(--main-bg);
border: 1px solid var(--sidebar-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.task-title {
font-size: 22px;
color: var(--success-color);
margin: 0 0 10px;
}
.task-desc {
background: var(--cmd-bg);
padding: 15px;
border-radius: 6px;
border-left: 4px solid var(--highlight-color);
margin: 10px 0;
line-height: 1.6;
}
.task-hint {
color: var(--highlight-color);
font-weight: bold;
margin: 10px 0;
}
.task-success { display: none; background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0; border:1px solid var(--success-color); }
.task-success.show { display: block; }
.task-success h4 { margin: 0 0 5px; color: var(--success-color); }
/* Command Area */
.cmd-area {
margin-top: 20px;
}
.cmd-bar {
background: var(--cmd-bg);
border: 1px solid var(--cmd-border);
border-radius: 6px;
padding: 10px;
display: flex;
align-items: center;
}
.prompt { margin-right: 10px; color: var(--success-color); font-weight: bold; }
#cmd {
flex: 1;
background: transparent;
border: none;
color: var(--cmd-text);
font-family: 'Courier New', monospace;
font-size: 16px;
padding: 8px;
outline: none;
}
#output {
margin-top: 15px;
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 6px;
padding: 15px;
min-height: 60px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
color: var(--text-color);
}
.btn {
background: var(--success-color);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background 0.2s;
}
.btn:hover { background: #45a049; }
.btn:disabled { background: #ccc; cursor: not-allowed; }
.btn-next { background: var(--highlight-color); }
.btn-next:hover { background: #f57c00; }
/* Footer */
footer {
background: var(--sidebar-bg);
border-top: 1px solid var(--sidebar-border);
padding: 10px 20px;
text-align: right;
font-size: 12px;
color: #888;
}
.progress-bar {
display: flex;
gap: 2px;
margin: 10px 0;
}
.progress-step {
width: 30px;
height: 4px;
background: var(--cmd-bg);
border-radius: 2px;
}
.progress-step.done { background: var(--success-color); }
.progress-step.current { background: var(--highlight-color); }
</style>
</head>
<body>
<header>
<h1>💻 Linux 命令沙盒练习</h1>
<div class="header-actions">
<button class="theme-toggle" onclick="showWrongAnswers()" title="错题本">📚 错题本</button>
<button class="theme-toggle" onclick="toggleTheme()" title="切换深色/浅色模式">
<span id="themeLabel">☀️</span> <span id="themeLabelText">浅色</span>
</button>
<span id="userStatus">未登录</span>
<button id="logoutBtn" class="logout-btn" onclick="logout()" style="display:none">👋 退出</button>
</div>
</header>
<div class="container">
<aside class="sidebar">
<div id="sidebarContent">
<!-- 课程目录将由 JS 动态生成 -->
<p style="text-align:center; color:#888; margin-top:20px;">加载课程中...</p>
</div>
</aside>
<main class="main-content">
<div id="taskPanel" class="task-card" style="display:none">
<h2 class="task-title" id="taskTitle"></h2>
<div class="progress-bar" id="progressBar"></div>
<p class="task-hint" id="taskHint"></p>
<div class="task-desc" id="taskDesc"></div>
<div class="task-success" id="taskSuccess">
<h4>✅ 回答正确!🎉</h4>
<p id="taskSuccessMsg"></p>
</div>
<div class="cmd-area">
<div class="cmd-bar">
<span class="prompt">$</span>
<input type="text" id="cmd" placeholder="输入 Linux 命令..." autocomplete="off" autofocus>
</div>
<div id="output">等待输入命令...</div>
<button id="submitBtn" class="btn" onclick="runCommand()">执行命令</button>
<button id="nextBtn" class="btn btn-next" onclick="nextLevel()" style="display:none">下一关 →</button>
</div>
</div>
<div id="welcomePanel" class="task-card" style="text-align:center; padding:40px 20px;">
<h2>欢迎来到 Linux 命令沙盒练习平台!</h2>
<p style="font-size:18px; margin:20px 0;">这是一个零风险的 Linux 命令学习环境,你可以安全地练习常用命令。</p>
<p><strong>📌 使用说明:</strong></p>
<ul style="text-align:left; display:inline-block;">
<li>左侧是课程目录,按顺序学习</li>
<li>阅读任务描述,输入对应命令</li>
<li>答对后自动解锁下一关</li>
<li>支持深色/浅色主题切换</li>
</ul>
<button class="btn" style="margin-top:30px;" onclick="startFirstLesson()">开始学习 →</button>
</div>
</main>
</div>
<footer>
<span id="userBadge">🔒 未认证</span> |
<span id="currentLevel">Lv.1</span> |
<a href="https://github.com/" target="_blank">Powered by Linux Sandbox</a>
</footer>
<script>
// ==============================
// 全局状态
// ==============================
let currentToken = localStorage.getItem('linux_sandbox_token') || '';
let currentUser = localStorage.getItem('linux_sandbox_user') || '';
let currentTheme = localStorage.getItem('linux_sandbox_theme') || 'light';
let currentTaskId = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_sandbox_completed') || '[]');
let wrongAnswers = JSON.parse(localStorage.getItem('linux_sandbox_wrong' || '[]');
let COURSE_DATA = null; // 将从 /api/tasks 动态加载
// ==============================
// 主题切换
// ==============================
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('linux_sandbox_theme', theme);
document.getElementById('themeLabel').textContent = theme === 'dark' ? '🌙' : '☀️';
document.getElementById('themeLabelText').textContent = theme === 'dark' ? '深色' : '浅色';
}
function toggleTheme() {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
currentTheme = newTheme;
}
applyTheme(currentTheme);
// ==============================
// 登录/登出
// ==============================
async function login() {
// 建议:实际从登录弹窗调用,这里简化直接用 token
if (currentToken && currentUser) {
updateUI();
return;
}
alert('请先登录!当前简化版支持直接使用内置 Token 执行命令。');
// 临时允许
currentToken = 'safe_linux_2026';
currentUser = 'sandbox_user';
localStorage.setItem('linux_sandbox_token', currentToken);
localStorage.setItem('linux_sandbox_user', currentUser);
logoutBtn.style.display = 'inline-block';
updateUI();
}
function logout() {
localStorage.removeItem('linux_sandbox_token');
localStorage.removeItem('linux_sandbox_user');
currentToken = '';
currentUser = '';
logoutBtn.style.display = 'none';
updateUI();
}
function updateUI() {
const userStatus = document.getElementById('userStatus');
const userBadge = document.getElementById('userBadge');
if (currentToken && currentUser) {
userStatus.textContent = `👤 ${currentUser}`;
userBadge.textContent = '✅ 已认证';
document.getElementById('logoutBtn').style.display = 'inline-block';
} else {
userStatus.textContent = '未登录';
userBadge.textContent = '🔒 未认证';
}
}
// ==============================
// 课程渲染
// ==============================
function renderSidebar() {
if (!COURSE_DATA || !COURSE_DATA.levels) {
console.log("课程数据尚未加载");
return;
}
const sidebar = document.getElementById('sidebarContent');
let html = '<ul class="lesson-list">';
COURSE_DATA.levels.forEach(level => {
html += `<h2>${level.title}</h2>`;
level.challenges.forEach((task, idx) => {
const done = completedTasks.includes(task.id);
const active = task.id === currentTaskId ? 'active' : '';
const locked = !done && idx > 0 && !completedTasks.includes(level.challenges[idx-1].id);
html += `<li class="lesson-item ${active} ${locked ? 'locked' : ''}" onclick="loadTask('${task.id}')">
<span>${idx+1}. ${task.title}</span>
<span class="lesson-badge">${done ? '✅' : '🔒'}</span>
</li>`;
});
});
html += '</ul>';
sidebar.innerHTML = html;
}
// ==============================
// 任务加载
// ==============================
function loadTask(taskId) {
// 检查前序任务是否完成
let prevTaskId = null;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (task.id === taskId) {
if (prevTaskId && !completedTasks.includes(prevTaskId)) {
alert('请先完成前一关!');
return;
}
currentTaskId = taskId;
renderTask(task);
renderSidebar(); // 更新高亮
return;
}
prevTaskId = task.id;
}
}
alert('任务未找到');
}
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('taskTitle').innerHTML = task.title;
document.getElementById('taskDesc').innerHTML = task.description;
document.getElementById('taskHint').textContent = `💡 ${task.hint}`;
document.getElementById('taskSuccess').classList.remove('show');
document.getElementById('taskSuccessMsg').textContent = task.success_msg || '恭喜你,任务完成!';
document.getElementById('submitBtn').style.display = 'inline-block';
document.getElementById('nextBtn').style.display = 'none';
// 渲染进度条
const progressBar = document.getElementById('progressBar');
let progressHtml = '';
for (let i=0; i<5; i++) {
progressHtml += `<div class="progress-step ${i === 0 ? 'current' : ''} ${i < 0 ? 'done' : ''}"></div>`;
}
progressBar.innerHTML = progressHtml;
// 聚焦输入框
document.getElementById('cmd').value = '';
document.getElementById('cmd').focus();
}
function startFirstLesson() {
if (!currentToken) {
login();
}
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
loadTask(COURSE_DATA.levels[0].challenges[0].id);
}
// ==============================
// 命令执行
// ==============================
async function runCommand() {
const cmd = document.getElementById('cmd').value.trim();
if (!cmd) return;
const output = document.getElementById('output');
output.textContent = `执行: ${cmd}\n正在运行...`;
document.getElementById('submitBtn').disabled = true;
document.getElementById('cmd').disabled = true;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
output.textContent = data.output || data.message || '(无输出)';
// 检查任务是否完成
checkTaskCompletion(cmd, data.output || '');
} catch (e) {
output.textContent = `❌ 错误: ${e.message}`;
} finally {
document.getElementById('submitBtn').disabled = false;
document.getElementById('cmd').disabled = false;
}
}
function checkTaskCompletion(cmd, output) {
const task = findTaskById(currentTaskId);
if (!task) return;
// 检查 solution 匹配
const solutionMatch = task.success_test || (task.solution && task.solution.includes(cmd));
if (solutionMatch || task.solution?.includes(cmd)) {
handleTaskSuccess(task);
} else {
// 简单启发式检查(未实现 full success_test 解析)
console.log('需要完成任务检查逻辑');
recordWrongAnswer(currentTaskId, cmd, output);
}
}
function handleTaskSuccess(task) {
if (!completedTasks.includes(task.id)) {
completedTasks.push(task.id);
localStorage.setItem('linux_sandbox_completed', JSON.stringify(completedTasks));
alert('✅ 回答正确!\n奖励经验值 +100');
}
document.getElementById('submitBtn').style.display = 'none';
const successBox = document.getElementById('taskSuccess');
successBox.querySelector('h4').innerHTML = '🎉 回答正确!';
successBox.querySelector('p').textContent = task.success_msg || '你已经掌握这个命令的基本用法!';
successBox.classList.add('show');
document.getElementById('nextBtn').style.display = 'inline-block';
}
function nextLevel() {
const task = findTaskById(currentTaskId);
if (!task) return;
// 找到下一关
let found = false;
for (const level of COURSE_DATA.levels) {
for (const t of level.challenges) {
if (found) {
loadTask(t.id);
return;
}
if (t.id === task.id) found = true;
}
}
alert('🎉 所有课程已完成!你已经是 Linux 大师了!🏆');
}
function findTaskById(id) {
for (const level of COURSE_DATA.levels) {
const task = level.challenges.find(t => t.id === id);
if (task) return task;
}
return null;
}
// ==============================
// 事件监听
// ==============================
document.getElementById('cmd').addEventListener('keypress', function(e) {
if (e.key === 'Enter') runCommand();
});
// ==============================
// 初始化
// ==============================
function init() {
updateUI();
// 渲染侧边栏(课程数据加载后调用 renderSidebar
if (COURSE_DATA && COURSE_DATA.levels) renderSidebar();
// 检查是否已有学习进度
if (currentToken && !currentTaskId && !completedTasks.length) {
// 从第一关开始(延迟加载)
setTimeout(startFirstLesson, 1000);
} else if (currentTaskId) {
const task = findTaskById(currentTaskId);
if (task) renderTask(task);
}
}
// 等待课程数据加载完成后初始化 UI
let dataLoaded = false;
setInterval(() => {
if (COURSE_DATA && !dataLoaded) {
init();
dataLoaded = true;
}
}, 500);
// ==============================
// 加载课程数据API
// ==============================
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功', COURSE_DATA);
renderSidebar();
} else {
console.error('❌ 课程数据加载失败:', res.statusText);
}
} catch (e) {
console.error('❌ 课程数据获取异常:', e);
}
}
// 页面全局函数(供 onclick 调用)
window.loadTask = loadTask;
window.runCommand = runCommand;
window.nextLevel = nextLevel;
window.startFirstLesson = startFirstLesson;
// ==============================
// 错题记录功能
// ==============================
function recordWrongAnswer(taskId, cmd, output) {
const wrongItem = {
taskId: taskId,
cmd: cmd,
output: output,
timestamp: new Date().toISOString(),
count: 1
};
// 检查是否已存在
const existing = wrongAnswers.find(w => w.taskId === taskId);
if (existing) {
existing.count++;
existing.cmd = cmd;
existing.output = output;
existing.timestamp = wrongItem.timestamp;
} else {
wrongAnswers.push(wrongItem);
}
localStorage.setItem('linux_sandbox_wrong', JSON.stringify(wrongAnswers));
}
function showWrongAnswers() {
if (wrongAnswers.length === 0) {
alert('🎉 还没有错题记录!继续保持!');
return;
}
let html = '<h3>📚 错题本</h3><ul>';
wrongAnswers.forEach(w => {
const task = findTaskById(w.taskId);
const title = task ? task.title : w.taskId;
html += `<li style="margin:10px 0;padding:10px;background:#fff3e0;border-radius:4px;">
<strong>${title}</strong> (错误 ${w.count} 次)<br>
<code>${w.cmd}</code><br>
<small>${new Date(w.timestamp).toLocaleString()}</small>
</li>`;
});
html += '</ul>';
// 创建弹窗显示
const modal = document.createElement('div');
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;';
modal.innerHTML = `<div style="background:white;padding:20px;border-radius:8px;max-width:600px;max-height:80vh;overflow:auto;">${html}<br><button onclick="this.parentElement.parentElement.remove()" style="padding:10px 20px;background:#4caf50;color:white;border:none;border-radius:4px;cursor:pointer;">关闭</button></div>`;
document.body.appendChild(modal);
}
function clearWrongAnswers() {
if (confirm('确定要清空所有错题记录吗?')) {
wrongAnswers = [];
localStorage.removeItem('linux_sandbox_wrong');
alert('✅ 错题记录已清空');
}
}
// 立即加载课程数据
document.addEventListener('DOMContentLoaded', loadCourseData);
</script>
</body>
</html>

582
index.html.v2.bak Normal file
View File

@@ -0,0 +1,582 @@
<!DOCTYPE html>
<html>
<head>
<title>Linux 命令沙盒练习 - 菜鸟式学习平台</title>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--sidebar-bg: #fff;
--sidebar-border: #ddd;
--main-bg: #fff;
--cmd-bg: #f0f0f0;
--cmd-border: #ccc;
--cmd-text: #333;
--success-color: #4caf50;
--error-color: #f44336;
--highlight-color: #ff9800;
}
[data-theme="dark"] {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--sidebar-bg: #2d2d2d;
--sidebar-border: #444;
--main-bg: #1e1e1e;
--cmd-bg: #2d2d2d;
--cmd-border: #444;
--cmd-text: #fff;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
/* Header */
header {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--sidebar-border);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 20px; color: var(--success-color); }
.header-actions { display: flex; gap: 10px; align-items: center; }
.theme-toggle, .logout-btn {
background: transparent;
border: 1px solid var(--sidebar-border);
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
color: var(--text-color);
}
.theme-toggle:hover, .logout-btn:hover { background: var(--cmd-bg); }
/* Layout */
.container {
display: flex;
height: calc(100vh - 50px);
}
/* Sidebar */
.sidebar {
width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
overflow-y: auto;
padding: 15px;
}
.sidebar h2 {
font-size: 16px;
margin: 10px 0 5px;
color: var(--success-color);
border-bottom: 2px solid var(--highlight-color);
padding-bottom: 5px;
}
.lesson-list {
list-style: none;
padding: 0;
margin: 0;
}
.lesson-item {
padding: 8px 12px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.lesson-item:hover { background: var(--cmd-bg); }
.lesson-item.active {
background: var(--highlight-color);
color: #fff;
}
.lesson-item.locked { opacity: 0.6; cursor: not-allowed; }
.lesson-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: #ccc;
color: #fff;
}
.lesson-item.active .lesson-badge { background: #fff; color: var(--highlight-color); }
.lesson-item.locked .lesson-badge { background: var(--highlight-color); }
/* Main Content */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.task-card {
background: var(--main-bg);
border: 1px solid var(--sidebar-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.task-title {
font-size: 22px;
color: var(--success-color);
margin: 0 0 10px;
}
.task-desc {
background: var(--cmd-bg);
padding: 15px;
border-radius: 6px;
border-left: 4px solid var(--highlight-color);
margin: 10px 0;
line-height: 1.6;
}
.task-hint {
color: var(--highlight-color);
font-weight: bold;
margin: 10px 0;
}
.task-success { display: none; background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0; border:1px solid var(--success-color); }
.task-success.show { display: block; }
.task-success h4 { margin: 0 0 5px; color: var(--success-color); }
/* Command Area */
.cmd-area {
margin-top: 20px;
}
.cmd-bar {
background: var(--cmd-bg);
border: 1px solid var(--cmd-border);
border-radius: 6px;
padding: 10px;
display: flex;
align-items: center;
}
.prompt { margin-right: 10px; color: var(--success-color); font-weight: bold; }
#cmd {
flex: 1;
background: transparent;
border: none;
color: var(--cmd-text);
font-family: 'Courier New', monospace;
font-size: 16px;
padding: 8px;
outline: none;
}
#output {
margin-top: 15px;
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 6px;
padding: 15px;
min-height: 60px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
color: var(--text-color);
}
.btn {
background: var(--success-color);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background 0.2s;
}
.btn:hover { background: #45a049; }
.btn:disabled { background: #ccc; cursor: not-allowed; }
.btn-next { background: var(--highlight-color); }
.btn-next:hover { background: #f57c00; }
/* Footer */
footer {
background: var(--sidebar-bg);
border-top: 1px solid var(--sidebar-border);
padding: 10px 20px;
text-align: right;
font-size: 12px;
color: #888;
}
.progress-bar {
display: flex;
gap: 2px;
margin: 10px 0;
}
.progress-step {
width: 30px;
height: 4px;
background: var(--cmd-bg);
border-radius: 2px;
}
.progress-step.done { background: var(--success-color); }
.progress-step.current { background: var(--highlight-color); }
</style>
</head>
<body>
<header>
<h1>💻 Linux 命令沙盒练习</h1>
<div class="header-actions">
<button class="theme-toggle" onclick="toggleTheme()" title="切换深色/浅色模式">
<span id="themeLabel">☀️</span> <span id="themeLabelText">浅色</span>
</button>
<span id="userStatus">未登录</span>
<button id="logoutBtn" class="logout-btn" onclick="logout()" style="display:none">👋 退出</button>
</div>
</header>
<div class="container">
<aside class="sidebar">
<div id="sidebarContent">
<!-- 课程目录将由 JS 动态生成 -->
<p style="text-align:center; color:#888; margin-top:20px;">加载课程中...</p>
</div>
</aside>
<main class="main-content">
<div id="taskPanel" class="task-card" style="display:none">
<h2 class="task-title" id="taskTitle"></h2>
<div class="progress-bar" id="progressBar"></div>
<p class="task-hint" id="taskHint"></p>
<div class="task-desc" id="taskDesc"></div>
<div class="task-success" id="taskSuccess">
<h4>✅ 回答正确!🎉</h4>
<p id="taskSuccessMsg"></p>
</div>
<div class="cmd-area">
<div class="cmd-bar">
<span class="prompt">$</span>
<input type="text" id="cmd" placeholder="输入 Linux 命令..." autocomplete="off" autofocus>
</div>
<div id="output">等待输入命令...</div>
<button id="submitBtn" class="btn" onclick="runCommand()">执行命令</button>
<button id="nextBtn" class="btn btn-next" onclick="nextLevel()" style="display:none">下一关 →</button>
</div>
</div>
<div id="welcomePanel" class="task-card" style="text-align:center; padding:40px 20px;">
<h2>欢迎来到 Linux 命令沙盒练习平台!</h2>
<p style="font-size:18px; margin:20px 0;">这是一个零风险的 Linux 命令学习环境,你可以安全地练习常用命令。</p>
<p><strong>📌 使用说明:</strong></p>
<ul style="text-align:left; display:inline-block;">
<li>左侧是课程目录,按顺序学习</li>
<li>阅读任务描述,输入对应命令</li>
<li>答对后自动解锁下一关</li>
<li>支持深色/浅色主题切换</li>
</ul>
<button class="btn" style="margin-top:30px;" onclick="startFirstLesson()">开始学习 →</button>
</div>
</main>
</div>
<footer>
<span id="userBadge">🔒 未认证</span> |
<span id="currentLevel">Lv.1</span> |
<a href="https://github.com/" target="_blank">Powered by Linux Sandbox</a>
</footer>
<script>
// ==============================
// 全局状态
// ==============================
let currentToken = localStorage.getItem('linux_sandbox_token') || '';
let currentUser = localStorage.getItem('linux_sandbox_user') || '';
let currentTheme = localStorage.getItem('linux_sandbox_theme') || 'light';
let currentTaskId = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_sandbox_completed') || '[]');
let COURSE_DATA = null; // 将从 /api/tasks 动态加载
// ==============================
// 主题切换
// ==============================
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('linux_sandbox_theme', theme);
document.getElementById('themeLabel').textContent = theme === 'dark' ? '🌙' : '☀️';
document.getElementById('themeLabelText').textContent = theme === 'dark' ? '深色' : '浅色';
}
function toggleTheme() {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
currentTheme = newTheme;
}
applyTheme(currentTheme);
// ==============================
// 登录/登出
// ==============================
async function login() {
// 建议:实际从登录弹窗调用,这里简化直接用 token
if (currentToken && currentUser) {
updateUI();
return;
}
alert('请先登录!当前简化版支持直接使用内置 Token 执行命令。');
// 临时允许
currentToken = 'safe_linux_2026';
currentUser = 'sandbox_user';
localStorage.setItem('linux_sandbox_token', currentToken);
localStorage.setItem('linux_sandbox_user', currentUser);
logoutBtn.style.display = 'inline-block';
updateUI();
}
function logout() {
localStorage.removeItem('linux_sandbox_token');
localStorage.removeItem('linux_sandbox_user');
currentToken = '';
currentUser = '';
logoutBtn.style.display = 'none';
updateUI();
}
function updateUI() {
const userStatus = document.getElementById('userStatus');
const userBadge = document.getElementById('userBadge');
if (currentToken && currentUser) {
userStatus.textContent = `👤 ${currentUser}`;
userBadge.textContent = '✅ 已认证';
document.getElementById('logoutBtn').style.display = 'inline-block';
} else {
userStatus.textContent = '未登录';
userBadge.textContent = '🔒 未认证';
}
}
// ==============================
// 课程渲染
// ==============================
function renderSidebar() {
const sidebar = document.getElementById('sidebarContent');
let html = '<ul class="lesson-list">';
COURSE_DATA.levels.forEach(level => {
html += `<h2>${level.title}</h2>`;
level.challenges.forEach((task, idx) => {
const done = completedTasks.includes(task.id);
const active = task.id === currentTaskId ? 'active' : '';
const locked = !done && idx > 0 && !completedTasks.includes(level.challenges[idx-1].id);
html += `<li class="lesson-item ${active} ${locked ? 'locked' : ''}" onclick="loadTask('${task.id}')">
<span>${idx+1}. ${task.title}</span>
<span class="lesson-badge">${done ? '✅' : '🔒'}</span>
</li>`;
});
});
html += '</ul>';
sidebar.innerHTML = html;
}
// ==============================
// 任务加载
// ==============================
function loadTask(taskId) {
// 检查前序任务是否完成
let prevTaskId = null;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (task.id === taskId) {
if (prevTaskId && !completedTasks.includes(prevTaskId)) {
alert('请先完成前一关!');
return;
}
currentTaskId = taskId;
renderTask(task);
renderSidebar(); // 更新高亮
return;
}
prevTaskId = task.id;
}
}
alert('任务未找到');
}
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('taskTitle').innerHTML = task.title;
document.getElementById('taskDesc').innerHTML = task.desc;
document.getElementById('taskHint').textContent = `💡 ${task.hint}`;
document.getElementById('taskSuccess').classList.remove('show');
document.getElementById('taskSuccessMsg').textContent = task.success_msg || '恭喜你,任务完成!';
document.getElementById('submitBtn').style.display = 'inline-block';
document.getElementById('nextBtn').style.display = 'none';
// 渲染进度条
const progressBar = document.getElementById('progressBar');
let progressHtml = '';
for (let i=0; i<5; i++) {
progressHtml += `<div class="progress-step ${i === 0 ? 'current' : ''} ${i < 0 ? 'done' : ''}"></div>`;
}
progressBar.innerHTML = progressHtml;
// 聚焦输入框
document.getElementById('cmd').value = '';
document.getElementById('cmd').focus();
}
function startFirstLesson() {
if (!currentToken) {
login();
}
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
loadTask(COURSE_DATA.levels[0].challenges[0].id);
}
// ==============================
// 命令执行
// ==============================
async function runCommand() {
const cmd = document.getElementById('cmd').value.trim();
if (!cmd) return;
const output = document.getElementById('output');
output.textContent = `执行: ${cmd}\n正在运行...`;
document.getElementById('submitBtn').disabled = true;
document.getElementById('cmd').disabled = true;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
output.textContent = data.output || data.message || '(无输出)';
// 检查任务是否完成
checkTaskCompletion(cmd, data.output || '');
} catch (e) {
output.textContent = `❌ 错误: ${e.message}`;
} finally {
document.getElementById('submitBtn').disabled = false;
document.getElementById('cmd').disabled = false;
}
}
function checkTaskCompletion(cmd, output) {
const task = findTaskById(currentTaskId);
if (!task) return;
// 检查 solution 匹配
const solutionMatch = task.success_test || (task.solution && task.solution.includes(cmd));
if (solutionMatch || task.solution?.includes(cmd)) {
handleTaskSuccess(task);
} else {
// 简单启发式检查(未实现 full success_test 解析)
console.log('需要完成任务检查逻辑');
}
}
function handleTaskSuccess(task) {
if (!completedTasks.includes(task.id)) {
completedTasks.push(task.id);
localStorage.setItem('linux_sandbox_completed', JSON.stringify(completedTasks));
alert('✅ 回答正确!\n奖励经验值 +100');
}
document.getElementById('submitBtn').style.display = 'none';
const successBox = document.getElementById('taskSuccess');
successBox.querySelector('h4').innerHTML = '🎉 回答正确!';
successBox.querySelector('p').textContent = task.success_msg || '你已经掌握这个命令的基本用法!';
successBox.classList.add('show');
document.getElementById('nextBtn').style.display = 'inline-block';
}
function nextLevel() {
const task = findTaskById(currentTaskId);
if (!task) return;
// 找到下一关
let found = false;
for (const level of COURSE_DATA.levels) {
for (const t of level.challenges) {
if (found) {
loadTask(t.id);
return;
}
if (t.id === task.id) found = true;
}
}
alert('🎉 所有课程已完成!你已经是 Linux 大师了!🏆');
}
function findTaskById(id) {
for (const level of COURSE_DATA.levels) {
const task = level.challenges.find(t => t.id === id);
if (task) return task;
}
return null;
}
// ==============================
// 事件监听
// ==============================
document.getElementById('cmd').addEventListener('keypress', function(e) {
if (e.key === 'Enter') runCommand();
});
// ==============================
// 初始化
// ==============================
function init() {
updateUI();
// 渲染侧边栏(课程数据加载后调用 renderSidebar
if (COURSE_DATA) renderSidebar();
// 检查是否已有学习进度
if (currentToken && !currentTaskId && !completedTasks.length) {
// 从第一关开始(延迟加载)
setTimeout(startFirstLesson, 1000);
} else if (currentTaskId) {
const task = findTaskById(currentTaskId);
if (task) renderTask(task);
}
}
// 等待课程数据加载完成后初始化 UI
let dataLoaded = false;
setInterval(() => {
if (COURSE_DATA && !dataLoaded) {
init();
dataLoaded = true;
}
}, 500);
// ==============================
// 加载课程数据API
// ==============================
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功', COURSE_DATA);
renderSidebar();
} else {
console.error('❌ 课程数据加载失败:', res.statusText);
}
} catch (e) {
console.error('❌ 课程数据获取异常:', e);
}
}
// 页面全局函数(供 onclick 调用)
window.loadTask = loadTask;
window.runCommand = runCommand;
window.nextLevel = nextLevel;
window.startFirstLesson = startFirstLesson;
// 立即加载课程数据
document.addEventListener('DOMContentLoaded', loadCourseData);
</script>
</body>
</html>

582
index.html.v3.bak Normal file
View File

@@ -0,0 +1,582 @@
<!DOCTYPE html>
<html>
<head>
<title>Linux 命令沙盒练习 - 菜鸟式学习平台</title>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--sidebar-bg: #fff;
--sidebar-border: #ddd;
--main-bg: #fff;
--cmd-bg: #f0f0f0;
--cmd-border: #ccc;
--cmd-text: #333;
--success-color: #4caf50;
--error-color: #f44336;
--highlight-color: #ff9800;
}
[data-theme="dark"] {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--sidebar-bg: #2d2d2d;
--sidebar-border: #444;
--main-bg: #1e1e1e;
--cmd-bg: #2d2d2d;
--cmd-border: #444;
--cmd-text: #fff;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
/* Header */
header {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--sidebar-border);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 20px; color: var(--success-color); }
.header-actions { display: flex; gap: 10px; align-items: center; }
.theme-toggle, .logout-btn {
background: transparent;
border: 1px solid var(--sidebar-border);
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
color: var(--text-color);
}
.theme-toggle:hover, .logout-btn:hover { background: var(--cmd-bg); }
/* Layout */
.container {
display: flex;
height: calc(100vh - 50px);
}
/* Sidebar */
.sidebar {
width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
overflow-y: auto;
padding: 15px;
}
.sidebar h2 {
font-size: 16px;
margin: 10px 0 5px;
color: var(--success-color);
border-bottom: 2px solid var(--highlight-color);
padding-bottom: 5px;
}
.lesson-list {
list-style: none;
padding: 0;
margin: 0;
}
.lesson-item {
padding: 8px 12px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.lesson-item:hover { background: var(--cmd-bg); }
.lesson-item.active {
background: var(--highlight-color);
color: #fff;
}
.lesson-item.locked { opacity: 0.6; cursor: not-allowed; }
.lesson-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: #ccc;
color: #fff;
}
.lesson-item.active .lesson-badge { background: #fff; color: var(--highlight-color); }
.lesson-item.locked .lesson-badge { background: var(--highlight-color); }
/* Main Content */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.task-card {
background: var(--main-bg);
border: 1px solid var(--sidebar-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.task-title {
font-size: 22px;
color: var(--success-color);
margin: 0 0 10px;
}
.task-desc {
background: var(--cmd-bg);
padding: 15px;
border-radius: 6px;
border-left: 4px solid var(--highlight-color);
margin: 10px 0;
line-height: 1.6;
}
.task-hint {
color: var(--highlight-color);
font-weight: bold;
margin: 10px 0;
}
.task-success { display: none; background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0; border:1px solid var(--success-color); }
.task-success.show { display: block; }
.task-success h4 { margin: 0 0 5px; color: var(--success-color); }
/* Command Area */
.cmd-area {
margin-top: 20px;
}
.cmd-bar {
background: var(--cmd-bg);
border: 1px solid var(--cmd-border);
border-radius: 6px;
padding: 10px;
display: flex;
align-items: center;
}
.prompt { margin-right: 10px; color: var(--success-color); font-weight: bold; }
#cmd {
flex: 1;
background: transparent;
border: none;
color: var(--cmd-text);
font-family: 'Courier New', monospace;
font-size: 16px;
padding: 8px;
outline: none;
}
#output {
margin-top: 15px;
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 6px;
padding: 15px;
min-height: 60px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
color: var(--text-color);
}
.btn {
background: var(--success-color);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background 0.2s;
}
.btn:hover { background: #45a049; }
.btn:disabled { background: #ccc; cursor: not-allowed; }
.btn-next { background: var(--highlight-color); }
.btn-next:hover { background: #f57c00; }
/* Footer */
footer {
background: var(--sidebar-bg);
border-top: 1px solid var(--sidebar-border);
padding: 10px 20px;
text-align: right;
font-size: 12px;
color: #888;
}
.progress-bar {
display: flex;
gap: 2px;
margin: 10px 0;
}
.progress-step {
width: 30px;
height: 4px;
background: var(--cmd-bg);
border-radius: 2px;
}
.progress-step.done { background: var(--success-color); }
.progress-step.current { background: var(--highlight-color); }
</style>
</head>
<body>
<header>
<h1>💻 Linux 命令沙盒练习</h1>
<div class="header-actions">
<button class="theme-toggle" onclick="toggleTheme()" title="切换深色/浅色模式">
<span id="themeLabel">☀️</span> <span id="themeLabelText">浅色</span>
</button>
<span id="userStatus">未登录</span>
<button id="logoutBtn" class="logout-btn" onclick="logout()" style="display:none">👋 退出</button>
</div>
</header>
<div class="container">
<aside class="sidebar">
<div id="sidebarContent">
<!-- 课程目录将由 JS 动态生成 -->
<p style="text-align:center; color:#888; margin-top:20px;">加载课程中...</p>
</div>
</aside>
<main class="main-content">
<div id="taskPanel" class="task-card" style="display:none">
<h2 class="task-title" id="taskTitle"></h2>
<div class="progress-bar" id="progressBar"></div>
<p class="task-hint" id="taskHint"></p>
<div class="task-desc" id="taskDesc"></div>
<div class="task-success" id="taskSuccess">
<h4>✅ 回答正确!🎉</h4>
<p id="taskSuccessMsg"></p>
</div>
<div class="cmd-area">
<div class="cmd-bar">
<span class="prompt">$</span>
<input type="text" id="cmd" placeholder="输入 Linux 命令..." autocomplete="off" autofocus>
</div>
<div id="output">等待输入命令...</div>
<button id="submitBtn" class="btn" onclick="runCommand()">执行命令</button>
<button id="nextBtn" class="btn btn-next" onclick="nextLevel()" style="display:none">下一关 →</button>
</div>
</div>
<div id="welcomePanel" class="task-card" style="text-align:center; padding:40px 20px;">
<h2>欢迎来到 Linux 命令沙盒练习平台!</h2>
<p style="font-size:18px; margin:20px 0;">这是一个零风险的 Linux 命令学习环境,你可以安全地练习常用命令。</p>
<p><strong>📌 使用说明:</strong></p>
<ul style="text-align:left; display:inline-block;">
<li>左侧是课程目录,按顺序学习</li>
<li>阅读任务描述,输入对应命令</li>
<li>答对后自动解锁下一关</li>
<li>支持深色/浅色主题切换</li>
</ul>
<button class="btn" style="margin-top:30px;" onclick="startFirstLesson()">开始学习 →</button>
</div>
</main>
</div>
<footer>
<span id="userBadge">🔒 未认证</span> |
<span id="currentLevel">Lv.1</span> |
<a href="https://github.com/" target="_blank">Powered by Linux Sandbox</a>
</footer>
<script>
// ==============================
// 全局状态
// ==============================
let currentToken = localStorage.getItem('linux_sandbox_token') || '';
let currentUser = localStorage.getItem('linux_sandbox_user') || '';
let currentTheme = localStorage.getItem('linux_sandbox_theme') || 'light';
let currentTaskId = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_sandbox_completed') || '[]');
let COURSE_DATA = null; // 将从 /api/tasks 动态加载
// ==============================
// 主题切换
// ==============================
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('linux_sandbox_theme', theme);
document.getElementById('themeLabel').textContent = theme === 'dark' ? '🌙' : '☀️';
document.getElementById('themeLabelText').textContent = theme === 'dark' ? '深色' : '浅色';
}
function toggleTheme() {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
currentTheme = newTheme;
}
applyTheme(currentTheme);
// ==============================
// 登录/登出
// ==============================
async function login() {
// 建议:实际从登录弹窗调用,这里简化直接用 token
if (currentToken && currentUser) {
updateUI();
return;
}
alert('请先登录!当前简化版支持直接使用内置 Token 执行命令。');
// 临时允许
currentToken = 'safe_linux_2026';
currentUser = 'sandbox_user';
localStorage.setItem('linux_sandbox_token', currentToken);
localStorage.setItem('linux_sandbox_user', currentUser);
logoutBtn.style.display = 'inline-block';
updateUI();
}
function logout() {
localStorage.removeItem('linux_sandbox_token');
localStorage.removeItem('linux_sandbox_user');
currentToken = '';
currentUser = '';
logoutBtn.style.display = 'none';
updateUI();
}
function updateUI() {
const userStatus = document.getElementById('userStatus');
const userBadge = document.getElementById('userBadge');
if (currentToken && currentUser) {
userStatus.textContent = `👤 ${currentUser}`;
userBadge.textContent = '✅ 已认证';
document.getElementById('logoutBtn').style.display = 'inline-block';
} else {
userStatus.textContent = '未登录';
userBadge.textContent = '🔒 未认证';
}
}
// ==============================
// 课程渲染
// ==============================
function renderSidebar() {
const sidebar = document.getElementById('sidebarContent');
let html = '<ul class="lesson-list">';
COURSE_DATA.levels.forEach(level => {
html += `<h2>${level.title}</h2>`;
level.challenges.forEach((task, idx) => {
const done = completedTasks.includes(task.id);
const active = task.id === currentTaskId ? 'active' : '';
const locked = !done && idx > 0 && !completedTasks.includes(level.challenges[idx-1].id);
html += `<li class="lesson-item ${active} ${locked ? 'locked' : ''}" onclick="loadTask('${task.id}')">
<span>${idx+1}. ${task.title}</span>
<span class="lesson-badge">${done ? '✅' : '🔒'}</span>
</li>`;
});
});
html += '</ul>';
sidebar.innerHTML = html;
}
// ==============================
// 任务加载
// ==============================
function loadTask(taskId) {
// 检查前序任务是否完成
let prevTaskId = null;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (task.id === taskId) {
if (prevTaskId && !completedTasks.includes(prevTaskId)) {
alert('请先完成前一关!');
return;
}
currentTaskId = taskId;
renderTask(task);
renderSidebar(); // 更新高亮
return;
}
prevTaskId = task.id;
}
}
alert('任务未找到');
}
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('taskTitle').innerHTML = task.title;
document.getElementById('taskDesc').innerHTML = task.desc;
document.getElementById('taskHint').textContent = `💡 ${task.hint}`;
document.getElementById('taskSuccess').classList.remove('show');
document.getElementById('taskSuccessMsg').textContent = task.success_msg || '恭喜你,任务完成!';
document.getElementById('submitBtn').style.display = 'inline-block';
document.getElementById('nextBtn').style.display = 'none';
// 渲染进度条
const progressBar = document.getElementById('progressBar');
let progressHtml = '';
for (let i=0; i<5; i++) {
progressHtml += `<div class="progress-step ${i === 0 ? 'current' : ''} ${i < 0 ? 'done' : ''}"></div>`;
}
progressBar.innerHTML = progressHtml;
// 聚焦输入框
document.getElementById('cmd').value = '';
document.getElementById('cmd').focus();
}
function startFirstLesson() {
if (!currentToken) {
login();
}
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
loadTask(COURSE_DATA.levels[0].challenges[0].id);
}
// ==============================
// 命令执行
// ==============================
async function runCommand() {
const cmd = document.getElementById('cmd').value.trim();
if (!cmd) return;
const output = document.getElementById('output');
output.textContent = `执行: ${cmd}\n正在运行...`;
document.getElementById('submitBtn').disabled = true;
document.getElementById('cmd').disabled = true;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
output.textContent = data.output || data.message || '(无输出)';
// 检查任务是否完成
checkTaskCompletion(cmd, data.output || '');
} catch (e) {
output.textContent = `❌ 错误: ${e.message}`;
} finally {
document.getElementById('submitBtn').disabled = false;
document.getElementById('cmd').disabled = false;
}
}
function checkTaskCompletion(cmd, output) {
const task = findTaskById(currentTaskId);
if (!task) return;
// 检查 solution 匹配
const solutionMatch = task.success_test || (task.solution && task.solution.includes(cmd));
if (solutionMatch || task.solution?.includes(cmd)) {
handleTaskSuccess(task);
} else {
// 简单启发式检查(未实现 full success_test 解析)
console.log('需要完成任务检查逻辑');
}
}
function handleTaskSuccess(task) {
if (!completedTasks.includes(task.id)) {
completedTasks.push(task.id);
localStorage.setItem('linux_sandbox_completed', JSON.stringify(completedTasks));
alert('✅ 回答正确!\n奖励经验值 +100');
}
document.getElementById('submitBtn').style.display = 'none';
const successBox = document.getElementById('taskSuccess');
successBox.querySelector('h4').innerHTML = '🎉 回答正确!';
successBox.querySelector('p').textContent = task.success_msg || '你已经掌握这个命令的基本用法!';
successBox.classList.add('show');
document.getElementById('nextBtn').style.display = 'inline-block';
}
function nextLevel() {
const task = findTaskById(currentTaskId);
if (!task) return;
// 找到下一关
let found = false;
for (const level of COURSE_DATA.levels) {
for (const t of level.challenges) {
if (found) {
loadTask(t.id);
return;
}
if (t.id === task.id) found = true;
}
}
alert('🎉 所有课程已完成!你已经是 Linux 大师了!🏆');
}
function findTaskById(id) {
for (const level of COURSE_DATA.levels) {
const task = level.challenges.find(t => t.id === id);
if (task) return task;
}
return null;
}
// ==============================
// 事件监听
// ==============================
document.getElementById('cmd').addEventListener('keypress', function(e) {
if (e.key === 'Enter') runCommand();
});
// ==============================
// 初始化
// ==============================
function init() {
updateUI();
// 渲染侧边栏(课程数据加载后调用 renderSidebar
if (COURSE_DATA) renderSidebar();
// 检查是否已有学习进度
if (currentToken && !currentTaskId && !completedTasks.length) {
// 从第一关开始(延迟加载)
setTimeout(startFirstLesson, 1000);
} else if (currentTaskId) {
const task = findTaskById(currentTaskId);
if (task) renderTask(task);
}
}
// 等待课程数据加载完成后初始化 UI
let dataLoaded = false;
setInterval(() => {
if (COURSE_DATA && !dataLoaded) {
init();
dataLoaded = true;
}
}, 500);
// ==============================
// 加载课程数据API
// ==============================
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功', COURSE_DATA);
renderSidebar();
} else {
console.error('❌ 课程数据加载失败:', res.statusText);
}
} catch (e) {
console.error('❌ 课程数据获取异常:', e);
}
}
// 页面全局函数(供 onclick 调用)
window.loadTask = loadTask;
window.runCommand = runCommand;
window.nextLevel = nextLevel;
window.startFirstLesson = startFirstLesson;
// 立即加载课程数据
document.addEventListener('DOMContentLoaded', loadCourseData);
</script>
</body>
</html>

926
index_new.html Normal file
View File

@@ -0,0 +1,926 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linux 命令学习平台 - 从入门到精通</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #4CAF50;
--primary-dark: #388E3C;
--secondary: #2196F3;
--accent: #FF9800;
--bg: #f5f7fa;
--card-bg: #ffffff;
--text: #333333;
--text-light: #666666;
--border: #e0e0e0;
--success: #4CAF50;
--error: #f44336;
--warning: #ff9800;
}
[data-theme="dark"] {
--bg: #1a1a2e;
--card-bg: #16213e;
--text: #eaeaea;
--text-light: #a0a0a0;
--border: #0f3460;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
/* Header */
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
}
.header-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.mode-toggle {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
}
.mode-toggle:hover {
background: rgba(255,255,255,0.3);
}
.mode-toggle.active {
background: white;
color: var(--primary);
}
/* Main Layout */
.container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 280px 1fr 350px;
gap: 1.5rem;
padding: 1.5rem;
min-height: calc(100vh - 70px);
}
/* Sidebar */
.sidebar {
background: var(--card-bg);
border-radius: 12px;
padding: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
height: fit-content;
}
.sidebar-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--primary);
}
.level-section {
margin-bottom: 1rem;
}
.level-header {
font-weight: 600;
color: var(--primary);
padding: 0.5rem;
background: rgba(76,175,80,0.1);
border-radius: 6px;
margin-bottom: 0.5rem;
cursor: pointer;
}
.task-list {
list-style: none;
}
.task-item {
padding: 0.6rem 0.8rem;
margin: 0.3rem 0;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
font-size: 0.9rem;
}
.task-item:hover {
background: var(--bg);
}
.task-item.active {
background: var(--primary);
color: white;
}
.task-item.completed {
opacity: 0.7;
}
.task-status {
font-size: 1rem;
}
/* Main Content */
.main-content {
background: var(--card-bg);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.welcome-panel {
text-align: center;
padding: 3rem 2rem;
}
.welcome-title {
font-size: 2rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.welcome-desc {
color: var(--text-light);
font-size: 1.1rem;
margin-bottom: 2rem;
}
.start-btn {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
border: none;
padding: 1rem 3rem;
font-size: 1.1rem;
border-radius: 30px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(76,175,80,0.4);
}
/* Task Panel */
.task-panel {
display: none;
}
.task-header {
margin-bottom: 1.5rem;
}
.task-title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.task-meta {
display: flex;
gap: 1rem;
color: var(--text-light);
font-size: 0.9rem;
}
.task-description {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 1.5rem;
border-radius: 10px;
margin-bottom: 1.5rem;
border-left: 4px solid var(--primary);
}
.task-description h3 {
color: var(--primary);
margin-bottom: 0.8rem;
}
.task-description code {
background: rgba(76,175,80,0.1);
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: var(--primary-dark);
font-family: 'Consolas', monospace;
}
/* Command Input Area */
.command-area {
background: #1e1e1e;
border-radius: 10px;
padding: 1rem;
margin-bottom: 1rem;
}
.command-input-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.prompt {
color: #4CAF50;
font-family: 'Consolas', monospace;
font-weight: bold;
}
.command-input {
flex: 1;
background: transparent;
border: none;
color: #fff;
font-family: 'Consolas', monospace;
font-size: 1rem;
outline: none;
}
.command-output {
background: #2d2d2d;
padding: 1rem;
border-radius: 6px;
min-height: 100px;
max-height: 300px;
overflow-y: auto;
font-family: 'Consolas', monospace;
color: #ddd;
white-space: pre-wrap;
margin-top: 0.5rem;
}
.action-buttons {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.btn {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--border);
color: var(--text);
}
.btn-hint {
background: var(--accent);
color: white;
}
/* Learning Panel (Right Sidebar) */
.learning-panel {
background: var(--card-bg);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
height: fit-content;
}
.panel-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.panel-title::before {
content: "📚";
}
.command-detail {
display: none;
}
.command-detail.active {
display: block;
}
.cmd-name {
font-size: 1.3rem;
font-weight: bold;
color: var(--primary);
margin-bottom: 0.5rem;
font-family: 'Consolas', monospace;
}
.cmd-desc {
color: var(--text-light);
margin-bottom: 1rem;
line-height: 1.8;
}
.cmd-section {
margin-bottom: 1.2rem;
}
.cmd-section-title {
font-weight: 600;
color: var(--secondary);
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.param-list {
list-style: none;
}
.param-item {
padding: 0.5rem;
background: var(--bg);
border-radius: 6px;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.param-name {
font-family: 'Consolas', monospace;
color: var(--accent);
font-weight: 600;
}
.example-box {
background: #f8f9fa;
border-left: 3px solid var(--secondary);
padding: 0.8rem;
border-radius: 0 6px 6px 0;
margin: 0.5rem 0;
}
.example-box code {
font-family: 'Consolas', monospace;
color: var(--primary-dark);
}
/* Success Panel */
.success-panel {
display: none;
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border: 1px solid var(--success);
border-radius: 10px;
padding: 1.5rem;
margin-top: 1rem;
}
.success-panel.show {
display: block;
}
.success-title {
color: var(--success);
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.deep-learning-btn {
background: var(--secondary);
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 6px;
cursor: pointer;
margin-top: 1rem;
}
/* Progress Bar */
.progress-section {
margin-bottom: 1.5rem;
}
.progress-bar {
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--secondary));
transition: width 0.3s;
}
.progress-text {
text-align: center;
margin-top: 0.5rem;
color: var(--text-light);
font-size: 0.9rem;
}
/* Responsive */
@media (max-width: 1200px) {
.container {
grid-template-columns: 260px 1fr;
}
.learning-panel {
display: none;
}
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo">🐧 Linux 学习平台</div>
<div class="header-actions">
<button class="mode-toggle active" id="learnMode" onclick="setMode('learn')">📖 学习模式</button>
<button class="mode-toggle" id="practiceMode" onclick="setMode('practice')">✏️ 练习模式</button>
<button class="mode-toggle" onclick="toggleTheme()">🌓</button>
</div>
</div>
</header>
<div class="container">
<!-- Left Sidebar: Course Navigation -->
<aside class="sidebar">
<div class="sidebar-title">📚 课程目录</div>
<div class="progress-section">
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
</div>
<div class="progress-text" id="progressText">0 / 95 完成</div>
</div>
<div id="courseNav"></div>
</aside>
<!-- Main Content: Task Area -->
<main class="main-content">
<!-- Welcome Panel -->
<div class="welcome-panel" id="welcomePanel">
<h1 class="welcome-title">🚀 开启 Linux 学习之旅</h1>
<p class="welcome-desc">
从入门到精通,系统学习 Linux 运维技能<br>
<strong>12 个级别 · 95 道题目 · 循序渐进</strong>
</p>
<button class="start-btn" onclick="startLearning()">开始学习</button>
</div>
<!-- Task Panel -->
<div class="task-panel" id="taskPanel">
<div class="task-header">
<h2 class="task-title" id="taskTitle">任务标题</h2>
<div class="task-meta">
<span id="taskLevel">Level 1</span>
<span>·</span>
<span id="taskNumber">1 / 95</span>
</div>
</div>
<div class="task-description" id="taskDescription">
<!-- 任务描述 -->
</div>
<!-- Command Input -->
<div class="command-area">
<div class="command-input-wrapper">
<span class="prompt">$</span>
<input type="text" class="command-input" id="cmdInput"
placeholder="输入 Linux 命令..."
onkeypress="if(event.key==='Enter')executeCommand()">
</div>
<div class="command-output" id="cmdOutput">输出将显示在这里...</div>
</div>
<div class="action-buttons">
<button class="btn btn-primary" onclick="executeCommand()">▶ 执行命令</button>
<button class="btn btn-hint" onclick="showHint()">💡 提示</button>
<button class="btn btn-secondary" onclick="showAnswer()">👀 查看答案</button>
</div>
<!-- Success Panel -->
<div class="success-panel" id="successPanel">
<div class="success-title">🎉 回答正确!</div>
<p id="successMessage">恭喜你完成了这个任务!</p>
<button class="deep-learning-btn" onclick="showDeepLearning()">
📚 查看深入学习资料
</button>
<button class="btn btn-primary" onclick="nextTask()" style="margin-left: 1rem;">
下一题 →
</button>
</div>
</div>
</main>
<!-- Right Sidebar: Learning Panel -->
<aside class="learning-panel" id="learningPanel">
<div class="panel-title">命令详解</div>
<div id="commandDetail">
<p style="color: var(--text-light); text-align: center; padding: 2rem 0;">
选择一个任务开始学习<br>
每个命令都有详细讲解
</p>
</div>
</aside>
</div>
<script>
// ====================
// 全局状态
// ====================
let COURSE_DATA = null;
let currentMode = 'learn'; // 'learn' 或 'practice'
let currentTask = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_completed') || '[]');
let currentTheme = localStorage.getItem('linux_theme') || 'light';
// ====================
// 命令知识库
// ====================
const COMMAND_KNOWLEDGE = {
'pwd': {
name: 'pwd',
fullName: 'Print Working Directory',
description: '显示当前工作目录的完整路径。这是你在文件系统中的"当前位置"。',
commonParams: [
{ param: '-L', desc: '显示逻辑路径(包括符号链接)' },
{ param: '-P', desc: '显示物理路径(解析所有符号链接)' }
],
examples: [
{ cmd: 'pwd', desc: '显示当前目录' },
{ cmd: 'pwd -P', desc: '显示物理路径' }
],
tips: '当你不确定自己在哪个目录时,随时使用 pwd 确认位置。',
related: ['cd', 'ls']
},
'ls': {
name: 'ls',
fullName: 'List Directory Contents',
description: '列出目录中的文件和子目录。这是最常用的文件查看命令。',
commonParams: [
{ param: '-l', desc: '长格式显示,包含权限、所有者、大小等详细信息' },
{ param: '-a', desc: '显示所有文件,包括以点开头的隐藏文件' },
{ param: '-h', desc: '人类可读格式,文件大小显示为 K、M、G' },
{ param: '-t', desc: '按修改时间排序' },
{ param: '-r', desc: '反向排序' },
{ param: '-S', desc: '按文件大小排序' }
],
examples: [
{ cmd: 'ls', desc: '列出当前目录文件' },
{ cmd: 'ls -la', desc: '详细列出所有文件' },
{ cmd: 'ls -lh', desc: '详细列出,人类可读大小' },
{ cmd: 'ls -lt', desc: '按时间排序列出' }
],
tips: 'ls -la 是查看目录内容最常用的组合命令。',
related: ['pwd', 'cd', 'll']
},
'cd': {
name: 'cd',
fullName: 'Change Directory',
description: '切换当前工作目录。这是文件系统导航的基础命令。',
commonParams: [
{ param: '..', desc: '切换到上级目录' },
{ param: '~', desc: '切换到用户主目录' },
{ param: '-', desc: '切换到刚才所在的目录' }
],
examples: [
{ cmd: 'cd /home', desc: '切换到 /home 目录' },
{ cmd: 'cd ..', desc: '切换到上级目录' },
{ cmd: 'cd ~', desc: '切换到主目录' },
{ cmd: 'cd -', desc: '返回刚才的目录' }
],
tips: 'cd - 可以快速在两个目录之间切换。',
related: ['pwd', 'ls']
},
'cat': {
name: 'cat',
fullName: 'Concatenate',
description: '连接文件并输出内容。常用于查看小文件的内容。',
commonParams: [
{ param: '-n', desc: '显示行号' },
{ param: '-b', desc: '显示行号,但空行不编号' },
{ param: '-s', desc: '压缩多个空行为一个' }
],
examples: [
{ cmd: 'cat file.txt', desc: '显示文件内容' },
{ cmd: 'cat -n file.txt', desc: '显示文件内容并带行号' },
{ cmd: 'cat file1 file2', desc: '连接多个文件' }
],
tips: '对于大文件,建议使用 less 或 more 而不是 cat。',
related: ['less', 'more', 'head', 'tail']
}
};
// ====================
// 初始化
// ====================
document.addEventListener('DOMContentLoaded', async () => {
applyTheme(currentTheme);
await loadCourseData();
renderCourseNav();
updateProgress();
});
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功');
}
} catch (e) {
console.error('❌ 加载课程数据失败:', e);
}
}
// ====================
// 模式切换
// ====================
function setMode(mode) {
currentMode = mode;
document.getElementById('learnMode').classList.toggle('active', mode === 'learn');
document.getElementById('practiceMode').classList.toggle('active', mode === 'practice');
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() {
if (!COURSE_DATA) return;
const nav = document.getElementById('courseNav');
let html = '';
COURSE_DATA.levels.forEach((level, levelIdx) => {
html += `
<div class="level-section">
<div class="level-header" onclick="toggleLevel(${levelIdx})">
${level.title}
</div>
<ul class="task-list" id="level-${levelIdx}">
`;
level.challenges.forEach((task, taskIdx) => {
const isCompleted = completedTasks.includes(task.id);
const isActive = currentTask && currentTask.id === task.id;
html += `
<li class="task-item ${isActive ? 'active' : ''} ${isCompleted ? 'completed' : ''}"
onclick="selectTask('${level.id}', '${task.id}')">
<span class="task-status">${isCompleted ? '✅' : '○'}</span>
<span>${taskIdx + 1}. ${task.title}</span>
</li>
`;
});
html += '</ul></div>';
});
nav.innerHTML = html;
}
function toggleLevel(levelIdx) {
const list = document.getElementById(`level-${levelIdx}`);
list.style.display = list.style.display === 'none' ? 'block' : 'none';
}
function selectTask(levelId, taskId) {
if (!COURSE_DATA) return;
const level = COURSE_DATA.levels.find(l => l.id === levelId);
const task = level.challenges.find(t => t.id === taskId);
currentTask = { ...task, level: level.title, levelNum: COURSE_DATA.levels.indexOf(level) + 1 };
renderTask(currentTask);
}
// ====================
// 任务渲染
// ====================
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('successPanel').classList.remove('show');
// 基本信息
document.getElementById('taskTitle').textContent = task.title;
document.getElementById('taskLevel').textContent = task.level;
document.getElementById('taskNumber').textContent = `${task.levelNum} / 95`;
// 任务描述
let descHtml = `<h3>📝 任务描述</h3><p>${task.description}</p>`;
// 学习模式:显示更多学习资料
if (currentMode === 'learn') {
descHtml += `
<div style="margin-top: 1rem; padding: 1rem; background: rgba(33,150,243,0.1); border-radius: 8px;">
<strong>💡 学习提示:</strong> ${task.hint}
</div>
`;
}
document.getElementById('taskDescription').innerHTML = descHtml;
// 清空输出
document.getElementById('cmdOutput').textContent = '输出将显示在这里...';
document.getElementById('cmdInput').value = '';
// 更新学习面板
updateLearningPanel(task);
// 更新导航高亮
renderCourseNav();
}
function updateLearningPanel(task) {
const panel = document.getElementById('commandDetail');
// 从任务描述中提取命令
const cmdMatch = task.description.match(/<code>(\w+)<\/code>/);
const cmdName = cmdMatch ? cmdMatch[1] : null;
if (cmdName && COMMAND_KNOWLEDGE[cmdName]) {
const cmd = COMMAND_KNOWLEDGE[cmdName];
let html = `
<div class="command-detail active">
<div class="cmd-name">${cmd.name}</div>
<div style="color: var(--text-light); font-size: 0.9rem; margin-bottom: 1rem;">
${cmd.fullName}
</div>
<div class="cmd-desc">${cmd.description}</div>
<div class="cmd-section">
<div class="cmd-section-title">🔧 常用参数</div>
<ul class="param-list">
`;
cmd.commonParams.forEach(param => {
html += `
<li class="param-item">
<span class="param-name">${param.param}</span>
<span style="color: var(--text-light);"> - ${param.desc}</span>
</li>
`;
});
html += `</ul></div>
<div class="cmd-section">
<div class="cmd-section-title">📝 使用示例</div>
`;
cmd.examples.forEach(ex => {
html += `
<div class="example-box">
<code>${ex.cmd}</code>
<div style="color: var(--text-light); margin-top: 0.3rem; font-size: 0.85rem;">
${ex.desc}
</div>
</div>
`;
});
html += `
</div>
<div class="cmd-section">
<div class="cmd-section-title">💡 小贴士</div>
<p style="font-size: 0.9rem; color: var(--text-light);">${cmd.tips}</p>
</div>
</div>
`;
panel.innerHTML = html;
} else {
panel.innerHTML = `
<div class="command-detail active">
<p style="color: var(--text-light);">
本任务涉及多个命令组合使用<br>
完成任务后可查看详细解析
</p>
</div>
`;
}
}
// ====================
// 命令执行
// ====================
async function executeCommand() {
const cmd = document.getElementById('cmdInput').value.trim();
if (!cmd) return;
const outputEl = document.getElementById('cmdOutput');
outputEl.textContent = `执行: ${cmd}\n正在运行...`;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
outputEl.textContent = data.output || data.message || '(无输出)';
// 检查答案
checkAnswer(cmd, data.output || '');
} catch (e) {
outputEl.textContent = `❌ 错误: ${e.message}`;
}
}
function checkAnswer(cmd, output) {
if (!currentTask) return;
// 简化检查:命令是否包含在 solution 中
const isCorrect = currentTask.solution && currentTask.solution.some(s =>
cmd.toLowerCase().includes(s.toLowerCase())
);
if (isCorrect) {
showSuccess();
}
}
function showSuccess() {
if (!completedTasks.includes(currentTask.id)) {
completedTasks.push(currentTask.id);
localStorage.setItem('linux_completed', JSON.stringify(completedTasks));
updateProgress();
}
document.getElementById('successPanel').classList.add('show');
document.getElementById('successMessage').textContent =
currentTask.success_msg || '恭喜你完成了这个任务!';
renderCourseNav();
}
function showDeepLearning() {
// 显示深入学习资料
alert('📚 深入学习资料功能开发中...\n\n这里将显示\n- 命令原理解析\n- 实际应用场景\n- 常见错误分析\n- 相关命令对比');
}
// ====================
// 辅助功能
// ====================
function showHint() {
if (!currentTask) return;
alert('💡 提示:\n\n' + currentTask.hint);
}
function showAnswer() {
if (!currentTask) return;
const answer = currentTask.solution ? currentTask.solution[0] : '暂无答案';
if (confirm('查看答案将标记此题为"已学习",确定要继续吗?')) {
document.getElementById('cmdInput').value = answer;
showSuccess();
}
}
function nextTask() {
if (!COURSE_DATA || !currentTask) return;
// 找到当前任务的下一个
let found = false;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (found) {
selectTask(level.id, task.id);
return;
}
if (task.id === currentTask.id) found = true;
}
}
alert('🎉 恭喜!你已完成所有课程!');
}
function startLearning() {
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
selectTask(COURSE_DATA.levels[0].id, COURSE_DATA.levels[0].challenges[0].id);
}
function updateProgress() {
const total = 95;
const completed = completedTasks.length;
const percent = Math.round((completed / total) * 100);
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressText').textContent = `${completed} / ${total} 完成 (${percent}%)`;
}
// ====================
// 全局函数暴露
// ====================
window.setMode = setMode;
window.toggleTheme = toggleTheme;
window.toggleLevel = toggleLevel;
window.selectTask = selectTask;
window.executeCommand = executeCommand;
window.showHint = showHint;
window.showAnswer = showAnswer;
window.showDeepLearning = showDeepLearning;
window.nextTask = nextTask;
window.startLearning = startLearning;
</script>
</body>
</html>

17
linux-practice.service Normal file
View File

@@ -0,0 +1,17 @@
[Unit]
Description=Linux Practice Server (reverse proxied by Caddy)
After=network.target
[Service]
Type=simple
User=llm
Group=llm
WorkingDirectory=/home/llm/projects/linux-practice
ExecStart=/usr/bin/python3 /home/llm/projects/linux-practice/server.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target

581
sandbox.py Normal file
View File

@@ -0,0 +1,581 @@
#!/usr/bin/env python3
"""
Linux 命令沙盒模拟器
- 零风险:不真实执行系统命令
- 模拟文件系统:虚拟内存字典结构
- 命令白名单:只允许安全命令
- 场景匹配:检查命令是否答对
"""
import re
# ==============================
# 1. 虚拟文件系统(内存树结构)
# ==============================
SANDBOX_FS = {
"/": {
"type": "dir",
"perm": "r-x",
"children": ["users", "projects", "logs", "sandbox_tips"],
},
"/users": {
"type": "dir",
"perm": "r-x",
"children": ["alice.txt", "bob.txt", "charlie"],
},
"/users/alice.txt": {
"type": "file",
"perm": "r--",
"content": "Hi, I'm Alice. I love Linux!",
},
"/users/bob.txt": {
"type": "file",
"perm": "r--",
"content": "Hello from Bob. Learning Linux daily.",
},
"/users/charlie": {
"type": "dir",
"perm": "r-x",
"children": ["profile.md", "skills.txt"],
},
"/users/charlie/profile.md": {
"type": "file",
"perm": "r--",
"content": "# Charlie\nLoves open source and Linux commands.",
},
"/users/charlie/skills.txt": {
"type": "file",
"perm": "r--",
"content": "Linux\nPython\nGit\nDocker",
},
"/projects": {
"type": "dir",
"perm": "r-x",
"children": ["web", "backend"],
},
"/projects/web": {
"type": "dir",
"perm": "r-x",
"children": ["index.html", "style.css"],
},
"/projects/web/index.html": {
"type": "file",
"perm": "r--",
"content": "<html><body>Hello Linux Sandbox</body></html>",
},
"/projects/web/style.css": {
"type": "file",
"perm": "r--",
"content": "body { font-family: sans-serif; }",
},
"/projects/backend": {
"type": "dir",
"perm": "r-x",
"children": ["app.py", "model.py", "utils.py"],
},
"/projects/backend/app.py": {
"type": "file",
"perm": "rw-",
"content": "def main():\n print('Hello, Linux!')\n\nif __name__ == '__main__':\n main()",
},
"/projects/backend/model.py": {
"type": "file",
"perm": "rw-",
"content": "class User:\n def __init__(self, name):\n self.name = name",
},
"/projects/backend/utils.py": {
"type": "file",
"perm": "rw-",
"content": "def log(msg):\n print(f'[LOG] {msg}')",
},
"/logs": {
"type": "dir",
"perm": "r-x",
"children": ["access.log", "error.log"],
},
"/logs/access.log": {
"type": "file",
"perm": "r--",
"content": "2024-01-01 10:00 GET /index.html 200\n2024-01-01 10:01 POST /api/login 200\n2024-01-01 10:02 GET /about 404",
},
"/logs/error.log": {
"type": "file",
"perm": "r--",
"content": "2024-01-01 10:02 ERROR Page not found\n2024-01-01 10:03 WARNING High memory usage",
},
"/sandbox_tips": {
"type": "dir",
"perm": "r-x",
"children": ["welcome.txt"],
},
"/sandbox_tips/welcome.txt": {
"type": "file",
"perm": "r--",
"content": "Welcome to Linux Sandbox!\n\nYou can practice commands safely here.\nTry 'ls', 'cat', 'grep', 'find' and more!",
},
}
# ==============================
# 2. 白名单命令定义
# ==============================
# 命令 → 参数验证规则
ALLOWED_CMDS = {
"ls": {
"flags": ["-l", "-a", "-h", "-R", "-t", "-S"],
"path": True,
},
"cd": {"path": True},
"pwd": {},
"echo": {"rest": True},
"cat": {"path": True},
"head": {"flags": ["-n"], "path": True},
"tail": {"flags": ["-n"], "path": True},
"grep": {"flags": ["-i", "-r", "-n"], "pattern": True, "path": True},
"find": {
"flags": ["-type", "-name", "-size"],
"path": True,
},
"du": {"flags": ["-h", "-s"], "path": True},
"sort": {"flags": ["-r", "-n", "-h"]},
"wc": {"flags": ["-l", "-w", "-c"], "path": True},
"mkdir": {"path": True},
"touch": {"path": True},
"cp": {"path": True}, # 模拟副本
"mv": {"path": True}, # 模拟重命名
"whoami": {},
"history": {"flags": ["-n"]},
"stat": {"path": True},
}
# ==============================
# 3. 沙盒核心类
# ==============================
class LinuxSandbox:
def __init__(self):
self.cwd = "/" # 当前目录
self.history = [] # 命令历史
self.current_task = None
def check_permissions(self, cmd_name: str, args: list) -> tuple[bool, str]:
"""验证命令是否在白名单 + 参数是否合法"""
if cmd_name not in ALLOWED_CMDS:
return False, f"❌ 拒绝执行 '{cmd_name}'(不在允许列表)"
rule = ALLOWED_CMDS[cmd_name]
# 检查非法参数
for arg in args:
if arg.startswith("-"):
if "flags" in rule and arg not in rule["flags"]:
return False, f"❌ 不支持参数: {arg}"
return True, ""
def resolve_path(self, path: str) -> str:
"""解析相对/绝对路径(简化版)"""
if path.startswith("/"):
return path
if path == ".":
return self.cwd
if path == "..":
parent = "/".join(self.cwd.split("/")[:-1]) or "/"
return parent if parent else "/"
return f"{self.cwd.rstrip('/')}/{path}"
def get_fspath(self, path: str) -> dict | None:
"""从虚拟 FS 中获取节点"""
return SANDBOX_FS.get(path)
def _simulate_ls(self, args: list) -> str:
"""模拟 ls 命令"""
if not args:
# ls 默认列出当前目录
node = SANDBOX_FS.get(self.cwd)
if not node or node.get("type") != "dir":
return f"ls: {self.cwd}: Not a directory"
children = node.get("children", [])
return " ".join(children)
# 处理路径参数
path = args[0]
resolved = self.resolve_path(path)
node = self.get_fspath(resolved)
if not node:
return f"ls: cannot access '{path}': No such file or directory"
if node["type"] != "dir":
return f"ls: cannot access '{path}': Not a directory"
return " ".join(node.get("children", []))
def _simulate_cat(self, args: list) -> str:
"""模拟 cat 命令"""
if not args:
return "cat: missing operand"
path = args[0]
resolved = self.resolve_path(path)
node = self.get_fspath(resolved)
if not node:
return f"cat: {path}: No such file or directory"
if node["type"] != "file":
return f"cat: {path}: Is a directory"
return node.get("content", "")
def _simulate_cd(self, args: list) -> str:
"""模拟 cd 命令"""
if not args or args[0] == "~":
self.cwd = "/home"
return ""
path = args[0]
resolved = self.resolve_path(path)
node = self.get_fspath(resolved)
if not node or node["type"] != "dir":
return f"cd: {path}: No such file or directory"
self.cwd = resolved
return ""
def _simulate_pwd(self) -> str:
"""模拟 pwd 命令"""
return self.cwd
def _simulate_echo(self, args: list) -> str:
"""模拟 echo 命令"""
return " ".join(args)
def _simulate_grep(self, args: list) -> str:
"""模拟 grep 命令"""
# grep pattern file or grep -r pattern dir
if len(args) < 1:
return "grep: missing file operand"
pattern = ""
paths = []
# 简化解析:第一个非 flag 参数是 pattern
i = 0
while i < len(args):
if args[i] == "-r" or args[i] == "-i" or args[i].startswith("-"):
i += 1
else:
if not pattern:
pattern = args[i]
else:
paths.append(args[i])
i += 1
if not pattern or not paths:
return "grep: pattern or file missing"
# 支持通配符匹配
import re
results = []
for path_arg in paths:
# 处理 * 通配符
if "*" in path_arg:
# 简单 glob 模拟
prefix = path_arg.split("/")[0] if "/" in path_arg else self.cwd.rstrip("/")
pattern_part = path_arg.replace("*", ".*")
for key in SANDBOX_FS:
if key.startswith(prefix) and SANDBOX_FS[key]["type"] == "file":
if re.match(pattern_part, key.split("/")[-1]):
node = self.get_fspath(key)
if node and node["type"] == "file":
lines = node.get("content", "").split("\n")
for line in lines:
if pattern in line:
results.append(f"{key}:{line}")
else:
resolved = self.resolve_path(path_arg)
node = self.get_fspath(resolved)
if not node:
return f"grep: {path_arg}: No such file or directory"
if node["type"] != "file":
return f"grep: {path_arg}: Is a directory"
lines = node.get("content", "").split("\n")
for line in lines:
if pattern in line:
results.append(line)
return "\n".join(results) if results else ""
def _simulate_find(self, args: list) -> str:
"""模拟 find 命令(简化版)"""
# find path -type f -name "*.ext"
# 兼容不同顺序find /path -type f -name "*.py" or find /path -name "*.py"
path = self.cwd
file_type = None
name_pattern = None
i = 0
while i < len(args):
arg = args[i]
if arg.startswith("-"):
if arg == "-type" and i + 1 < len(args):
file_type = args[i + 1]
i += 2
elif arg == "-name" and i + 1 < len(args):
name_pattern = args[i + 1]
i += 2
else:
i += 1
else:
path = self.resolve_path(arg)
i += 1
# 如果没有指定文件类型,默认查找所有文件
if file_type is None:
file_type = "f"
# 如果没有指定 -name返回所有文件
if name_pattern is None:
name_pattern = "*"
# 匹配逻辑
results = []
for key in SANDBOX_FS:
if key.startswith(path) and SANDBOX_FS[key]["type"] == file_type:
# 简单 glob 匹配
filename = key.split("/")[-1]
if name_pattern == "*" or filename.endswith(name_pattern.replace("*", "")):
results.append(key)
return "\n".join(results) if results else ""
def _simulate_du(self, args: list) -> str:
"""模拟 du 命令"""
# du -sh * or du -h path
path = self.cwd
show_all = False
# 解析参数
i = 0
while i < len(args):
if args[i] == "-sh" or args[i] == "-h":
show_all = True
i += 1
elif args[i].startswith("-"):
i += 1
else:
path = self.resolve_path(args[i])
i += 1
node = self.get_fspath(path)
if not node or node["type"] != "dir":
return f"du: {path}: No such file or directory"
# 模拟大小KB- 简化版
size_map = {"file": 1, "dir": 4}
total = 0
children = node.get("children", [])
for child in children:
child_path = f"{path.rstrip('/')}/{child}"
child_node = self.get_fspath(child_path)
if child_node:
total += size_map.get(child_node["type"], 1)
return f"{total}K\t{path}"
def _simulate_sort(self, args: list, input_text: str) -> str:
"""模拟 sort 命令"""
lines = input_text.strip().split("\n")
reverse = "-r" in args
numeric = "-n" in args
if numeric:
try:
lines = sorted(lines, key=lambda x: float(x.split()[0]), reverse=reverse)
except:
pass
else:
lines.sort(reverse=reverse)
return "\n".join(lines)
def _simulate_wc(self, args: list, input_text: str = "") -> str:
"""模拟 wc 命令"""
lines = input_text.count("\n") + 1 if input_text else 0
words = len(input_text.split()) if input_text else 0
chars = len(input_text)
# 从文件计算
if args and args[-1] not in ["-l", "-w", "-c"]:
path = args[-1]
resolved = self.resolve_path(path)
node = self.get_fspath(resolved)
if node and node["type"] == "file":
content = node.get("content", "")
lines = content.count("\n") + 1
words = len(content.split())
chars = len(content)
flag = args[0] if args and args[0].startswith("-") else ""
if flag == "-l":
return str(lines)
elif flag == "-w":
return str(words)
elif flag == "-c":
return str(chars)
else:
return f" {lines} {words} {chars}"
def _simulate_mkdir(self, args: list) -> str:
"""模拟 mkdir -p"""
if not args:
return "mkdir: missing operand"
path = self.resolve_path(args[0])
# 简化:只允许在 sandbox 下创建
if not path.startswith("/sandbox"):
return "mkdir: permission denied: only sandbox paths allowed"
return f"mkdir: created directory '{path}' (simulated)"
def _simulate_touch(self, args: list) -> str:
"""模拟 touch"""
if not args:
return "touch: missing file operand"
path = self.resolve_path(args[0])
# 只读沙盒,不允许创建
return f"touch: cannot touch '{path}': Read-only file system"
def _simulate_cp(self, args: list) -> str:
"""模拟 cp"""
if len(args) < 2:
return "cp: missing file operand"
src = self.resolve_path(args[0])
dst = args[-1] # 简化:目标总是最后一个参数
if not self.get_fspath(src):
return f"cp: cannot stat '{args[0]}': No such file or directory"
return f"cp: copied (simulated) '{src}''{dst}'"
def _simulate_mv(self, args: list) -> str:
"""模拟 mv"""
if len(args) < 2:
return "mv: missing file operand"
return f"mv: moved (simulated) '{args[0]}''{args[1]}'"
def _simulate_stat(self, args: list) -> str:
"""模拟 stat"""
if not args:
return "stat: missing operand"
path = self.resolve_path(args[0])
node = self.get_fspath(path)
if not node:
return f"stat: cannot stat '{path}': No such file or directory"
return f""" File: {path}
Size: {len(node.get('content', ''))} Blocks: 1 IO Block: 4096 {'directory' if node['type'] == 'dir' else 'regular file'}
Access: ({node['perm']}/0{'' if node['type']=='dir' else '0' }44) uid=1000 gid=1000
Access: 2026-03-04 00:00:00.000000000
Modify: 2026-03-04 00:00:00.000000000
Change: 2026-03-04 00:00:00.000000000
Birth: -"""
# ==============================
# 主执行入口
# ==============================
def execute(self, cmd: str) -> dict:
"""执行命令,返回结果 & 社交化反馈"""
cmd = cmd.strip()
if not cmd:
return {"output": "", "success": True, "message": ""}
# 记录历史
self.history.append(cmd)
# 解析命令
parts = cmd.split()
cmd_name = parts[0]
args = parts[1:]
# 安全检查
# 1. 关键危险命令拦截
if any(x in cmd for x in ["rm -rf", "sudo", "chmod 777", "wget", "curl"]):
return {
"output": "",
"success": False,
"message": "❌ 拒绝执行危险命令!这是沙盒环境,为安全起见禁止危险指令。",
}
# 2. 白名单验证
allowed, err = self.check_permissions(cmd_name, args)
if not allowed:
return {"output": "", "success": False, "message": err}
# 执行模拟命令
output = ""
try:
if cmd_name == "ls":
output = self._simulate_ls(args)
elif cmd_name == "cd":
msg = self._simulate_cd(args)
output = msg if msg else self.cwd
elif cmd_name == "pwd":
output = self._simulate_pwd()
elif cmd_name == "echo":
output = self._simulate_echo(args)
elif cmd_name == "cat":
output = self._simulate_cat(args)
elif cmd_name == "grep":
output = self._simulate_grep(args)
elif cmd_name == "find":
output = self._simulate_find(args)
elif cmd_name == "du":
output = self._simulate_du(args)
elif cmd_name == "sort":
output = self._simulate_sort(args, "")
elif cmd_name == "wc":
output = self._simulate_wc(args)
elif cmd_name == "mkdir":
output = self._simulate_mkdir(args)
elif cmd_name == "touch":
output = self._simulate_touch(args)
elif cmd_name == "cp":
output = self._simulate_cp(args)
elif cmd_name == "mv":
output = self._simulate_mv(args)
elif cmd_name == "whoami":
output = "sandbox_user"
elif cmd_name == "history":
n = int(args[0].lstrip("-n")) if args and args[0].startswith("-n") else 5
output = "\n".join(f" {i+1} {h}" for i, h in enumerate(self.history[-n:]))
elif cmd_name == "stat":
output = self._simulate_stat(args)
else:
output = f"{cmd_name}: command not found (sandbox mode)"
except Exception as e:
output = f"Error: {e}"
return {"output": output, "success": True, "message": "✅ 命令执行成功"}
# ==============================
# 4. 单元测试(可选)
# ==============================
if __name__ == "__main__":
sb = LinuxSandbox()
print("=== Linux Sandbox Test ===")
print(sb.execute("ls"))
print(sb.execute("pwd"))
print(sb.execute("cat /users/alice.txt"))
print(sb.execute("grep Linux /users/*"))
print(sb.execute("find /projects -name '*.py'"))
print(sb.execute("du -sh /projects"))
print(sb.execute("rm -rf /")) # 应该被拦截

285
server.py Executable file
View File

@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""
Linux 命令沙盒练习平台 Server
- 集成 sandbox.py 沙盒引擎
- 路由:/ → HTML 页面, /api/run → 命令执行, /api/check → 任务检查
"""
import http.server
import urllib.parse
import json
import os
import base64
import hashlib
# ==============================
# 认证配置
# ==============================
# 预置用户:{username: password_hash}
# 生成方式hashlib.sha256(b"password").hexdigest()
USERS = {
# 默认管理员账户
"admin": hashlib.sha256(b"safe_linux_2026").hexdigest(),
# 可添加更多用户
}
# Token 有效期(秒)
TOKEN_TTL = 86400 # 24 小时
def check_auth(request_body: bytes = None) -> tuple[bool, str]:
"""
检查 Authorization 请求头
支持: Bearer token 或 Basic auth
"""
# 默认允许访问(未启用强制认证)
# 启用:true 改为 True
ENABLE_AUTH = False
if not ENABLE_AUTH:
return True, "admin"
if not request_body:
return False, "Missing Authorization header"
# 解析请求头
# 实际在 do_GET 中通过 headers 获取
return True, "admin" # 暂时 Allow-All后续优化
# 导入沙盒模块
from sandbox import LinuxSandbox
SANDBOX = LinuxSandbox()
# ==============================
# 任务数据加载
# ==============================
TASKS_FILE = os.path.join(os.path.dirname(__file__), "COURSE_TASKS.json")
def load_tasks():
"""加载课程任务"""
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 {"levels": []}
TASKS = load_tasks()
# ==============================
# Handler 类
# ==============================
class LinuxSandboxHandler(http.server.BaseHTTPRequestHandler):
def send_json(self, data, status=200):
"""发送 JSON 响应"""
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
def send_text(self, text, status=200):
"""发送纯文本响应"""
self.send_response(status)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(text.encode("utf-8"))
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
# 🔐 认证检查(跳过 / 登录页 / API 登录)
if path not in ["/", "/login.html", "/api/login", "/api/logout"]:
# 检查 Cookie 或 Authorization
auth_header = self.headers.get("Authorization", "")
token = self.headers.get("X-Token", "")
# 简化:如果带了有效 token 或直接 local 环境127.0.0.1)则放行
if not self.check_auth(auth_header, token):
# 对公网域名强制认证
if "xiaoxiaoluohao.indevs.in" in self.headers.get("Host", ""):
self.send_json({"error": "Authentication required"}, 401)
return
# 1. 主页:返回 HTML 页面
if path == "/":
html_path = os.path.join(os.path.dirname(__file__), "index.html")
try:
with open(html_path, "rb") as f:
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(f.read())
except Exception as e:
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(f"Error: {e}".encode())
# 2. 命令执行 API
elif path == "/api/run":
query = urllib.parse.parse_qs(parsed.query)
cmd = query.get("cmd", [""])[0]
if not cmd:
self.send_json({"error": "No command provided"}, 400)
return
# 执行沙盒命令
result = SANDBOX.execute(cmd)
self.send_json(result)
# 3. 任务检查 API
elif path == "/api/check":
query = urllib.parse.parse_qs(parsed.query)
task_id = query.get("task_id", [""])[0]
if not task_id:
self.send_json({"error": "-task_id required"}, 400)
return
# 查找任务
task = None
for level in TASKS.get("levels", []):
for t in level.get("challenges", []):
if t.get("id") == task_id:
task = t
break
if task:
break
if not task:
self.send_json({"error": "Task not found"}, 404)
return
# 获取当前命令执行结果
current_cmd = query.get("last_cmd", [""])[0]
current_output = query.get("output", [""])[0]
# 检查是否匹配
success = False
message = task.get("fail_message", "❌ 未通过测试")
# 尝试匹配 solution
for sol in task.get("solution", []):
if current_cmd.strip() == sol.strip():
success = True
message = "✅ 回答正确!🎉"
break
# 如果没匹配上,检查 success_test 逻辑
if not success and "success_test" in task:
if self.evaluate_test(task["success_test"], current_output):
success = True
message = "✅ 回答正确!🎉"
self.send_json({
"task_id": task_id,
"success": success,
"message": message,
"hint": task.get("hint", "💡 没有提示"),
"title": task.get("title", "未知任务"),
})
# 4. 获取课程总览
elif path == "/api/tasks":
self.send_json(TASKS)
# 404
else:
self.send_response(404)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"Not Found")
def check_auth(self, auth_header: str, token: str) -> bool:
"""认证检查:支持 Bearer token / X-Token header"""
# 本地环境直接放行
if self.client_address[0] == "127.0.0.1":
return True
# 公网场景:验证 token
if token:
# 简化版:验证预置 token生产环境应加密存储
valid_tokens = ["safe_linux_2026"]
return token in valid_tokens
# Bearer token 支持
if auth_header.startswith("Bearer "):
token = auth_header[7:]
return token in ["safe_linux_2026"]
return False
def evaluate_test(self, test_expr: str, output: str) -> bool:
"""简单评估 success_test 表达式"""
try:
# 支持简单的 if/contains 语法
# if output contains 'xxx' then pass
if "contains" in test_expr:
parts = test_expr.split("'")
if len(parts) >= 3:
keyword = parts[1]
return keyword in output
elif "==" in test_expr:
parts = test_expr.split("==")
if len(parts) == 2:
expected = parts[1].strip().strip("'")
return output.strip() == expected
return False
except Exception:
return False
def log_message(self, format, *args):
pass # Suppress logging
# ==============================
# Auth APIs
# ==============================
def do_POST(self):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
# /api/login
if path == "/api/login":
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode()
try:
data = json.loads(body)
username = data.get("username", "")
password = data.get("password", "")
# 简单验证
if username in USERS:
pwd_hash = hashlib.sha256(password.encode()).hexdigest()
if pwd_hash == USERS[username]:
# 返回成功 token
self.send_json({
"success": True,
"token": "safe_linux_2026",
"message": "✅ 登录成功!"
})
return
self.send_json({"success": False, "error": "❌ 用户名或密码错误"}, 401)
except Exception as e:
self.send_json({"success": False, "error": str(e)}, 400)
# /api/logout
elif path == "/api/logout":
self.send_json({"success": True, "message": "👋 已退出登录"})
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b"Not Found")
if __name__ == "__main__":
PORT = 8084
print(f"🌱 Linux Sandbox Practice Server with Auth 启动中... http://127.0.0.1:{PORT}")
print("📚 地址: https://linux.xiaoxiaoluohao.indevs.in")
print("🔑 Account: admin / safe_linux_2026")
http.server.HTTPServer(("127.0.0.1", PORT), LinuxSandboxHandler).serve_forever()

285
server.py.bak Executable file
View File

@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""
Linux 命令沙盒练习平台 Server
- 集成 sandbox.py 沙盒引擎
- 路由:/ → HTML 页面, /api/run → 命令执行, /api/check → 任务检查
"""
import http.server
import urllib.parse
import json
import os
import base64
import hashlib
# ==============================
# 认证配置
# ==============================
# 预置用户:{username: password_hash}
# 生成方式hashlib.sha256(b"password").hexdigest()
USERS = {
# 默认管理员账户
"admin": hashlib.sha256(b"safe_linux_2026").hexdigest(),
# 可添加更多用户
}
# Token 有效期(秒)
TOKEN_TTL = 86400 # 24 小时
def check_auth(request_body: bytes = None) -> tuple[bool, str]:
"""
检查 Authorization 请求头
支持: Bearer token 或 Basic auth
"""
# 默认允许访问(未启用强制认证)
# 启用:true 改为 True
ENABLE_AUTH = False
if not ENABLE_AUTH:
return True, "admin"
if not request_body:
return False, "Missing Authorization header"
# 解析请求头
# 实际在 do_GET 中通过 headers 获取
return True, "admin" # 暂时 Allow-All后续优化
# 导入沙盒模块
from sandbox import LinuxSandbox
SANDBOX = LinuxSandbox()
# ==============================
# 任务数据加载
# ==============================
TASKS_FILE = os.path.join(os.path.dirname(__file__), "COURSE_TASKS.json")
def load_tasks():
"""加载课程任务"""
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 {"levels": []}
TASKS = load_tasks()
# ==============================
# Handler 类
# ==============================
class LinuxSandboxHandler(http.server.BaseHTTPRequestHandler):
def send_json(self, data, status=200):
"""发送 JSON 响应"""
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
def send_text(self, text, status=200):
"""发送纯文本响应"""
self.send_response(status)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(text.encode("utf-8"))
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
# 🔐 认证检查(跳过 / 登录页 / API 登录)
if path not in ["/", "/login.html", "/api/login", "/api/logout"]:
# 检查 Cookie 或 Authorization
auth_header = self.headers.get("Authorization", "")
token = self.headers.get("X-Token", "")
# 简化:如果带了有效 token 或直接 local 环境127.0.0.1)则放行
if not self.check_auth(auth_header, token):
# 对公网域名强制认证
if "xiaoxiaoluohao.indevs.in" in self.headers.get("Host", ""):
self.send_json({"error": "Authentication required"}, 401)
return
# 1. 主页:返回 HTML 页面
if path == "/":
html_path = os.path.join(os.path.dirname(__file__), "index.html")
try:
with open(html_path, "rb") as f:
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(f.read())
except Exception as e:
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(f"Error: {e}".encode())
# 2. 命令执行 API
elif path == "/api/run":
query = urllib.parse.parse_qs(parsed.query)
cmd = query.get("cmd", [""])[0]
if not cmd:
self.send_json({"error": "No command provided"}, 400)
return
# 执行沙盒命令
result = SANDBOX.execute(cmd)
self.send_json(result)
# 3. 任务检查 API
elif path == "/api/check":
query = urllib.parse.parse_qs(parsed.query)
task_id = query.get("task_id", [""])[0]
if not task_id:
self.send_json({"error": "-task_id required"}, 400)
return
# 查找任务
task = None
for level in TASKS.get("levels", []):
for t in level.get("challenges", []):
if t.get("id") == task_id:
task = t
break
if task:
break
if not task:
self.send_json({"error": "Task not found"}, 404)
return
# 获取当前命令执行结果
current_cmd = query.get("last_cmd", [""])[0]
current_output = query.get("output", [""])[0]
# 检查是否匹配
success = False
message = task.get("fail_message", "❌ 未通过测试")
# 尝试匹配 solution
for sol in task.get("solution", []):
if current_cmd.strip() == sol.strip():
success = True
message = "✅ 回答正确!🎉"
break
# 如果没匹配上,检查 success_test 逻辑
if not success and "success_test" in task:
if self.evaluate_test(task["success_test"], current_output):
success = True
message = "✅ 回答正确!🎉"
self.send_json({
"task_id": task_id,
"success": success,
"message": message,
"hint": task.get("hint", "💡 没有提示"),
"title": task.get("title", "未知任务"),
})
# 4. 获取课程总览
elif path == "/api/tasks":
self.send_json(TASKS)
# 404
else:
self.send_response(404)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"Not Found")
def check_auth(self, auth_header: str, token: str) -> bool:
"""认证检查:支持 Bearer token / X-Token header"""
# 本地环境直接放行
if self.client_address[0] == "127.0.0.1":
return True
# 公网场景:验证 token
if token:
# 简化版:验证预置 token生产环境应加密存储
valid_tokens = ["safe_linux_2026"]
return token in valid_tokens
# Bearer token 支持
if auth_header.startswith("Bearer "):
token = auth_header[7:]
return token in ["safe_linux_2026"]
return False
def evaluate_test(self, test_expr: str, output: str) -> bool:
"""简单评估 success_test 表达式"""
try:
# 支持简单的 if/contains 语法
# if output contains 'xxx' then pass
if "contains" in test_expr:
parts = test_expr.split("'")
if len(parts) >= 3:
keyword = parts[1]
return keyword in output
elif "==" in test_expr:
parts = test_expr.split("==")
if len(parts) == 2:
expected = parts[1].strip().strip("'")
return output.strip() == expected
return False
except Exception:
return False
def log_message(self, format, *args):
pass # Suppress logging
# ==============================
# Auth APIs
# ==============================
def do_POST(self):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path
# /api/login
if path == "/api/login":
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode()
try:
data = json.loads(body)
username = data.get("username", "")
password = data.get("password", "")
# 简单验证
if username in USERS:
pwd_hash = hashlib.sha256(password.encode()).hexdigest()
if pwd_hash == USERS[username]:
# 返回成功 token
self.send_json({
"success": True,
"token": "safe_linux_2026",
"message": "✅ 登录成功!"
})
return
self.send_json({"success": False, "error": "❌ 用户名或密码错误"}, 401)
except Exception as e:
self.send_json({"success": False, "error": str(e)}, 400)
# /api/logout
elif path == "/api/logout":
self.send_json({"success": True, "message": "👋 已退出登录"})
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b"Not Found")
if __name__ == "__main__":
PORT = 8084
print(f"🌱 Linux Sandbox Practice Server with Auth 启动中... http://127.0.0.1:{PORT}")
print("📚 地址: https://linux.xiaoxiaoluohao.indevs.in")
print("🔑 Account: admin / safe_linux_2026")
http.server.HTTPServer(("127.0.0.1", PORT), LinuxSandboxHandler).serve_forever()