feat: Linux练习平台
- Web界面Linux命令练习 - Python后端 + sandbox安全沙箱 - 课程和任务管理
This commit is contained in:
147
COURSE.md
Normal file
147
COURSE.md
Normal 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
192
COURSE_FULL.json
Normal 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
192
COURSE_TASKS.json
Normal 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": "⏰ 定时任务已显示!"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
121
COURSE_TASKS.json.backup.1772810579
Normal file
121
COURSE_TASKS.json.backup.1772810579
Normal 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
314
COURSE_TASKS.json.bak
Normal 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
314
COURSE_TASKS_NEW.json
Normal 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
314
COURSE_TASKS_v2.json
Normal 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": "⏰ 定时任务已查看!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
__pycache__/sandbox.cpython-312.pyc
Normal file
BIN
__pycache__/sandbox.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/server.cpython-312.pyc
Normal file
BIN
__pycache__/server.cpython-312.pyc
Normal file
Binary file not shown.
926
index.html
Normal file
926
index.html
Normal 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>
|
||||
582
index.html.backup.1772810579
Normal file
582
index.html.backup.1772810579
Normal 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
582
index.html.backup.test
Normal 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
582
index.html.bak
Normal 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
644
index.html.bak.1772815479
Normal 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
648
index.html.debug
Normal 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
582
index.html.v2.bak
Normal 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
582
index.html.v3.bak
Normal 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
926
index_new.html
Normal 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
17
linux-practice.service
Normal 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
581
sandbox.py
Normal 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
285
server.py
Executable 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
285
server.py.bak
Executable 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()
|
||||
Reference in New Issue
Block a user