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 + + + + + + + + + +
+ + + $site$subtitle + + +
+ + GitHub + + +
+
+
+ +
+$content + +
+ +
+ + + +""" +) + + +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

+ +

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 YAMLMihomo / Smart机场给的 Clash / mihomo 订阅
sing-box JSONSing-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.pem25.12+ 的 apk 索引签名(ECDSA P-256)
dllkids-feed.pub24.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、面板、订阅)
yqYAML 处理
firewall4 / ip-fullnftables 防火墙与完整 iproute2
kmod-nft-socket / kmod-nft-tproxynftables 的 socket 匹配与 TPROXY
kmod-tun / kmod-dummyTUN 设备与 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 AlphaYAML要新特性、能接受偶发兼容问题;普通用户不建议
SmartYAML配了 Smart 策略组、要用策略权重
sing-box 稳定版JSON机场给的是 JSON,或想用 sing-box 生态
sing-box AlphaJSON同上,尝鲜用
+ +

安装包自带 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 这些大文件 —— 那些在「组件更新」里重新下载就行。
+ +

两条经验:更新内核、批量改配置之前先导一份;改坏了先「导入还原」,找不到原因再「还原默认配置」。

+ +

定时任务

+ +

用来定时刷新资源。值得开的:

+ +
    +
  • 大陆白名单更新
  • +
  • GeoIP 更新
  • +
+ +

订阅自动更新按需 —— 如果你的订阅是转换类链接,自动更新可能把手工调过的配置覆盖掉,那就关掉它。

+ +

日志

+ +

日志分插件日志和核心日志,排障时先看核心日志里的 ERROR / FATAL:

+ +
系统 → 日志 → 核心日志
+ +

存储小的设备建议打开自动清理日志,别让日志把闪存写满。

+ +

软件包更新

+ +

如果你按软件源安装配了 feed,升级走系统包管理器即可:

+ +
apk update && apk upgrade                            # 25.12
+opkg update && opkg upgrade clashoo luci-app-clashoo  # 24.10
+ +

用一键脚本装的机器没有把 feed 写进源,想要包管理器升级就先跑一遍 feed 脚本。

+ +

彻底重来

+ +
    +
  1. 导出备份(万一还想回退)
  2. +
  3. 停服务
  4. +
  5. 卸载:scripts/uninstall.sh
  6. +
  7. 确认要清空配置的话,删掉 /etc/config/clashoo 与配置文件目录
  8. +
  9. 重新安装
  10. +
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 下不听话
MixedTUN 的另一种网络栈(mixed stack)System / gVisor 表现不稳、虚拟机环境兼容性差时试它
+ +

TUN 系(含 Mixed)的前提:设备有 /dev/net/tun,容器或 PVE LXC 还要宿主机放行 TUN。

+ +

透明代理细分

+ +

Clashoo 把 TCP、UDP、网络栈拆开配,不必一个模式全包:

+ + + + + + +
项目可选说明
TCPRedirect / TProxy / TUNRedirect 最省事,TProxy 兼容性好,TUN 接管最彻底
UDPTProxy / 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 监听端口1053dnsmasq / 防火墙把请求转到这里,不冲突就别改
增强模式fake-ip推荐保留
Fake-IP 网段198.18.0.1/16测试网段,不与公网冲突
Fake-IP 过滤域名*.lan、rule-set:cn_domain 等国内域名走真实 IP 直连;内置 cn.mrs,不用加载 10MB 的 geosite.dat,启动更快
Bootstrap DNS223.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. 选内核

+ +

路径:概览 → 内核切换。三个选项对应三种配置:

+ + + + + + +
选择什么时候用
MihomoYAML 订阅,普通家庭路由器,默认选这个
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 @@ +

故障排查

+ +

下手之前先固定三个事实,不然后面全是猜:

+ +
    +
  1. 当前用哪个内核(Mihomo / Smart / Sing-box)
  2. +
  3. 当前生效的是哪份配置文件
  4. +
  5. 当前运行模式是 Fake-IP、TUN 还是 Mixed
  6. +
+ +

健康检查不通过

+ +

概览页的健康检查分 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 {