diff --git a/clashoo/Makefile b/clashoo/Makefile
index b1918a71c..8f838e117 100644
--- a/clashoo/Makefile
+++ b/clashoo/Makefile
@@ -6,13 +6,13 @@ PKG_UPSTREAM_TAG:=v$(PKG_UPSTREAM_VERSION)
PKG_SHORT_SHA:=3f27ef3
PKG_VERSION:=2026.11.04.$(PKG_UPSTREAM_VERSION)
-PKG_RELEASE:=23
+PKG_RELEASE:=24
PKG_SOURCE:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
-PKG_SOURCE_VERSION:=2d264afc85d34b9a70e07c8505e7e94c235d79c9
+PKG_SOURCE_VERSION:=f03c662c952072d2e7bdae30e6ab5a0360606266
PKG_HASH:=skip
PKG_BUILD_VERSION:=$(PKG_UPSTREAM_TAG)
diff --git a/docs/site/README.md b/docs/site/README.md
new file mode 100644
index 000000000..191c6fa6a
--- /dev/null
+++ b/docs/site/README.md
@@ -0,0 +1,36 @@
+# Clashoo 在线文档
+
+线上地址:
+
+内容在 `content/`,每页只写正文片段;页头、侧栏、页脚和版本号由 `build.py` 统一生成。构建只用 Python 标准库,没有第三方依赖。
+
+## 本地预览
+
+```sh
+python3 docs/site/build.py
+python3 -m http.server 4173 --directory .build/docs-site
+```
+
+## 自检
+
+```sh
+python3 docs/site/build.py --check
+```
+
+`--check` 会拦下这几类问题:锚点 id 重复、站内链接指向不存在的文件、导航里漏登的正文页、`h2`/`h3` 缺 `id`、正文没有以 `
` 开头、出现以 `/` 开头的绝对路径。
+
+## 加一页
+
+1. 在 `content/` 新建 `xxx.html`,以 `` 开头,`h2`/`h3` 都带 `id`
+2. 在 `site.json` 的 `nav` 里登记,决定它出现在哪一组
+3. 跑一遍 `--check`,再提交
+
+## 内容从哪来
+
+页面正文与仓库 Wiki(`.github/skills/clashoo-user-guide/references/`)同源,Wiki 由 CI 自动同步。改功能说明时两边都要照顾到:站点讲「怎么点」,Wiki 讲「原理与排障内幕」。
+
+## 发布
+
+`.github/workflows/docs.yml` 在 `main` 分支上跑自检、构建并发布到 GitHub Pages。
+
+前提:仓库 Settings → Pages → Source 选择 **GitHub Actions**。没有这一步,`deploy` 任务会因为拿不到 Pages 环境而失败。
diff --git a/docs/site/assets/logo.png b/docs/site/assets/logo.png
new file mode 100644
index 000000000..021e4c692
Binary files /dev/null and b/docs/site/assets/logo.png differ
diff --git a/docs/site/assets/site.css b/docs/site/assets/site.css
new file mode 100644
index 000000000..47128778a
--- /dev/null
+++ b/docs/site/assets/site.css
@@ -0,0 +1,310 @@
+/* Clashoo 文档站 —— 产品化三栏:顶栏 + 左轨 + 正文 + 右目录 */
+
+:root {
+ --bg: #fbfcfb;
+ --surface: #ffffff;
+ --surface-2: #f2f5f3;
+ --fg: #161b19;
+ --fg-soft: #5a6662;
+ --line: #e4e9e6;
+ --accent: #2f6f4f;
+ --accent-ink: #1d4b34;
+ --accent-soft: #eaf3ee;
+ --shadow: 0 1px 2px rgba(22, 27, 25, 0.05), 0 12px 28px -18px rgba(22, 27, 25, 0.25);
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ --radius: 14px;
+ --topbar: 60px;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #0f1412;
+ --surface: #151b18;
+ --surface-2: #1a211e;
+ --fg: #e8eee9;
+ --fg-soft: #9aa8a1;
+ --line: #26302c;
+ --accent: #79c79c;
+ --accent-ink: #c8ecd9;
+ --accent-soft: #16261e;
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 16px 32px -20px rgba(0, 0, 0, 0.8);
+ }
+}
+
+html[data-theme='light'] {
+ --bg: #fbfcfb;
+ --surface: #ffffff;
+ --surface-2: #f2f5f3;
+ --fg: #161b19;
+ --fg-soft: #5a6662;
+ --line: #e4e9e6;
+ --accent: #2f6f4f;
+ --accent-ink: #1d4b34;
+ --accent-soft: #eaf3ee;
+ --shadow: 0 1px 2px rgba(22, 27, 25, 0.05), 0 12px 28px -18px rgba(22, 27, 25, 0.25);
+}
+
+html[data-theme='dark'] {
+ --bg: #0f1412;
+ --surface: #151b18;
+ --surface-2: #1a211e;
+ --fg: #e8eee9;
+ --fg-soft: #9aa8a1;
+ --line: #26302c;
+ --accent: #79c79c;
+ --accent-ink: #c8ecd9;
+ --accent-soft: #16261e;
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 16px 32px -20px rgba(0, 0, 0, 0.8);
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--fg);
+ font: 15.5px/1.75 -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
+ 'Microsoft YaHei', sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+
+a { color: var(--accent); text-decoration: none; }
+a:hover { color: var(--accent-ink); }
+
+.skip-link { position: absolute; left: -9999px; }
+.skip-link:focus {
+ left: 10px; top: 10px; padding: 6px 12px; z-index: 200;
+ background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
+}
+
+/* ── 顶栏 ───────────────────────────────── */
+.topbar {
+ position: sticky; top: 0; z-index: 30;
+ height: var(--topbar);
+ display: flex; align-items: center; gap: 18px;
+ padding: 0 22px;
+ background: color-mix(in srgb, var(--bg) 90%, transparent);
+ backdrop-filter: saturate(140%) blur(10px);
+ border-bottom: 1px solid var(--line);
+}
+
+.brand { display: flex; align-items: center; gap: 10px; color: var(--fg); }
+.brand img { border-radius: 9px; }
+.brand-text { display: flex; flex-direction: column; line-height: 1.15; font-weight: 700; font-size: 15px; letter-spacing: -0.01em; }
+.brand-text em { font-style: normal; font-weight: 500; font-size: 11px; color: var(--fg-soft); letter-spacing: 0.02em; }
+
+.topnav { display: flex; gap: 4px; margin-left: 8px; }
+.topnav a {
+ padding: 6px 11px; border-radius: 8px; font-size: 13.5px; font-weight: 500;
+ color: var(--fg-soft);
+}
+.topnav a:hover { background: var(--surface-2); color: var(--fg); }
+
+.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
+
+.search-trigger {
+ display: inline-flex; align-items: center; gap: 7px;
+ padding: 7px 12px; font: inherit; font-size: 13px;
+ color: var(--fg-soft); background: var(--surface);
+ border: 1px solid var(--line); border-radius: 10px; cursor: pointer;
+}
+.search-trigger:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); color: var(--fg); }
+kbd {
+ font: 11px/1 var(--mono); padding: 3px 5px; border-radius: 5px;
+ background: var(--surface-2); border: 1px solid var(--line); color: var(--fg-soft);
+}
+.gh-link { font-size: 13px; color: var(--fg-soft); padding: 7px 10px; border-radius: 8px; }
+.gh-link:hover { background: var(--surface-2); color: var(--fg); }
+.icon-button {
+ height: 32px; min-width: 32px; padding: 0 8px; font: inherit; font-size: 13px;
+ color: var(--fg-soft); background: transparent;
+ border: 1px solid var(--line); border-radius: 9px; cursor: pointer;
+}
+.icon-button:hover { color: var(--fg); background: var(--surface-2); }
+.menu-button { display: none; }
+
+/* ── 三栏骨架 ───────────────────────────── */
+.shell {
+ display: grid;
+ grid-template-columns: 216px minmax(0, 1fr) 196px;
+ gap: 40px;
+ max-width: 1240px;
+ margin: 0 auto;
+ padding: 34px 26px 80px;
+ align-items: start;
+}
+
+/* 左轨 */
+.rail { position: sticky; top: calc(var(--topbar) + 26px); font-size: 13.5px; }
+.nav-label {
+ margin: 20px 0 6px; font-size: 10.5px; font-weight: 700; letter-spacing: 0.09em;
+ text-transform: uppercase; color: var(--fg-soft); opacity: 0.75;
+}
+.rail-nav a {
+ display: block; padding: 6px 12px; margin: 1px 0; border-radius: 8px;
+ color: var(--fg-soft); border-left: 2px solid transparent;
+}
+.rail-nav a:hover { color: var(--fg); background: var(--surface-2); }
+.rail-nav a[aria-current='page'] {
+ color: var(--accent-ink); background: var(--accent-soft);
+ border-left-color: var(--accent); font-weight: 600;
+}
+.rail-meta {
+ margin-top: 26px; padding-top: 14px; border-top: 1px dashed var(--line);
+ display: flex; flex-direction: column; gap: 2px;
+}
+.rail-version { font: 12px/1.4 var(--mono); color: var(--fg); }
+.rail-note { font-size: 11px; color: var(--fg-soft); }
+
+/* 右目录 */
+.toc { position: sticky; top: calc(var(--topbar) + 26px); font-size: 13px; }
+.toc-label {
+ margin: 0 0 8px; font-size: 10.5px; font-weight: 700; letter-spacing: 0.09em;
+ text-transform: uppercase; color: var(--fg-soft); opacity: 0.75;
+}
+.toc nav { border-left: 1px solid var(--line); }
+.toc a {
+ display: block; padding: 4px 0 4px 12px; color: var(--fg-soft);
+ border-left: 2px solid transparent; margin-left: -1px; line-height: 1.5;
+}
+.toc a:hover { color: var(--accent); border-left-color: var(--accent); }
+
+/* ── 正文 ───────────────────────────────── */
+.doc { min-width: 0; }
+
+.doc h1 {
+ margin: 0 0 18px; font-size: 34px; line-height: 1.2;
+ letter-spacing: -0.02em; font-weight: 800;
+}
+.doc h2 {
+ display: flex; align-items: center; gap: 10px;
+ margin: 44px 0 14px; font-size: 20px; line-height: 1.35;
+ letter-spacing: -0.01em; font-weight: 700;
+}
+.doc h2::before {
+ content: ''; flex: none; width: 7px; height: 7px; border-radius: 2px;
+ background: var(--accent); transform: rotate(45deg);
+}
+.doc h3 { margin: 28px 0 10px; font-size: 16px; font-weight: 700; color: var(--fg); }
+.doc p { margin: 12px 0; color: color-mix(in srgb, var(--fg) 88%, var(--fg-soft)); }
+.doc ul, .doc ol { margin: 12px 0; padding-left: 22px; }
+.doc li { margin: 5px 0; }
+.doc li::marker { color: var(--accent); }
+
+.doc a { text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent); text-underline-offset: 3px; }
+.doc a:hover { text-decoration-color: var(--accent); }
+
+.doc code {
+ font: 0.88em/1.5 var(--mono); padding: 1.5px 5px; border-radius: 5px;
+ background: var(--surface-2); color: var(--accent-ink);
+ border: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
+}
+.doc pre {
+ margin: 16px 0; padding: 16px 18px; overflow-x: auto;
+ background: var(--surface-2); border: 1px solid var(--line);
+ border-radius: 12px;
+}
+.doc pre code { padding: 0; border: 0; background: none; font-size: 13px; color: var(--fg); white-space: pre-wrap; overflow-wrap: anywhere; }
+
+.doc blockquote {
+ margin: 18px 0; padding: 12px 16px;
+ background: var(--accent-soft);
+ border-left: 3px solid var(--accent);
+ border-radius: 0 10px 10px 0;
+ color: var(--fg);
+}
+.doc blockquote p { margin: 0; color: inherit; }
+
+.doc table {
+ width: 100%; margin: 18px 0; border-collapse: separate; border-spacing: 0;
+ font-size: 14px; background: var(--surface);
+ border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
+}
+.doc th {
+ text-align: left; padding: 10px 14px; font-size: 11.5px; font-weight: 700;
+ letter-spacing: 0.06em; text-transform: uppercase;
+ color: var(--fg-soft); background: var(--surface-2);
+ border-bottom: 1px solid var(--line);
+}
+.doc td b { white-space: nowrap; }
+.doc td { padding: 11px 14px; border-top: 1px solid var(--line); vertical-align: top; }
+.doc tr:first-child td { border-top: 0; }
+.doc tbody tr:hover { background: color-mix(in srgb, var(--accent-soft) 55%, transparent); }
+
+/* 卡片 */
+.hero { margin: 0 0 22px; }
+.hero img {
+ border-radius: 22px; padding: 6px; background: var(--surface);
+ box-shadow: var(--shadow); border: 1px solid var(--line);
+}
+.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; margin: 22px 0 8px; }
+.card {
+ display: flex; flex-direction: column; gap: 4px;
+ padding: 15px 16px; background: var(--surface);
+ border: 1px solid var(--line); border-radius: 12px;
+ box-shadow: var(--shadow); color: var(--fg);
+ transition: transform 0.16s ease, border-color 0.16s ease;
+}
+.card strong { font-size: 14px; color: var(--accent); }
+.card span { font-size: 12.5px; color: var(--fg-soft); line-height: 1.5; }
+.card:hover { transform: translateY(-2px); border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
+.card::after { content: '↗'; margin-top: 6px; font-size: 12px; color: var(--accent); }
+
+.doc-footer {
+ display: flex; justify-content: space-between; gap: 12px;
+ margin-top: 56px; padding-top: 16px; border-top: 1px solid var(--line);
+ font-size: 12.5px; color: var(--fg-soft);
+}
+.doc-footer a { color: var(--fg-soft); text-decoration: none; }
+.doc-footer a:hover { color: var(--accent); }
+
+/* ── 搜索 ───────────────────────────────── */
+.search-overlay {
+ position: fixed; inset: 0; z-index: 100;
+ display: flex; flex-direction: column; align-items: center;
+ padding: 12vh 18px 0;
+ background: color-mix(in srgb, #000 42%, transparent);
+ backdrop-filter: blur(3px);
+}
+.search-overlay[hidden] { display: none; }
+.search-box {
+ width: min(620px, 100%); display: flex; gap: 8px; align-items: center;
+ padding: 10px 12px; background: var(--surface);
+ border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow);
+}
+#search-input {
+ flex: 1; font: inherit; font-size: 15px; color: var(--fg);
+ background: transparent; border: 0; outline: none;
+}
+.search-results {
+ width: min(620px, 100%); margin: 10px 0 0; padding: 6px; list-style: none;
+ background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
+ box-shadow: var(--shadow); max-height: 56vh; overflow: auto;
+}
+.search-results:empty { display: none; }
+.search-results a {
+ display: block; padding: 9px 12px; border-radius: 9px;
+ color: var(--fg); font-size: 14px;
+}
+.search-results a small { display: block; font-size: 11.5px; color: var(--fg-soft); }
+.search-results a:hover, .search-results a[data-active='true'] { background: var(--accent-soft); }
+
+/* ── 响应式 ─────────────────────────────── */
+@media (max-width: 1140px) {
+ .shell { grid-template-columns: 200px minmax(0, 1fr); gap: 32px; }
+ .toc { display: none; }
+}
+@media (max-width: 800px) {
+ .topnav { display: none; }
+ .menu-button { display: inline-flex; align-items: center; }
+ .shell { display: block; padding: 20px 18px 64px; }
+ .rail {
+ position: static; display: none;
+ margin-bottom: 18px; padding: 10px 12px;
+ background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
+ }
+ .rail.open { display: block; }
+ .doc h1 { font-size: 27px; }
+ .doc h2 { font-size: 18px; }
+ .doc table, .doc pre { font-size: 13px; }
+}
diff --git a/docs/site/assets/site.js b/docs/site/assets/site.js
new file mode 100644
index 000000000..206e4037d
--- /dev/null
+++ b/docs/site/assets/site.js
@@ -0,0 +1,168 @@
+// Clashoo 文档站交互:主题、手机目录、站内搜索。无第三方依赖。
+(function () {
+ 'use strict';
+
+ var THEME_KEY = 'clashoo-docs-theme';
+ var root = document.documentElement;
+
+ function applyTheme(theme) {
+ if (theme) root.setAttribute('data-theme', theme);
+ else root.removeAttribute('data-theme');
+ var toggle = document.getElementById('theme-toggle');
+ if (toggle) toggle.setAttribute('aria-pressed', String(theme === 'dark'));
+ }
+
+ try {
+ applyTheme(localStorage.getItem(THEME_KEY));
+ } catch (e) {}
+
+ document.addEventListener('click', function (event) {
+ var toggle = event.target.closest('#theme-toggle');
+ if (toggle) {
+ var next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
+ try {
+ localStorage.setItem(THEME_KEY, next);
+ } catch (e) {}
+ applyTheme(next);
+ return;
+ }
+
+ var menu = event.target.closest('#menu-toggle');
+ if (menu) {
+ var sidebar = document.getElementById('sidebar');
+ if (!sidebar) return;
+ var open = sidebar.classList.toggle('open');
+ menu.setAttribute('aria-expanded', String(open));
+ }
+ });
+
+ // ── 站内搜索 ──
+ var overlay = document.getElementById('search');
+ var input = document.getElementById('search-input');
+ var results = document.getElementById('search-results');
+ var index = null;
+ var active = -1;
+
+ function loadIndex() {
+ if (index) return Promise.resolve(index);
+ return fetch('search-index.json')
+ .then(function (response) {
+ return response.ok ? response.json() : [];
+ })
+ .then(function (data) {
+ index = data;
+ return index;
+ })
+ .catch(function () {
+ index = [];
+ return index;
+ });
+ }
+
+ function openSearch() {
+ if (!overlay) return;
+ overlay.hidden = false;
+ loadIndex().then(function () {
+ if (input) {
+ input.value = '';
+ input.focus();
+ }
+ render('');
+ });
+ }
+
+ function closeSearch() {
+ if (!overlay) return;
+ overlay.hidden = true;
+ active = -1;
+ }
+
+ function score(entry, needle) {
+ var hits = [];
+ if (entry.title.toLowerCase().indexOf(needle) >= 0) hits.push({ url: entry.page, label: entry.title, kind: entry.group });
+ entry.headings.forEach(function (heading) {
+ if (heading.text.toLowerCase().indexOf(needle) >= 0) {
+ hits.push({ url: entry.page + '#' + heading.id, label: heading.text, kind: entry.title });
+ }
+ });
+ return hits;
+ }
+
+ function render(query) {
+ if (!results) return;
+ var needle = query.trim().toLowerCase();
+ results.innerHTML = '';
+ active = -1;
+ if (!needle || !index) return;
+
+ var hits = [];
+ index.forEach(function (entry) {
+ hits = hits.concat(score(entry, needle));
+ if (hits.length >= 24) return;
+ });
+ if (!hits.length && index) {
+ index.forEach(function (entry) {
+ if (hits.length >= 8) return;
+ if (entry.text.toLowerCase().indexOf(needle) >= 0) hits.push({ url: entry.page, label: entry.title, kind: '正文命中 · ' + entry.group });
+ });
+ }
+
+ hits.slice(0, 24).forEach(function (hit) {
+ var item = document.createElement('li');
+ var link = document.createElement('a');
+ link.href = hit.url;
+ link.textContent = hit.label;
+ var meta = document.createElement('small');
+ meta.textContent = hit.kind;
+ link.appendChild(meta);
+ item.appendChild(link);
+ results.appendChild(item);
+ });
+ }
+
+ document.addEventListener('click', function (event) {
+ if (event.target.closest('[data-search-open]')) {
+ openSearch();
+ return;
+ }
+ if (event.target.closest('[data-search-close]')) {
+ closeSearch();
+ return;
+ }
+ if (overlay && !overlay.hidden && event.target === overlay) closeSearch();
+ });
+
+ document.addEventListener('input', function (event) {
+ if (event.target === input) render(input.value);
+ });
+
+ document.addEventListener('keydown', function (event) {
+ var isK = event.key === 'k' || event.key === 'K';
+ if (isK && (event.metaKey || event.ctrlKey)) {
+ event.preventDefault();
+ if (overlay && overlay.hidden) openSearch();
+ else closeSearch();
+ return;
+ }
+ if (event.key === 'Escape' && overlay && !overlay.hidden) {
+ closeSearch();
+ return;
+ }
+ if (overlay && overlay.hidden) return;
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Enter') return;
+
+ var links = results ? Array.prototype.slice.call(results.querySelectorAll('a')) : [];
+ if (!links.length) return;
+ if (event.key === 'Enter' && active >= 0) {
+ links[active].click();
+ return;
+ }
+ event.preventDefault();
+ active = event.key === 'ArrowDown' ? (active + 1) % links.length : (active + links.length - 1) % links.length;
+ links.forEach(function (link, i) {
+ if (i === active) link.setAttribute('data-active', 'true');
+ else link.removeAttribute('data-active');
+ });
+ links[active].scrollIntoView({ block: 'nearest' });
+ });
+})();
diff --git a/docs/site/build.py b/docs/site/build.py
new file mode 100644
index 000000000..c116fe255
--- /dev/null
+++ b/docs/site/build.py
@@ -0,0 +1,303 @@
+#!/usr/bin/env python3
+"""构建 Clashoo 文档站。
+
+只用 Python 标准库:读取 site.json 与 content/ 下的正文片段,套上统一布局,
+顺手做一遍自检(锚点重复、站内死链、导航漏页),最后输出到 .build/docs-site。
+
+ python3 docs/site/build.py # 构建
+ python3 docs/site/build.py --check # 只自检,不写文件
+ python3 docs/site/build.py --output custom # 指定输出目录
+"""
+
+from __future__ import annotations
+
+import argparse
+import html
+import json
+import re
+import shutil
+import sys
+from pathlib import Path
+from string import Template
+from urllib.parse import urlsplit
+
+ROOT = Path(__file__).resolve().parent
+REPO = ROOT.parents[1]
+
+HEADING = re.compile(r"]*)>(.*?)", re.S)
+ID_ATTR = re.compile(r'id="([^"]+)"')
+LINK_ATTR = re.compile(r'(?:href|src)="([^"]+)"')
+TAG = re.compile(r"<[^>]+>")
+
+LAYOUT = Template(
+ """
+
+
+
+
+
+ $title$title_suffix
+
+
+
+
+
+
+
+
+ 跳到正文
+
+
+
+
+
+"""
+)
+
+
+def read_site() -> dict:
+ return json.loads((ROOT / "site.json").read_text(encoding="utf-8"))
+
+
+def read_version(site: dict) -> str:
+ """版本号从 Makefile 读,文档不会比包版本落后。"""
+ makefile = REPO / site["version_file"]
+ if not makefile.is_file():
+ return ""
+ text = makefile.read_text(encoding="utf-8")
+ version = re.search(r"^PKG_VERSION:=(.+)$", text, re.M)
+ release = re.search(r"^PKG_RELEASE:=(.+)$", text, re.M)
+ if not version:
+ return ""
+ return version.group(1).strip() + (f"-r{release.group(1).strip()}" if release else "")
+
+
+def pages(site: dict) -> list[dict]:
+ return [item | {"group": group["group"]} for group in site["nav"] for item in group["items"]]
+
+
+def strip_tags(text: str) -> str:
+ return re.sub(r"\s+", " ", html.unescape(TAG.sub(" ", text))).strip()
+
+
+def collect_ids(body: str, page: str) -> set[str]:
+ ids: set[str] = set()
+ for value in ID_ATTR.findall(body):
+ if value in ids:
+ raise SystemExit(f"{page}: 锚点 id 重复 —— {value}")
+ ids.add(value)
+ return ids
+
+
+def check_structure(body: str, page: str) -> list[str]:
+ """Pages 部署在 /openwrt-clashoo/ 子路径下,绝对路径会 404;正文要在第一个 h2 之前出现 h1。"""
+ problems = []
+ h1 = body.find("")
+ elif h2 >= 0 and h2 < h1:
+ problems.append(f"{page}: 必须出现在第一个 之前")
+ if 'href="/' in body or 'src="/' in body:
+ problems.append(f"{page}: 出现以 / 开头的绝对路径(Pages 部署在子路径下会 404)")
+ return problems
+
+
+def check_links(body: str, page: str, known: set[str], assets: set[str]) -> list[str]:
+ problems = []
+ for raw in LINK_ATTR.findall(body):
+ if raw.startswith(("http://", "https://", "mailto:", "//")):
+ continue
+ target = urlsplit(raw)
+ path = target.path
+ if not path:
+ continue # 纯锚点,交给浏览器
+ if path in known or path in assets:
+ continue
+ problems.append(f"{page}: 站内链接指向不存在的文件 —— {raw}")
+ return problems
+
+
+def build(site: dict, output: Path, check_only: bool) -> list[str]:
+ errors: list[str] = []
+ known = {item["page"] for item in pages(site)}
+ asset_dir = ROOT / "assets"
+ assets = {f"assets/{p.name}" for p in asset_dir.iterdir() if p.is_file()} if asset_dir.is_dir() else set()
+
+ content_dir = ROOT / "content"
+ on_disk = {p.name for p in content_dir.glob("*.html")}
+ for orphan in sorted(on_disk - known):
+ errors.append(f"content/{orphan}: 没有出现在 site.json 导航里")
+
+ rendered: dict[str, str] = {}
+ headings_of: dict[str, list[dict]] = {}
+ index: list[dict] = []
+ for item in pages(site):
+ page = item["page"]
+ source = content_dir / page
+ if not source.is_file():
+ errors.append(f"content/{page}: 导航里列了但文件不存在")
+ continue
+ body = source.read_text(encoding="utf-8")
+ ids = collect_ids(body, page)
+ errors.extend(check_structure(body, page))
+ errors.extend(check_links(body, page, known, assets))
+
+ headings = []
+ for level, attrs, text in HEADING.findall(body):
+ match = ID_ATTR.search(attrs)
+ if not match:
+ errors.append(f"{page}: 标题缺少 id —— {strip_tags(text)}")
+ continue
+ headings.append({"level": int(level), "id": match.group(1), "text": strip_tags(text)})
+
+ headings_of[page] = headings
+ rendered[page] = body
+ index.append(
+ {
+ "page": page,
+ "title": item["title"],
+ "group": item["group"],
+ "headings": headings,
+ "text": strip_tags(body)[:1200],
+ }
+ )
+
+ if errors:
+ return errors
+ if check_only:
+ return []
+
+ if output.exists():
+ shutil.rmtree(output)
+ output.mkdir(parents=True)
+
+ for item in pages(site):
+ page = item["page"]
+ sidebar = [' ")
+
+ toc = [' ")
+
+ topnav = [
+ f' {html.escape(group["group"])}'
+ for group in site["nav"]
+ ]
+
+ title = site["title"] if page == "index.html" else f'{item["title"]} · {site["title"]}'
+ document = LAYOUT.substitute(
+ title=html.escape(title),
+ title_suffix="" if page == "index.html" else "",
+ description=html.escape(item.get("summary", site["description"])),
+ canonical=site["base_url"] + page,
+ page=page.removesuffix(".html"),
+ repo=site["repo"],
+ license=site["license"],
+ site=html.escape(site["title"]),
+ subtitle=html.escape(site["subtitle"]),
+ sidebar="\n".join(sidebar),
+ toc="\n".join(toc),
+ topnav="\n".join(topnav),
+ content=rendered[page],
+ version=f'v{html.escape(read_version(site))}' if read_version(site) else "",
+ )
+ (output / page).write_text(document, encoding="utf-8")
+
+ if asset_dir.is_dir():
+ shutil.copytree(asset_dir, output / "assets")
+
+ (output / "search-index.json").write_text(
+ json.dumps(index, ensure_ascii=False, separators=(",", ":")), encoding="utf-8"
+ )
+ (output / "404.html").write_text(
+ LAYOUT.substitute(
+ title=f"页面不存在 · {site['title']}",
+ title_suffix="",
+ description="页面不存在",
+ canonical=site["base_url"],
+ page="404",
+ repo=site["repo"],
+ license=site["license"],
+ site=html.escape(site["title"]),
+ subtitle=html.escape(site["subtitle"]),
+ sidebar=' ',
+ toc=' ',
+ topnav=' 概览',
+ content=' 页面不存在
\n
链接可能已经调整,回到文档首页。
',
+ version="",
+ ),
+ encoding="utf-8",
+ )
+ return []
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="构建 Clashoo 文档站")
+ parser.add_argument("--output", default=str(REPO / ".build/docs-site"), help="输出目录")
+ parser.add_argument("--check", action="store_true", help="只做自检,不写文件")
+ args = parser.parse_args()
+
+ site = read_site()
+ errors = build(site, Path(args.output), args.check)
+ if errors:
+ for line in errors:
+ print(f"错误: {line}", file=sys.stderr)
+ return 1
+ if args.check:
+ print("自检通过")
+ return 0
+ print(f"已生成 {len(pages(site))} 页 → {args.output}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/docs/site/content/index.html b/docs/site/content/index.html
new file mode 100644
index 000000000..757f50b3c
--- /dev/null
+++ b/docs/site/content/index.html
@@ -0,0 +1,46 @@
+
+
+Clashoo
+
+Clashoo 是 OpenWrt 上的代理管理插件:一套 LuCI 界面,管三种内核(mihomo 稳定版 / Alpha、Smart、sing-box),订阅、分流、DNS、透明代理都在图形界面里点完,不用手写配置文件。
+
+支持 OpenWrt 24.10+,推荐 25.x。装完进 服务 → Clashoo,三个页面:概览(启停、内核切换、健康检查)、配置(订阅、配置文件、DNS、Smart 策略)、系统(组件更新、备份还原、日志、定时任务)。
+
+
+
+它管什么
+
+
+ | 能力 | 说明 |
+ | 三内核切换 | mihomo 稳定版 / mihomo Alpha / Smart / sing-box 稳定版 / Alpha,切换不用重装,同一套 UCI 配置两端自动适配 |
+ | 配置管理 | 订阅一键拉取、上传、在线编辑、模板复写(YAML 模板 + 订阅链接);生成后自动 mihomo -t 干跑校验,坏配置不落盘 |
+ | DNS 策略 | Fake-IP / Redir-Host 切换,上游按「默认 / 代理 / 直连 / Fallback」分角色,内置 cn.mrs 域名集,防 DNS 泄漏 |
+ | 透明代理 | TCP 支持 Redirect / TProxy / TUN,UDP 支持 TProxy / TUN,网络栈 gVisor / System / Mixed;局域网 ACL 可按 IPv4 / IPv6 / MAC 分组 |
+ | 系统与数据 | 组件分开更新(内核 / 内核数据 / 插件本体)、备份还原、面板四选一(Zashboard / MetaCubeXD / YACD / Razord) |
+
+
+
+
+
+ | 配置 | 内核 | 典型来源 |
+ | mihomo YAML | Mihomo / Smart | 机场给的 Clash / mihomo 订阅 |
+ | sing-box JSON | Sing-box | 机场给的 sing-box JSON,或由 YAML 转换而来 |
+
+
+sing-box 不能直接吃 mihomo YAML;YAML 转出来的 JSON 也要自己测兼容性。首次使用建议先用 mihomo YAML,兼容性最好。
+
+它不是什么
+
+
+ - 不是订阅转换器,转换只是附带功能,复杂规则还得靠机场或 Tower 这类工具整理。
+ - 不接管内核本体之外的系统代理设置,别和 HomeProxy 等插件同时抢透明代理。
+ - sing-box 内核下载到私有目录,不覆盖系统的
/usr/bin/sing-box,所以可以和 HomeProxy 共存装;共存时把 Clashoo 切到「仅内核」并错开监听端口。
+
+
+从哪里开始
+
+没装过先看安装与升级;装好不知道点哪,看快速开始;起来了但不通,直接跳故障排查。
diff --git a/docs/site/content/install.html b/docs/site/content/install.html
new file mode 100644
index 000000000..6190ef542
--- /dev/null
+++ b/docs/site/content/install.html
@@ -0,0 +1,78 @@
+安装与升级
+
+安装包分两层:clashoo(内核 + 服务脚本 + 默认配置)和 luci-app-clashoo(LuCI 界面 + 中文翻译)。24.10 用 .ipk,25.12 用 .apk,脚本会自己判断。
+
+一键安装
+
+以 root 执行。大陆推荐走 jsDelivr 镜像:
+
+wget -O - https://cdn.jsdelivr.net/gh/kenzok8/openwrt-clashoo@main/scripts/install.sh | ash
+
+镜像还没同步到最新脚本时,直连 GitHub:
+
+wget -O - https://raw.githubusercontent.com/kenzok8/openwrt-clashoo/refs/heads/main/scripts/install.sh | ash
+
+jsDelivr 对 @main 有缓存(最长约 12 小时),刚改过的脚本可能延迟生效。安装包下载默认走 GITHUB_PROXY_PREFIX(默认 https://ghfast.top/),失败自动回落直连。换加速站:GITHUB_PROXY_PREFIX=https://你的加速站/ wget -O - … | ash。
+
+脚本校验什么
+
+
+ - 默认只从签名 feed 装:先比对内置公钥指纹,再用 apk/opkg 校验索引签名与包摘要
+ - 指纹不匹配、验签失败、摘要不符 —— 直接停止,不自动降级
+ - 只有签名 feed 不可达(纯网络原因)时,才可以用
CLASHOO_ALLOW_GITHUB_FALLBACK=1 显式走 GitHub Release 回退;那条路只核对 GitHub 记录的 SHA-256,不是包签名
+
+
+CLASHOO_ALLOW_GITHUB_FALLBACK=1 sh install.sh
+
+公钥脚本自己会导入,不用手工操作。要自己核对指纹:
+
+
+ | 公钥 | 用途 |
+ dllkids-feed.pub.pem | 25.12+ 的 apk 索引签名(ECDSA P-256) |
+ dllkids-feed.pub | 24.10 及更早的 opkg Packages.gz 签名(usign Ed25519) |
+
+
+curl -fsSLO https://down.dllkids.xyz/openwrt-feed/keys/dllkids-feed.pub.pem
+curl -fsSLO https://down.dllkids.xyz/openwrt-feed/keys/dllkids-feed.pub
+sha256sum dllkids-feed.pub dllkids-feed.pub.pem
+
+软件源安装与升级
+
+wget -qO- https://down.dllkids.xyz/openwrt-feed/openwrt-feed-setup.sh | sh
+
+脚本会检测 SDK 版本与架构、下载对应公钥、写源配置、刷索引,最后 apk add / opkg install。之后升级只走系统包管理器:
+
+apk update && apk upgrade # 25.12
+opkg update && opkg upgrade clashoo luci-app-clashoo # 24.10
+
+源码编译
+
+git clone https://github.com/kenzok8/openwrt-clashoo.git package/openwrt-clashoo
+make package/clashoo/compile V=s
+make package/luci-app-clashoo/compile V=s
+
+依赖
+
+
+ | 包名 | 用途 |
+ ca-bundle / curl | 证书与下载(GeoIP、面板、订阅) |
+ yq | YAML 处理 |
+ firewall4 / ip-full | nftables 防火墙与完整 iproute2 |
+ kmod-nft-socket / kmod-nft-tproxy | nftables 的 socket 匹配与 TPROXY |
+ kmod-tun / kmod-dummy | TUN 设备与 dummy 网卡 |
+ kmod-inet-diag | 网络诊断 |
+
+
+卸载
+
+wget -O - https://raw.githubusercontent.com/kenzok8/openwrt-clashoo/refs/heads/main/scripts/uninstall.sh | ash
+
+卸载会停服务、删包。配置留在 /etc/config/clashoo 与配置文件目录,确认不再用之后手动清理。
+
+配套 App
+
+仓库自带 AI 技能 clashoo-user-guide,把功能说明、双内核内幕和排障经验喂给 AI 助手,让它能回答「某功能怎么开」「为什么不通」「日志报错什么意思」:
+
+npx skills add kenzok8/openwrt-clashoo -a '*'
+
+技能里的 references/ 与本仓库 Wiki 同源,由 CI 自动同步。
diff --git a/docs/site/content/kernel.html b/docs/site/content/kernel.html
new file mode 100644
index 000000000..ae584a0ba
--- /dev/null
+++ b/docs/site/content/kernel.html
@@ -0,0 +1,52 @@
+内核与组件更新
+
+Clashoo 把「内核」「内核数据」「插件本体」拆成独立条目,逐个检查、逐个更新 —— 失败了能立刻看出是哪个组件的问题,不用一锅端。
+
+五种内核形态
+
+
+ | 内核 | 配置格式 | 适合 |
+ | mihomo 稳定版 | YAML | 默认选择,普通家庭路由器 |
+ | mihomo Alpha | YAML | 要新特性、能接受偶发兼容问题;普通用户不建议 |
+ | Smart | YAML | 配了 Smart 策略组、要用策略权重 |
+ | sing-box 稳定版 | JSON | 机场给的是 JSON,或想用 sing-box 生态 |
+ | sing-box Alpha | JSON | 同上,尝鲜用 |
+
+
+安装包自带 mihomo 正式版,新装默认就是它。Alpha 需要自己在「系统 → 组件更新」里装;已有设备升级会保留原来的内核选择,不会被悄悄换掉。
+
+没有 Smart 策略时,用 Smart 内核和普通 mihomo 体验差别不大 —— 它的价值在策略组能力,不在内核本身更快。
+
+组件更新
+
+路径:系统 → 组件更新。
+
+
+ - 点「检查更新」拉各组件最新版本
+ - 每个组件单独点「更新」,互不影响
+ - 架构自动识别,也可以手动指定
+ - 内核类(mihomo / Smart / sing-box)收在「高级设置」里,页面不至于太长
+
+
+下载失败的常见原因就三条:路由器连不上 GitHub、镜像源不可用、架构选错。镜像源会按「自定义 → gh-proxy → 直连 GitHub」自动回退。
+
+管理面板
+
+面板四选一:Zashboard / MetaCubeXD / YACD / Razord。面板更新只换 Web 界面,跟代理内核无关 —— 面板打不开别急着重装内核。
+
+GeoIP / GeoSite / 大陆白名单
+
+这三类是分流与 DNS 判断的原料,通常在「组件更新」里一起更新:
+
+
+ | 资源 | 用途 |
+ | GeoIP | 按 IP 归属判断是否直连 |
+ | GeoSite | 按域名归类判断走向 |
+ | 大陆白名单 | 国内域名直连名单,配合 DNS 分流 |
+
+
+sing-box 内核下载到 Clashoo 私有目录,不覆盖系统的 /usr/bin/sing-box,可以和 HomeProxy 同时装着。但两个插件不要同时抢透明代理 —— 要并行就切「仅内核」并错开端口。
+
+更新前先备份
+
+换内核、批量改配置之前,先去 系统 → 备份与还原 导出一份。配置炸了能一键回到那个时间点,具体见系统维护。
diff --git a/docs/site/content/maintenance.html b/docs/site/content/maintenance.html
new file mode 100644
index 000000000..ad7114e48
--- /dev/null
+++ b/docs/site/content/maintenance.html
@@ -0,0 +1,56 @@
+系统维护
+
+系统 页面管四件事:组件更新、备份还原、定时任务、日志。组件更新在内核与组件更新里讲,这页讲剩下的。
+
+备份与还原
+
+位置:组件更新卡片下方,三个按钮。
+
+
+ | 按钮 | 做什么 |
+ | 导出备份 | 把 UCI 设置和订阅 / 上传 / 自定义 / sing-box 配置文件打包成一个 JSON 下载 |
+ | 导入还原 | 选之前导出的备份,一键回到那个时间点 |
+ | 还原默认配置 | 设置重置为出厂默认,订阅和配置文件保留 |
+
+
+备份只含配置与设置,不含内核和 GeoIP 这些大文件 —— 那些在「组件更新」里重新下载就行。
+
+两条经验:更新内核、批量改配置之前先导一份;改坏了先「导入还原」,找不到原因再「还原默认配置」。
+
+定时任务
+
+用来定时刷新资源。值得开的:
+
+
+
+订阅自动更新按需 —— 如果你的订阅是转换类链接,自动更新可能把手工调过的配置覆盖掉,那就关掉它。
+
+日志
+
+日志分插件日志和核心日志,排障时先看核心日志里的 ERROR / FATAL:
+
+系统 → 日志 → 核心日志
+
+存储小的设备建议打开自动清理日志,别让日志把闪存写满。
+
+软件包更新
+
+如果你按软件源安装配了 feed,升级走系统包管理器即可:
+
+apk update && apk upgrade # 25.12
+opkg update && opkg upgrade clashoo luci-app-clashoo # 24.10
+
+用一键脚本装的机器没有把 feed 写进源,想要包管理器升级就先跑一遍 feed 脚本。
+
+彻底重来
+
+
+ - 导出备份(万一还想回退)
+ - 停服务
+ - 卸载:
scripts/uninstall.sh
+ - 确认要清空配置的话,删掉
/etc/config/clashoo 与配置文件目录
+ - 重新安装
+
diff --git a/docs/site/content/proxy.html b/docs/site/content/proxy.html
new file mode 100644
index 000000000..7d4063c7d
--- /dev/null
+++ b/docs/site/content/proxy.html
@@ -0,0 +1,74 @@
+代理模式与 DNS
+
+这两块最容易混:代理模式决定路由器怎么接管流量,DNS 设置决定域名怎么解析。流量不通先分清是哪一层出的问题。
+
+三种代理模式
+
+
+ | 模式 | 怎么接管 | 什么时候用 |
+ | Fake-IP | 给需要代理的域名分一个虚拟 IP(198.18.x.x),内核收到再还原成域名转发 | 默认。兼容性好、配置简单,绝大多数家庭路由器够用 |
+ | TUN | 建虚拟网卡接管流量 | 需要更完整的接管,或某些应用在 Fake-IP 下不听话 |
+ | Mixed | TUN 的另一种网络栈(mixed stack) | System / gVisor 表现不稳、虚拟机环境兼容性差时试它 |
+
+
+TUN 系(含 Mixed)的前提:设备有 /dev/net/tun,容器或 PVE LXC 还要宿主机放行 TUN。
+
+透明代理细分
+
+Clashoo 把 TCP、UDP、网络栈拆开配,不必一个模式全包:
+
+
+ | 项目 | 可选 | 说明 |
+ | TCP | Redirect / TProxy / TUN | Redirect 最省事,TProxy 兼容性好,TUN 接管最彻底 |
+ | UDP | TProxy / TUN | 走代理的 UDP 出问题时,可以只把 UDP 443 拦掉让应用回落 TCP |
+ | 网络栈 | gVisor / System / Mixed | 只有 TCP 或 UDP 选了 TUN 才生效 |
+
+
+另有「禁用 QUIC GSO」开关,个别网卡驱动下 GSO 会导致 UDP 分片异常。
+
+选了 TUN 但设备不支持时,Clashoo 会自动降级到兼容模式继续跑。能上网就说明只是没吃上 TUN,不是配错了。
+
+局域网 ACL
+
+可以按 IPv4、IPv6、MAC 分组,分别控制两件事:是否接管 DNS、是否走代理。想让某台设备(比如电视盒子)绕开代理,或者反过来说「只让这台走代理」,都在这一块。
+
+DNS 的三块拼图
+
+DNS 页面按作用拆成三区,先分清谁管谁:
+
+
+ | 区域 | 管什么 |
+ | 基础 DNS | 内核 DNS 本身怎么工作:接管开关、监听端口、Fake-IP / Redir-Host、是否解析 IPv6、Fake-IP 网段 |
+ | 上游 DNS | 解析域名时去问谁:国内 DNS、国外加密 DNS、节点域名专用 DNS、直连域名 DNS |
+ | 分流 DNS | 什么域名问哪个上游:默认 geosite:cn 走国内,geosite:geolocation-!cn 走国外 |
+
+
+默认值基本可以直接用,关键的几项:
+
+
+ | 设置 | 默认 | 说明 |
+ | DNS 监听端口 | 1053 | dnsmasq / 防火墙把请求转到这里,不冲突就别改 |
+ | 增强模式 | fake-ip | 推荐保留 |
+ | Fake-IP 网段 | 198.18.0.1/16 | 测试网段,不与公网冲突 |
+ | Fake-IP 过滤域名 | *.lan、rule-set:cn_domain 等 | 国内域名走真实 IP 直连;内置 cn.mrs,不用加载 10MB 的 geosite.dat,启动更快 |
+ | Bootstrap DNS | 223.5.5.5、119.29.29.29 | 用来解析 DoH / DoT 服务器本身的域名,要稳定 |
+ | ECS 客户端子网 | 留空 | 留空最省心;填错会拿到不合适的 CDN 节点 |
+
+
+IPv6 的两个开关别搞反
+
+
+ | 基础 DNS → IPv6 解析 | 透明代理 → IPv6 DNS 劫持 |
+ | 作用对象 | Clashoo 内核自己解析域名时查不查 AAAA | 局域网设备发出的 IPv6 DNS 请求要不要被接管 |
+ | 关掉的后果 | 内核基本只拿 IPv4 地址 | 设备可能绕过 Clashoo,分流规则失效、DNS 泄漏 |
+
+
+即使不用 IPv6 代理,也建议保留 IPv6 DNS 劫持。运营商下发的 IPv6 DNS 一旦绕过 Clashoo,分流和 Fake-IP 都会失灵。
+
+防泄漏
+
+
+ - 阻止国内 DNS 解析国外域名
+ - 阻断 DoT / DoQ,避免应用自带加密 DNS 绕过分流
+ - 节点域名单独指定 DNS,防止节点域名被规则误分流
+
diff --git a/docs/site/content/quickstart.html b/docs/site/content/quickstart.html
new file mode 100644
index 000000000..0560c832a
--- /dev/null
+++ b/docs/site/content/quickstart.html
@@ -0,0 +1,62 @@
+快速开始
+
+目标:十分钟内让路由器代理起来。装好插件后进 服务 → Clashoo,菜单没出现就刷新浏览器缓存或重启 LuCI / rpcd。
+
+1. 准备配置
+
+先确认手上有哪种配置:机场给的 Clash / mihomo 订阅(YAML),还是 sing-box JSON。拿不准就先用 YAML —— 兼容性最好。
+
+2. 导入配置
+
+路径:配置 → 配置文件。四种常见做法:
+
+
+ - 粘贴订阅链接下载
+ - 上传本地配置文件
+ - 把 YAML 转换成 sing-box JSON
+ - 直接导入机场提供的 sing-box JSON
+
+
+导入后在配置文件列表里点 切换,让这份配置生效。生成类订阅会自动跑 mihomo -t 干跑校验,配置有问题会写不进去。
+
+3. 选内核
+
+路径:概览 → 内核切换。三个选项对应三种配置:
+
+
+ | 选择 | 什么时候用 |
+ | Mihomo | YAML 订阅,普通家庭路由器,默认选这个 |
+ | Smart | 配了 Smart 策略组、要用策略权重时 |
+ | Sing-box | 手上是 JSON 配置时 |
+
+
+切换内核后 Clashoo 会按当前配置重新准备运行环境,不用重装包。
+
+4. 选代理模式
+
+概览底部的运行模式下拉框,含义见代理模式与 DNS。一句话:不确定就 Fake-IP;设备有 /dev/net/tun 且想更完整接管流量,再试 TUN 或 Mixed。
+
+5. 启动
+
+概览页打开 启用服务 开关。然后看三处:
+
+
+ | 看什么 | 正常表现 |
+ | 运行状态 | 显示运行中 |
+ | 健康检查 | DNS / 显式代理 / 透明代理三项通过 |
+ | 访问检查 | 字节跳动与 YouTube 都能出延迟分档(<400ms 绿 / <800ms 黄 / 更高红) |
+
+
+再看眼流量监控有没有上下行与活跃连接变化 —— 有数字说明流量真的过内核了。
+
+6. DNS 要不要动
+
+默认配置能跑就先别动。确实要按域名分流、或者遇到 DNS 泄漏,再看代理模式与 DNS 里的 DNS 三块拼图。
+
+下一步
+
+
diff --git a/docs/site/content/troubleshooting.html b/docs/site/content/troubleshooting.html
new file mode 100644
index 000000000..dfe42e09b
--- /dev/null
+++ b/docs/site/content/troubleshooting.html
@@ -0,0 +1,85 @@
+故障排查
+
+下手之前先固定三个事实,不然后面全是猜:
+
+
+ - 当前用哪个内核(Mihomo / Smart / Sing-box)
+ - 当前生效的是哪份配置文件
+ - 当前运行模式是 Fake-IP、TUN 还是 Mixed
+
+
+健康检查不通过
+
+概览页的健康检查分 DNS、显式代理、透明代理三项。哪项红就从哪项查起:
+
+
+ - DNS 不响应 —— 看 DNS 监听端口有没有被占、上游 DNS 是否可达
+ - 显式代理端口不通 —— 内核没起来,或者端口被别的插件占了
+ - 透明代理规则没生效 —— nftables 规则没写上,重启服务再看
+ - 也可能是节点本身挂了,或配置与当前内核不匹配
+
+
+顺序:看核心日志 → 切回 Fake-IP 试 → 换个已知可用的节点或配置 → 重启服务。
+
+国内通、国外不通
+
+
+ - 节点不可用,或策略组选到了死节点
+ - DNS 能解析但出站失败 —— 先测显式代理,再测透明代理
+ - sing-box JSON 转换后节点协议不兼容
+
+
+国外通、国内不通
+
+
+ - 国内域名被错误代理:检查规则与策略组
+ - 大陆白名单或 GeoIP 过期
+ - DNS 上游不可用,直连域名解析不出来
+
+
+先更新白名单与 GeoIP,再不行就临时切回默认 DNS 策略排除变量。
+
+TUN / Mixed 不通
+
+ls -l /dev/net/tun
+
+没有这个设备就别折腾 TUN,用 Fake-IP。容器、PVE LXC 还要宿主机放行 TUN。概览里显示「降级运行」就是这个意思:想用 TUN,环境不允许,自动退回兼容模式。
+
+sing-box 起不来
+
+
+ - JSON 格式错误 —— 用配置编辑页看是否已格式化
+ - YAML 转出来的字段不兼容,缺必需项
+ - 入站端口与其它插件冲突
+ - TUN 与
routing_mark 这类字段冲突
+
+
+先切回 Fake-IP 排除模式因素,再看核心日志里的 FATAL。
+
+内核下载失败
+
+三条原因占绝大多数:GitHub / 镜像不可达、架构选错、上游 Release 资产改名。换镜像源、核对架构先试。
+
+面板打不开
+
+
+ - 服务没在跑
+ - 面板端口没监听,或配额与其它服务撞了
+ - 面板文件没下下来,重新更新面板
+ - 浏览器缓存旧资源,硬刷新一次
+
+
+停不掉服务
+
+异步任务还在跑、内核进程卡住、更新任务占着锁,都会让停止按钮看起来没反应。等几秒刷新状态,或看插件日志;别连着猛点启停。
+
+配置改坏了
+
+去 系统 → 备份与还原:
+
+
+ - 导出过备份 —— 「导入还原」回到那一份
+ - 没备份也找不到原因 —— 「还原默认配置」重置设置,订阅与配置文件保留
+
+
+具体按钮含义见系统维护。
diff --git a/docs/site/site.json b/docs/site/site.json
new file mode 100644
index 000000000..db1c26479
--- /dev/null
+++ b/docs/site/site.json
@@ -0,0 +1,33 @@
+{
+ "title": "Clashoo",
+ "subtitle": "OpenWrt 双内核代理插件",
+ "description": "luci-app-clashoo:mihomo / Smart / sing-box 三内核,一套 LuCI 界面管理订阅、分流、DNS 与透明代理。",
+ "base_url": "https://kenzok8.github.io/openwrt-clashoo/",
+ "repo": "kenzok8/openwrt-clashoo",
+ "license": "GPL-3.0",
+ "version_file": "luci-app-clashoo/Makefile",
+ "nav": [
+ {
+ "group": "开始使用",
+ "items": [
+ { "page": "index.html", "title": "概览" },
+ { "page": "install.html", "title": "安装与升级" },
+ { "page": "quickstart.html", "title": "快速开始" }
+ ]
+ },
+ {
+ "group": "日常使用",
+ "items": [
+ { "page": "proxy.html", "title": "代理模式与 DNS" },
+ { "page": "kernel.html", "title": "内核与组件更新" },
+ { "page": "maintenance.html", "title": "系统维护" }
+ ]
+ },
+ {
+ "group": "参考",
+ "items": [
+ { "page": "troubleshooting.html", "title": "故障排查" }
+ ]
+ }
+ ]
+}
diff --git a/ks-cli/Makefile b/ks-cli/Makefile
index 9da682411..0edd76959 100644
--- a/ks-cli/Makefile
+++ b/ks-cli/Makefile
@@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_KS_CLI:=$(ARCH)
PKG_NAME:=ks-cli
-PKG_VERSION:=0.8.5
-PKG_RELEASE:=3
+PKG_VERSION:=0.8.7
+PKG_RELEASE:=4
PKG_SOURCE:=ks-cli-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$(PKG_VERSION)/
diff --git a/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js b/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
index 780f817ac..867c64c04 100644
--- a/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
+++ b/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
@@ -172,6 +172,17 @@ return view.extend({
o.retain = true;
o.depends('offloading_type', '2');
}
+
+ if (runtimeStatus?.ppe_bind_rate_available === true) {
+ o = s.option(form.Value, 'ppe_bind_rate', _('PPE bind rate threshold'),
+ _('Packets per second required for hardware flow binding. Applies to both L2B and L3 on all active PPE engines. Default: 30 pps.'));
+ o.datatype = 'and(uinteger,range(0,65535))';
+ o.default = '30';
+ o.placeholder = '30';
+ o.rmempty = false;
+ o.retain = true;
+ o.depends('offloading_type', '2');
+ }
}
diff --git a/luci-app-firewall/po/zh_Hans/firewall.po b/luci-app-firewall/po/zh_Hans/firewall.po
index 00c2cd10d..5e575dd7f 100644
--- a/luci-app-firewall/po/zh_Hans/firewall.po
+++ b/luci-app-firewall/po/zh_Hans/firewall.po
@@ -1612,3 +1612,9 @@ msgstr "Qualcomm NSS ECM 加速"
#: applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js
msgid "Automatically select the acceleration engine in ECM Auto mode."
msgstr "在 ECM Auto 模式下自动选择加速引擎。"
+
+msgid "PPE bind rate threshold"
+msgstr "PPE 绑定阈值"
+
+msgid "Packets per second required for hardware flow binding. Applies to both L2B and L3 on all active PPE engines. Default: 30 pps."
+msgstr "触发硬件流绑定所需的每秒包数,同时应用于所有已启用 PPE 引擎的 L2B 和 L3。默认:30 pps。"
diff --git a/luci-app-firewall/root/usr/libexec/luci-firewall-status b/luci-app-firewall/root/usr/libexec/luci-firewall-status
index 9368ecd0b..eab4923c3 100755
--- a/luci-app-firewall/root/usr/libexec/luci-firewall-status
+++ b/luci-app-firewall/root/usr/libexec/luci-firewall-status
@@ -8,6 +8,14 @@ ecm_available=false
[ -x /etc/init.d/qca-nss-ecm ] && [ -f /etc/config/ecm ] &&
[ -f /usr/share/ucitrack/qca-nss-ecm.json ] && ecm_available=true
+ppe_bind_rate_available=false
+if [ -f /etc/openwrt_release ]; then
+ . /etc/openwrt_release
+ case "$DISTRIB_TARGET" in
+ airoha/*) [ ! -e /sys/kernel/debug/ppe/bind_rate ] || ppe_bind_rate_available=true ;;
+ esac
+fi
+
rules="$(iptables-save -t nat 2>/dev/null)"
if printf '%s\n' "$rules" | grep -q -- '--mode fullcone'; then
@@ -24,4 +32,4 @@ else
fi
fi
-printf '{"nat_mode":"%s","tcp_cca":"%s","hnat_available":%s,"ecm_available":%s}\n' "$nat_mode" "$tcp_cca" "$hnat_available" "$ecm_available"
+printf '{"nat_mode":"%s","tcp_cca":"%s","hnat_available":%s,"ecm_available":%s,"ppe_bind_rate_available":%s}\n' "$nat_mode" "$tcp_cca" "$hnat_available" "$ecm_available" "$ppe_bind_rate_available"
diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile
index e757c2e62..9ccdc5090 100644
--- a/luci-app-open-box/Makefile
+++ b/luci-app-open-box/Makefile
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box
-PKG_VERSION:=0.1.407
-PKG_RELEASE:=65
+PKG_VERSION:=0.1.411
+PKG_RELEASE:=66
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile
index 0712fa486..1f62f0736 100644
--- a/luci-app-passwall/Makefile
+++ b/luci-app-passwall/Makefile
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.11.3
-PKG_RELEASE:=366
+PKG_RELEASE:=367
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm b/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm
index 305f04577..aeb4e7f9f 100644
--- a/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm
+++ b/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm
@@ -39,9 +39,15 @@ table td, .table .td {
#set_node_div {
display: none;
- width: 30rem;
+ width: 90%;
+ max-width: 30rem;
+ max-height: 90vh;
+ box-sizing: border-box;
+ overflow-y: auto;
position: fixed;
- top:45%;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
z-index: 99;
text-align: center;
background: white;
@@ -273,15 +279,16 @@ table td, .table .td {