diff --git a/.github/workflows/html2video-for-mcode-smoke.yml b/.github/workflows/html2video-for-mcode-smoke.yml
new file mode 100644
index 00000000..24dbc216
--- /dev/null
+++ b/.github/workflows/html2video-for-mcode-smoke.yml
@@ -0,0 +1,97 @@
+# html2video-for-mcode · scoped smoke CI
+# 官方主 CI(ci.yml)只跑 validate + node --test(无 ffmpeg/playwright, 渲染冒烟会 skip)。
+# 本 workflow 按 CONTRIBUTING/ci.yml 注释给出的"单插件 scoped workflow"模式,
+# 装齐依赖后真实执行全部测试 —— 这是 PR #41 评审要求的可执行测试证据。
+# 触发条件: 仅本插件目录(.github/workflows/ 本文件除外)或本文件自身变更。
+#
+# 第三轮 review 修正(2026-09-18, 1.7.1 才真正推上来 —— 1.7.0 时这份修正只改了本地没推送,
+# PR 分支上一直是旧枚举版, 漏跑 cover-transition / review-round2 / review-round3 / subtitles-invalidate):
+# ① 测试文件不再逐个枚举, 改用 shell glob `tests/*.test.mjs` —— 枚举写法每加一个测试文件就要
+# 手工记得改这里, 历史上已经漏过 css-kit / tokens-fx / chart-kit / table-kit 与上述四个文件。
+# ② 补 permissions: contents: read(与本仓库其余 scoped workflow 一致, 只读仓库)。
+# ③ actions 按仓库主 ci.yml 的同一批版本做**完整 SHA pin**(附版本注释), 不用浮动 tag;
+# SHA 由 gh api repos///commits/ 解析得到, 40 位整(主 ci.yml 的 checkout
+# 那行多了一位字符, 那是宿主仓库自己的笔误, 不去动它 —— 见 PR comment)。
+#
+# 第四轮 CHANGES_REQUESTED 修正(2026-09-21): 评审要求 exact-head 证据覆盖 **Windows** ——
+# 新增 windows-latest 作业(同一条命令, 依赖换成 choco ffmpeg + chromium)。两个作业的命令与
+# 依赖组合已先在技能镜像仓的 CI 上双平台跑绿(248 pass / 0 fail / 0 skip, 两个平台各一次),
+# Node 版本也从 22 提到 24 与那份证过的配置对齐(此 workflow 此前因审批门从未真正执行过)。
+# ffmpeg 一律走系统包(PATH 发现): 测试会把脚本放进临时项目目录当 cwd 跑, node_modules 发现
+# 依赖 cwd/上两层锚点, 在插件这种深层布局里够不到仓库根的 node_modules。
+name: html2video-for-mcode smoke
+
+on:
+ pull_request:
+ paths:
+ - 'plugins/Wzdhehe/html2video-for-mcode/**'
+ - '.github/workflows/html2video-for-mcode-smoke.yml'
+ push:
+ branches: [main]
+ paths:
+ - 'plugins/Wzdhehe/html2video-for-mcode/**'
+ - '.github/workflows/html2video-for-mcode-smoke.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ smoke-linux:
+ name: smoke (ubuntu-latest · ffmpeg + chromium)
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Set up Node
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: 24
+
+ - name: Install ffmpeg
+ run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ffmpeg
+
+ - name: Install playwright (no package.json / lockfile changes)
+ run: |
+ npm install --no-save --no-package-lock playwright
+ npx playwright install --with-deps chromium
+
+ # 一条命令跑完 tests/ 下的**全部** *.test.mjs(安全 / 策略 / 模板 / 受管块 / 渲染冒烟)。
+ # glob 交给 shell 展开: 以后新增测试文件自动进 CI, 不需要改这里(旧枚举写法漏过 8 个文件)。
+ # 单测超时 300s: 渲染冒烟里的 capture / build-video 在 CI 上要几十秒。
+ - name: All tests (security · policy · templates · css blocks · render smoke)
+ run: |
+ node --test --test-timeout=300000 \
+ plugins/Wzdhehe/html2video-for-mcode/skills/html2video-for-mcode/tests/*.test.mjs
+
+ smoke-windows:
+ name: smoke (windows-latest · ffmpeg + chromium)
+ runs-on: windows-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Set up Node
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: 24
+
+ # choco 的 ffmpeg 包含 ffmpeg.exe 与 ffprobe.exe, 落在 PATH 上
+ - name: Install ffmpeg
+ shell: bash
+ run: choco install ffmpeg -y --no-progress
+
+ - name: Install playwright (no package.json / lockfile changes)
+ shell: bash
+ run: |
+ npm install --no-save --no-package-lock playwright
+ npx playwright install chromium
+
+ - name: All tests (security · policy · templates · css blocks · render smoke)
+ shell: bash
+ run: |
+ node --test --test-timeout=300000 \
+ plugins/Wzdhehe/html2video-for-mcode/skills/html2video-for-mcode/tests/*.test.mjs
diff --git a/plugins/Wzdhehe/html2video-for-mcode/.claude-plugin/plugin.json b/plugins/Wzdhehe/html2video-for-mcode/.claude-plugin/plugin.json
new file mode 100644
index 00000000..1e3e089f
--- /dev/null
+++ b/plugins/Wzdhehe/html2video-for-mcode/.claude-plugin/plugin.json
@@ -0,0 +1,8 @@
+{
+ "name": "html2video-for-mcode",
+ "version": "1.9.19",
+ "description": "Turn a topic, outline, or script into a narrated MP4: HTML slides with staged entrance animations, TTS voiceover, ffmpeg assembly, and ASR verification.",
+ "skills": [
+ "./skills/html2video-for-mcode/SKILL.md"
+ ]
+}
diff --git a/plugins/Wzdhehe/html2video-for-mcode/.gitattributes b/plugins/Wzdhehe/html2video-for-mcode/.gitattributes
new file mode 100644
index 00000000..c304db3a
--- /dev/null
+++ b/plugins/Wzdhehe/html2video-for-mcode/.gitattributes
@@ -0,0 +1,10 @@
+# Force LF for this plugin's text files. Added in 1.9.3: nine files carried CRLF
+# (authored on Windows), which made `git diff --check` report trailing-whitespace
+# errors on the PR under default whitespace rules. Same convention as
+# plugins/antianqi/mcode-island/.gitattributes.
+# Override at clone time: `git config core.autocrlf input`.
+* text=auto eol=lf
+*.md text eol=lf
+*.mjs text eol=lf
+*.json text eol=lf
+LICENSE text eol=lf
diff --git a/plugins/Wzdhehe/html2video-for-mcode/CHANGELOG.md b/plugins/Wzdhehe/html2video-for-mcode/CHANGELOG.md
new file mode 100644
index 00000000..b00cb86d
--- /dev/null
+++ b/plugins/Wzdhehe/html2video-for-mcode/CHANGELOG.md
@@ -0,0 +1,583 @@
+# Changelog
+
+## 1.9.19 — 2026-10-10
+
+**One regression, found by reviewing our own 1.9.18 fix — and a test that structurally could not have caught it.**
+
+- **`--open` stopped opening the browser on Windows.** The 1.9.18 fix for M1 added `windowsVerbatimArguments` so the target would be quoted explicitly instead of relying on Node's quote-only-when-it-contains-spaces rule. But verbatim mode also stops Node from quoting the **empty-string argument**, so `start`'s empty *title* vanished: `start "" ""` became `start ""`. START treats the first quoted token as the **window title**, so with no command left to run nothing opens — while the script still prints "已在浏览器打开(若没弹出…)". A security fix that broke the feature it was securing.
+ - The rule was measured, not assumed. With `mkdir` as a marker: `cmd /c start "T" cmd /c ` runs `` (T became the title), while `cmd /c start T cmd /c ` does not (T became a command that does not exist). One quoted token and no command after it therefore opens nothing.
+ - Fix: the empty title is now a **literal** `'""'`, and the argv construction moved into an exported `openCmdArgv(target, platform)` so it can be asserted directly rather than inferred from a spawn.
+- **Why our own M1 test missed it, and what changed.** That test substituted `echo` for `start`. `echo` has no title semantics, so "the quoted path is consumed as a title" was outside anything the test could observe — the proxy proved quoting, not START's parser. The replacement asserts on `openCmdArgv`'s argv, **red-proves** that the pre-fix shape fails the same assertion, and then runs **`start` itself**: the quoted form must create the marker, the unquoted form must not.
+- **Housekeeping:** the new behavioural test runs `start /b`. Without `/b` every suite run flashes a console window on the machine running it (measured: 3639 ms and one new conhost with `/b` absent; 134 ms and none with it).
+
+**Tests +3 → 317 in sixteen files.** Suite re-run before pushing: **317 tests, 313 pass, 0 fail, 4 named capability skips** — all four the file-symlink canaries, skipped with the reason "当前环境建不了文件符号链接(Windows 需开发者模式; ubuntu CI 真跑)" because this Windows machine has Developer Mode off. The directory-junction equivalents of those guards (`safeRel` escape, `L1` image gate) run and pass here via the `mklink /J` fallback, and the render-smoke suite ran for real on this machine rather than skipping.
+
+## 1.9.18 — 2026-10-10
+
+**Independent security audit of the "parse and generate" dimension — the one the hosted review rounds never covered.** The auditor was barred from reading the earlier review records so as not to anchor; it reported 2 HIGH / 4 MEDIUM / 6 LOW. Every finding was reproduced on our own tree before acting, three of its claims did not survive that, and what it confirmed is fixed here.
+
+**HIGH — fixed**
+
+- **H1 · attribute injection in the play page (`preview-page.mjs`).** `setAttr` re-serialised a value it had just read out of a **single-quoted** attribute into double quotes **without escaping**, so an author's `` came back from `addNoFx` with a live event handler — and `X` (the animation toggle the docs push) is the trigger. Reproduced by calling the real function. Values are now HTML-escaped on the way out (`&` `"` `<` `>`); the same applies to `injectHtmlVars`'s `style` path. The regression test **parses attribute names** instead of searching for `on*=` text — after the fix the text legitimately lives inside the attribute value, so a text search would report a false red — and it carries a detector self-check (the unescaped shape must be recognised) so the pair cannot be green for free.
+- **H2 · SRT cue injection (`build-video.mjs`).** `clause` text was spliced into `out/subs.srt` verbatim, so a clause containing `\n\n99\n00:59:00,000 --> 01:00:00,000\n…` produced **more timecodes than cues** — forged cues in a file that gets uploaded to a platform. SRT generation moved to a single source, `tools.buildSrt`, which sanitises each line: bidi and C0/C1 control characters stripped, all whitespace runs folded to one space (so a cue is always one line), a residual inline `-->` turned into `→` because lenient parsers — ffmpeg's srt demuxer scans lines for timecodes — would otherwise still mis-read it. The invariant asserted is *line-anchored*: line-anchored timecode lines === cue count.
+
+**MEDIUM — fixed**
+
+- **M1 · `--open` could run a second command.** `spawn('cmd', ['/c','start','',target])` relied on Node quoting the target, and Node only quotes when the argument contains spaces or quotes — a Windows path may contain `&` without a space. Reproduced with a controlled origin: the second command really executed. The target is now quoted explicitly with `windowsVerbatimArguments`. The test runs both shapes and requires the unquoted one to still be exploitable, so it cannot pass for the wrong reason.
+- **M2 · `fx-*` classes were trusted by prefix.** `class="fx-notreal"` satisfied the data-stage rule while nothing animates, and a locally defined `.fx-mine` with a keyframe lacking `opacity` was never inspected (only `tokens.css` classes were). Both now check the slide's own `
+
+
+