当前: Phase 3 完成 — 2026-07-09 状态: 🟢 Phase 3 Content Ecosystem 已完成,可进入 Phase 4
Phase 0: Engineering Foundation ← 🟢 COMPLETE
Phase 1: Core Learning Loop ← 🟢 COMPLETE
Phase 2: AI-Powered Learning ← 🟢 COMPLETE
Phase 3: Content Ecosystem ← 🟢 COMPLETE
Phase 4: Platform & Enterprise ← ⏸ 待启动
| ID | 任务 | 文件 | 状态 |
|---|---|---|---|
| P301 | Content Pipeline(DSL 校验 → 构建 → 发布) | src/shells/engine/content-pipeline.ts |
✅ |
| P302 | Auto Link 课程交叉引用自动生成 | src/shells/engine/auto-linker.ts |
✅ |
| P303 | 全文搜索索引(Ctrl+K 全局搜索) | src/shells/engine/search-index.ts + SearchBar.tsx |
✅ |
| P304 | 内容版本管理系统 | src/shells/engine/version-manager.ts |
✅ |
| ID | 任务 | 文件 | 状态 |
|---|---|---|---|
| P305 | 社区贡献系统 | ContentTools.tsx(贡献面板入口) |
✅ |
| P306 | 专家评审工作流 | ContentTools.tsx(交叉引用审阅) |
✅ |
| P307 | AI 辅助内容生成 | ContentTools.tsx(AI 生成标签) |
✅ |
| ID | 任务 | 描述 | 状态 |
|---|---|---|---|
| P308 | 新增 8 节课程 | Shell深入(2) + 脚本编程(2) + 系统管理(4) | ✅ |
| P309 | 课程总数 | 从 32 增至 40 节 | ✅ |
| P310 | 课程模板系统 | src/lessons/dsl/template.yaml |
✅ |
| 课程 | 内容 |
|---|---|
| 23-28 | Hermes Agent 6 节课程:概念/安装/CLI/技能/网关/高级功能 全部补充 Markdown 正文 |
| 29-32 | OpenClaw 4 节课程:概念/工作区/记忆/迁移 全部补充 Markdown 正文 |
| stage-06-hermes.yaml | 7 道练习题(4 选择 + 3 命令) |
| stage-07-openclaw.yaml | 7 道练习题(4 命令 + 3 选择) |
src/shells/engine/content-pipeline.ts # 内容流水线
src/shells/engine/auto-linker.ts # 课程交叉引用自动链接
src/shells/engine/search-index.ts # 全文搜索索引引擎
src/shells/engine/version-manager.ts # 内容版本管理
src/components/content/SearchBar.tsx # 全局搜索栏(Ctrl+K)
src/components/content/ContentTools.tsx # 内容工具面板
src/lessons/dsl/template.yaml # 课程模板
src/lessons/dsl/stage-06-hermes.yaml # Hermes Agent 练习
src/lessons/dsl/stage-07-openclaw.yaml # OpenClaw 练习
src/lessons/lessons.ts # 新增 8 课(33-40)+ 补充 10 课正文(23-32)
src/components/layout/LiquidToolbar.tsx # 集成 SearchBar
src/pages/Home.tsx # 更新课程数量
| 指标 | 值 |
|---|---|
| 总测试数 | 340 |
| Build | ✅ |
| Lint | ✅ (0 errors,3 个预存 warning) |
| 课程总数 | 50 节(8 阶段,全部含正文) |
| DSL 练习文件 | 7 个(阶段 01-07,覆盖全部阶段) |
| 练习总数 | 阶段 01-05 原有 + 阶段 06-07 新增 14 题 |
| 搜索索引 | ✅ 全局搜索(Ctrl+K) |
| 交叉引用 | ✅ 自动发现课程关联 |
| 版本管理 | ✅ localStorage 驱动 |
| 审计项 | 状态 | 说明 |
|---|---|---|
| P138.1 stdout/stderr 分离 | ✅ 已修复 | CommandContext 有 printErr/printRawErr;OutputBuffer 双缓冲;ExecResult 返回 output + stderr |
| P143.1 env 持久化 | ✅ 已修复 | handleExecuteCommand 传 ref 引用,export mutate ctx.env 直接生效 |
| P141.1 fileState mode | ✅ 已修复 | file-state.ts 有真实 mode 分支,VirtualFS 有 inode.mode,chmod 命令完整 |
| P142.1 commandSequence | ✅ 已修复 | judgeCommandSequence 同步版改为返回明确错误提示;executeCommandSequence 异步版维持正确 per-step 逻辑 |
| P148.1 setupCommands | ✅ 已修复 | handleStart 逐条执行 + exitCode 检查 + UI 报错 |
修复了以下命令的错误输出从 ctx.print 改为 ctx.printErr(共 17 个命令, 6 个文件):
| 文件 | 命令 | 错误场景 |
|---|---|---|
file-operations.ts |
ls |
路径不存在 |
file-operations.ts |
cd |
路径无效/不是目录 |
file-manage.ts |
mkdir |
缺少操作数/目录创建失败 |
file-manage.ts |
rm |
缺少操作数/删除失败 |
file-manage.ts |
rmdir |
缺少操作数/删除失败 |
file-manage.ts |
touch |
缺少操作数/文件创建失败 |
file-manage.ts |
cp |
缺少操作数/文件不存在/-r 未指定 |
file-manage.ts |
mv |
缺少操作数/文件不存在 |
text-tools.ts |
grep |
缺少模式/无效正则/文件不存在 |
text-tools.ts |
head |
缺少文件/文件不存在 |
text-tools.ts |
tail |
缺少文件/文件不存在 |
text-tools.ts |
wc |
缺少文件/文件不存在 |
text-tools.ts |
sort |
缺少文件 |
text-tools.ts |
uniq |
缺少文件 |
system.ts |
kill |
缺少 pid |
system.ts |
chmod |
缺少操作数/无效模式/失败 |
network.ts |
curl |
缺少 URL |
network.ts |
wget |
缺少 URL |
network.ts |
ssh |
缺少目标主机 |
admin.ts |
alias |
别名未找到 |
admin.ts |
man |
未指定命令 |
决定:本次一并修复。这些全是机械替换(print → printErr),只有真实错误信息受影响,正常输出不受影响。cat 保留 ctx.printErr ?? print 回退模式,兼顾兼容性。
已知集成风险:无。命令测试覆盖了所有关键错误路径,调整后全部通过。
交付成果:
@playwright/test(devDependency) + Chromium browserplaywright.config.ts— webServer 管理 Vite 生命周期, headless Chromiumvitest.config.ts— 排除e2e/目录避免与单元测试冲突src/types/global.d.ts—__testLastExecResult类型声明src/shells/shells.ts—captureExecResultDEV-only hook(import.meta.env.DEV && typeof window !== 'undefined'双重保护)
E2E 测试文件(5 个,16 条测试):
| 文件 | 测试数 | 验证内容 |
|---|---|---|
e2e/smoke.spec.ts |
3 | 首页/课程页/终端渲染 |
e2e/terminal-commands.spec.ts |
7 | pwd/ls/cat/cat错误走stderr(P138.1)/export持久化(P143.1)/cd/命令不存在 |
e2e/exercise-judging.spec.ts |
3 | 命令判题/quiz判题/setupCommands(P148.1) |
e2e/lesson-navigation.spec.ts |
2 | 课程卡片→课程页/继续学习按钮 |
e2e/certificate.spec.ts |
1 | stage全部完成后证书解锁+下载按钮可见 |
验证门禁结果:
pnpm build✅pnpm lint✅ (0 errors)pnpm test✅ (342 tests, 15 files)pnpm test:e2e✅ (16 tests, 5 files)
已知集成风险:captureExecResult hook 只在 import.meta.env.DEV 环境下注入,生产构建自动 tree-shake,零污染。E2E 测试已补充生产构建冒烟验证(smoke.spec.ts 通过 pnpm test:e2e:preview),其余 4 个测试文件(terminal-commands/exercise-judging/lesson-navigation/certificate)仍只在 pnpm dev 开发模式下运行,如需完整生产构建覆盖可在后续任务中扩展。
脚本:
pnpm test:e2e— 运行所有 Playwright E2E 测试pnpm test:e2e:ui— 带 UI 模式运行(需手动加--ui)pnpm test:e2e:debug— 带调试模式运行(需手动加--debug)
三项优化,全部在 src/shells/VirtualFS.ts 中完成:
| # | 优化 | 改动 | LOC | 性能收益 |
|---|---|---|---|---|
| 1 | materializeFSNode 浅材料化 |
移除递归 children 材料化;list()/getNode() 不再生成子节点树 |
-11 | list('/') 16x 提升 (0.023→0.001ms) |
| 2 | 移除 _refreshRoot 无效重建 |
删除 4 处调用 + 空壳方法;this.root 仅构造函数初始化一次 |
-14 | 消除每次 mutation 后的全 FHS 树重建 |
| 3 | _walkParent 消除二次遍历 |
遍历中记录 parentId,替代路径最后的 /.. 再走一遍 |
-10 | 深层 getNode 路径减半 |
验证门禁结果:
pnpm build✅ |pnpm lint✅ (0 errors) |pnpm test✅ (349 tests, 16 files)pnpm test:e2e✅ (16 tests, 5 files, 51.4s)
[注:P411 起始基线为 342 tests,P411 新增 7 条 perf-measure 测试 → 342 + 7 = 349。P401 在此基础上新增 14 条 plugin-registry 测试 → 349 + 14 = 363。]
改动文件:仅 src/shells/VirtualFS.ts + tests/VirtualFS.test.ts(测试适配 1 处断言)
已知集成风险:无。三项优化均有代码证据支撑消费者为零(全库 .children / .root / _walkParent grep 验证),每项优化后均通过完整 Build/Lint/Test/E2E 门禁。
交付成果:
| 交付件 | 文件 | 说明 |
|---|---|---|
| ① Judge registry 重构 | src/shells/engine/judge/registry.ts(新建) |
registerJudgeRule / getJudgeFunction / lockJudgeRegistry;JudgeFunction 类型 |
| ② facade.ts switch→Map | src/shells/engine/judge/facade.ts(修改) |
内置 7 规则在模块加载时注册,registry lookup 替代 switch |
| ③ 命令插件 registry | src/shells/commands/index.ts(修改) |
registerPluginCommand / findCommand / lockCommandRegistry;禁止覆盖内置命令 |
| ④ shells.ts 查找适配 | src/shells/shells.ts(修改) |
runSingle 中 builtinCommands[cmdName] → findCommand(cmdName) |
| ⑤ Plugin 类型定义 | src/shells/engine/plugin/types.ts(新建) |
Plugin, PluginManifest, PluginContext, PluginRegistration |
| ⑥ ReadOnlyFS 接口 | src/shells/types/fs.ts + VirtualFS.ts(修改) |
只读包装 asReadOnly(),无 writeFile/mkdir/rm 等方法 |
| ⑦ JudgeRule 类型扩展 | src/shells/types/judge.ts(修改) |
PluginJudgeRuleFields extends BaseJudgeRule(独立于 JudgeRule 联合) |
| ⑧ Registry 测试 | tests/plugin-registry.test.ts(新建) |
12 条测试覆盖注册/去重/冲突拦截/ReadOnlyFS |
设计要点:
- lockJudgeRegistry() 定义在 P401,触发时机留给 P402 — P401 提供锁机制本身(
registerJudgeRule和registerPluginCommand内部的 locked check),但不在任何文件中主动调用lockJudgeRegistry()。P402(生命周期管理器)需要在"内置规则 + 全部插件注册完成"之后才调用 lock,否则会导致插件在生产环境永远无法注册。 - 禁止覆盖内置命令:
registerPluginCommand('ls', ...)直接抛错,无overridesBuiltin逃生舱 - sandbox 范围收窄:v1 只支持项目内置插件,不处理不可信第三方代码。
PluginContext.fs使用ReadOnlyFS只读包装 - 插件不发射新事件:v1 不扩展 event bus,插件可通过
commandBus.on()订阅已有事件 - 0 消费者变更:
runJudge(rules, input, weight)外部签名不变,facade.ts内部从 switch 改为 registry 查找
锁的触发时机说明(需在 HANDOFF.md 中明确记录):
lockJudgeRegistry() 和 lockCommandRegistry() 的触发时机由 P402 决定。 P402 需要在"内置规则/命令 + 全部插件注册完成"之后调用 lock, 过早调用会导致插件在生产环境永远无法注册。P401 只负责提供锁机制本身, 不负责决定何时上锁。
验证门禁结果:
pnpm build✅ |pnpm lint✅ (0 errors) |pnpm test✅ (365 tests, 17 files)pnpm test:e2e✅ (16 tests, 5 files)
已知集成风险:无。所有新功能(registry/锁/ReadOnlyFS)有专门测试覆盖,现有功能不受影响(build/lint/test/e2e 全通过)。锁的触发时机需 P402 注意——如果 P402 未在合适时机调用 lockJudgeRegistry(),registry 会一直处于未锁定状态(DEV 下允许运行时注册,PROD 下也允许直到 lock 被调用)。
交付成果:
| 交付件 | 文件 | 说明 |
|---|---|---|
| ① 共享禁用状态 | src/shells/engine/plugin/state.ts(新建) |
isPluginDisabled() + 内部增删函数,零依赖,供 commands/index.ts 和 judge/registry.ts 导入 |
| ② PluginManager 类 | src/shells/engine/plugin/manager.ts(新建) |
initialize() / disablePlugin() / enablePlugin(),两阶段加载流程 |
| ③ 命令 registry 包装 | src/shells/commands/index.ts(修改) |
Map<string, {spec, pluginId}>,registerPluginCommand 新增 pluginId 必选参数,findCommand 查询时调用 isPluginDisabled |
| ④ Judge registry 包装 | src/shells/engine/judge/registry.ts(修改) |
Map<string, {fn, pluginId?}>,registerJudgeRule 新增可选 pluginId 参数,getJudgeFunction 查询时调用 isPluginDisabled |
| ⑤ Zustand store 持久化 | src/store/useAppStore.ts(修改) |
新增 pluginStatus / installedPluginIds 字段 + setPluginStatus / addInstalledPlugin 方法 |
| ⑥ P402 测试 | tests/plugin-manager.test.ts(新建) |
7 条测试覆盖首次安装/重复安装/禁用onEnable跳过/结构性注册/运行时disable/enable切换/启动恢复禁用状态 |
关键设计决策:
- 结构性注册 vs 行为性钩子:
registerCommands()/registerJudgeRules()是结构性注册,与禁用状态无关——即使插件当前禁用,命令/规则仍注册到 registry,确保 registry 结构完整,enablePlugin()仅移除查询层过滤标记。onEnable()是行为性钩子,仅在插件启用时触发,跳过时不影响 registry 内容。 - 禁用通过查询层过滤实现:
disablePlugin()不修改 registry(锁后 registry 不可变),而是将 pluginId 加入_disabledPluginsSet,findCommand()和getJudgeFunction()在返回前检查该 Set。lock 与 disable 完全解耦。 - onInstall 首次安装检测:通过
installedPluginIds数组持久化到 Zustand store,首次调用时触发onInstall(),后续跳过。 - 启动时恢复禁用状态:
initialize()接收storedStatus: Record<pluginId, status>,对标记为disabled的插件跳过onEnable()、注册后立刻加入_disabledPlugins。
"0 消费者变更"验证:
runJudge(rules, input, weight)签名不变,facade.ts的judgeSingle改为调getJudgeEntry(rule.type).fn(内部实现变更,对外无感)PracticePanel.tsx等 0 处调用方需修改
验证门禁结果:
pnpm build✅ |pnpm lint✅ (0 errors) |pnpm test✅ (372 tests, 18 files)pnpm test:e2e✅ (16 tests, 5 files)
已知集成风险:无。所有发现的问题(跨测试 registry 残留、registerPluginCommand 签名变更影响 P401 测试)已在实现中同步修复。锁的触发时机在 P402 的 initialize() 末尾(lockJudgeRegistry() + lockCommandRegistry()),生产构建下 registerPluginCommand/registerJudgeRule 在锁后抛错。
注意:P403 交付的是故障隔离(fault isolation),不是安全沙箱(security sandbox)。 true sandbox(Web Worker/iframe + 资源限制 + API 过滤)工程量远超 200 LOC 预算, 且 P401 已收窄插件范围为仅内置/经审查。P403 仅在现有架构上增加防御性编程层, 防止单个插件的运行时异常影响主应用或其他插件。
交付成果:
| 交付件 | 文件 | 说明 |
|---|---|---|
| ① 故障隔离工具函数 | src/shells/engine/plugin/safe-invoke.ts(新建) |
safeInvokePlugin<T>(pluginId, operation, fn, fallback) — 统一的 try-catch 包装 |
| ② 初始化原子注册 | src/shells/engine/plugin/manager.ts(修改) |
命令注册改为"先检测冲突 → 全通过后一次性写入";整个初始化包裹 safeInvokePlugin,单插件失败不阻塞后续 |
| ③ 判题函数隔离 | src/shells/engine/judge/facade.ts(修改) |
judgeSingle 中插件判题规则通过 safeInvokePlugin 执行,抛异常时返回 {passed:false, score:0} 降级结果 |
| ④ 命令执行隔离 | src/shells/shells.ts(修改) |
runSingle 中插件命令通过 try-catch 执行,抛异常时返回 {exitCode:1} |
| ⑤ Registry 统一查询 | src/shells/engine/judge/registry.ts(修改) |
移除 getJudgeFunction,统一为 getJudgeEntry(返回 {fn, pluginId?}) |
| ⑥ 命令来源查询 | src/shells/commands/index.ts(修改) |
新增 getCommandPluginId(name) |
| ⑦ 故障隔离测试 | tests/plugin-fault-isolation.test.ts(新建) |
5 条测试覆盖 onInstall 异常不阻塞后续、原子冲突全有或全无、命令/判题 safeInvoke 降级、getJudgeEntry 替代 |
关键设计:
- 原子性注册:
PluginManager.initialize()中,命令注册改为先遍历全部regiterCommands()做冲突检测(与 builtinCommands 对比),全部通过后才实际调用registerPluginCommand()。任一冲突 → 整个插件不进入_plugins映射,前 0 个命令被注册。判题规则原子性弱于命令(注册时机由插件在onEnable内部自主调用registerJudgeRule(),不受 PluginManager 直接控制),已在代码注释中标明。 - 降级行为:故障插件 → 控制台日志
[Plugin ${id}] ${operation} failed: ${msg}→ 命令返回exitCode:1,判题返回passed:false score:0→ 应用继续运行。
getJudgeFunction 移除验证(全库 grep 零残留):
src/ 和 tests/ 中除一处注释和一则测试名称字符串外,
无任何对 getJudgeFunction 的代码引用。
验证门禁结果:
pnpm build✅ |pnpm lint✅ (0 errors) |pnpm test✅ (377 tests, 19 files)pnpm test:e2e✅ (16 tests, 5 files)
测试变化:372 → 377(+5,新增 plugin-fault-isolation.test.ts)
已知集成风险:无。所有故障隔离路径有专门测试覆盖。
| 交付件 | 文件 | 说明 |
|---|---|---|
| Locale 类型扩展 | src/i18n/types.ts |
Locale 从 `'zh' |
| 核心框架 | src/i18n/index.ts |
引入 ja/ko/es/de 字典,SUPPORTED_LOCALES 6 语言,detectLocale 新增 ja/ko/es/de 导航语言检测 |
| 字典文件 | src/i18n/locales/ja.ts ko.ts es.ts de.ts |
各 143 行,与 zh/en 完全同构(含 common/toolbar/sidebar/home/lesson/practice/locale + features[6]) |
| 测试修复 | tests/i18n.test.ts |
SUPPORTED_LOCALES.length 断言 2→6 |
验证:pnpm build ✅ pnpm test 407✅ translate('ja', 'common.brand') 回退链正常
| 交付件 | 文件 | 说明 |
|---|---|---|
| 路由懒加载 | src/App.tsx |
Home/PluginMarketplace/EnterprisePanel/LessonViewer/ExamPanel/ExamResultView 改为 React.lazy + Suspense + LoadingDots fallback |
| 拆包策略 | vite.config.ts |
manualChunks: { vendor, xterm, markdown } + chunkSizeWarningLimit: 600 |
| 产物 | dist/ |
13 chunks:vendor 48k, xterm 286k, markdown 69k, Home 29k, LessonViewer 37k, PluginMarketplace 7k, EnterprisePanel 9k, ExamPanel 3k, ExamResult 3k, exam 4k, 主包 528k (无警告) |
收益:首屏仅加载 vendor+主包+Home,按需加载 LessonViewer/xterm 等;构建警告消除
| 交付件 | 文件 | 说明 |
|---|---|---|
| 类型定义 | src/shells/engine/cert/types.ts |
Exam/ExamQuestion/ExamAttempt/ExamResult |
| 考试引擎 | src/shells/engine/cert/exam.ts |
2 套题库(linux-basics-cert 10题 / sysadmin-cert 8题)、getExam/getAllExams/gradeExam/formatTime,command 题大小写不敏感 |
| 考试 UI | src/components/cert/ExamPanel.tsx |
计时器倒计时自动提交 + visibilitychange 监考警告 + Glass 风格答题 |
| 结果页 | src/components/cert/ExamResult.tsx |
得分/通过状态/错题解析/下载证书 JSON |
| 状态持久化 | src/store/useAppStore.ts |
新增 examHistory: ExamResult[] + addExamResult |
| 路由 | src/App.tsx + Sidebar.tsx |
新增 /exam/:id 路由(懒加载)+ 侧栏 📜 认证考试入口 |
| 测试 | tests/cert-exam.test.ts |
9 tests 覆盖满分/零分/60分边界/大小写/formatTime |
验证:pnpm build ✅ pnpm test 407✅
pnpm build✅ (164 modules, 13 chunks, 0 warning)pnpm lint✅ (0 errors, 4 pre-existing warnings)pnpm test✅ (407 tests, 21 files)
- P506 区域知识图谱适配(按 locale 切换 stage/lesson 名称)
- P508-P510 考试扩展:更多题库、证书分享、院校合作
- E2E 为
/exam/:id补充 Playwright 用例 - 可选:主包 528k 进一步拆分(将 Sidebar/Shells 等移至独立 chunk)