From bce4ed5b399aba16c42014c72dd7853b7a28acf7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 09:08:11 +0000 Subject: [PATCH 01/28] feat(clash): add Stash override for the mihomo config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapts Clash/Sample.yaml for the Stash client, based on the official Stash docs (config sample, DNS page, QUIC page, MRS support note). Two things the override actually fixes: - DNS: mihomo expresses "resolve through the proxy rules" as a per-nameserver "#RULES" URL suffix, which Stash does not have — its "#" fragment only carries options like "#h3=true". Stash's equivalent is the global dns.follow-rule switch, so the override sets it and replaces the nameserver array (via the #!replace marker) to drop the invalid suffix. The config already sets proxy-server-nameserver, which satisfies one of the three preconditions the docs require before enabling follow-rule. - nameserver-policy: General.yaml joins the NTP domains into a single comma-separated key (mihomo-only). Stash keys are exact / wildcard / geosite: only, so that key would be read as one literal domain and never match; the override re-adds them as separate keys. - QUIC: replaces the mihomo logic rule (AND/NOT/OR over NETWORK+DST-PORT+ GEOSITE/GEOIP) with Stash's documented script shortcut + SCRIPT rule. Documented in the file rather than silently fixed, because an override cannot express them: override rules are PREPENDED, so the QUIC SCRIPT rule cannot sit after the CN rules and therefore also rejects domestic UDP:443 (falls back to TCP); rule-providers carry a mihomo-only "format:" key whose handling by Stash needs a load test (the 8 .mrs sets are all domain/ipcidr, which Stash supports); proxy-groups "filter" is undocumented for Stash, which would leave the five region groups populated with every node rather than empty; and Mihomo.yaml should not be fed to Stash at all (YAML anchors, merge keys, a non-schema anchors: key, include-all-providers). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- Clash/Stash.stoverride | 120 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 Clash/Stash.stoverride diff --git a/Clash/Stash.stoverride b/Clash/Stash.stoverride new file mode 100644 index 000000000..f16e63ac2 --- /dev/null +++ b/Clash/Stash.stoverride @@ -0,0 +1,120 @@ +name: HotKids Stash 适配 +desc: 将 HotKids/Rules 的 mihomo 配置适配到 Stash:DNS 跟随规则改用 follow-rule、QUIC 拦截改用 Script Shortcuts。配合 Clash/Sample.yaml 使用。 + +# ============================================================================ +# 覆写合并规则(Stash 官方): +# 标量(string/number/bool)同键 → 直接覆盖 +# 字典同键 → 按键递归合并 +# 数组同键 → 【覆写文件的数组插入到原数组的开头】 +# 键后加 `#!replace` 行注释 → 改为整体替换而非合并 +# 因此本文件中的 rules 会被前置到 Sample.yaml 规则列表最前面,顺序敏感项见下方说明。 +# ============================================================================ + + +# ── DNS ────────────────────────────────────────────────────────────────── +# +# 背景:Sample.yaml / General.yaml 用的是 mihomo 的「每条 nameserver 级」跟随规则语法 +# nameserver: ["https://1.1.1.1/dns-query#RULES"] +# Stash 没有这个后缀语义——它的 `#` 片段只用于 DNS 选项(如 #h3=true),`#RULES` +# 不是合法选项。Stash 的等价物是全局布尔开关 dns.follow-rule。 +# +# 官方警告:DNS 经代理转发可能破坏云服务商 CDN 全球优化(静态资源变慢),并轻微增加延迟, +# 「绝大部分场景下不需要开启」。同时存在递归查询问题,需满足以下之一: +# - 转发 DNS 的代理地址是 IP 而非域名 +# - DNS 服务器地址是 IP 而非域名 +# - 用 proxy-server-nameserver 为代理服务器域名配置独立解析 ← 我们满足这条 +# +# 若你更认同官方建议(DNS 直连、把 CDN 优化留给运营商),把 follow-rule 改成 false 即可, +# 其余配置无需改动。 +dns: + follow-rule: true + + # 必须整体替换:原数组里的 "https://1.1.1.1/dns-query#RULES" 在 Stash 下无效, + # 若用默认的「前置合并」它会被保留下来。 + # 保持与原配置一致的单上游,仅去掉 Stash 无法识别的 #RULES 后缀。 + # 如需冗余可再加一条(官方建议不超过 2 条,多了增加系统功耗)。 + nameserver: #!replace + - 'https://1.1.1.1/dns-query' + + # 解析代理服务器域名的独立链路,不跟随代理规则——这既是我们原本的设计, + # 也是上面开启 follow-rule 的前置条件。(iOS/tvOS 3.6+,macOS 4.3+) + proxy-server-nameserver: + - 223.5.5.5 + - https://doh.pub/dns-query + - https://dns.alidns.com/dns-query + + # Stash 的 nameserver-policy 键只支持「精确域名 / 通配域名 / geosite:」三种形式。 + # General.yaml 里 NTP 那条把多个域名用逗号拼成单个键(mihomo 专属写法),在 Stash 下 + # 会被当成一个字面域名、永远匹配不上。这里按 Stash 语法拆成独立键补回。 + # (字典递归合并,原有的 geosite:private / geosite:cn 条目保持不变。) + nameserver-policy: + 'time.*.com': [223.5.5.5, 119.29.29.29] + 'time.*.gov': [223.5.5.5, 119.29.29.29] + 'time.*.apple.com': [223.5.5.5, 119.29.29.29] + 'ntp.*.com': [223.5.5.5, 119.29.29.29] + '+.pool.ntp.org': [223.5.5.5, 119.29.29.29] + '*.ntp.org.cn': [223.5.5.5, 119.29.29.29] + + +# ── QUIC 拦截 ──────────────────────────────────────────────────────────── +# +# 背景:Sample.yaml 用 mihomo 逻辑规则表达「境外 QUIC 拦截、国内放行」: +# AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT +# Stash 的官方做法是 Script Shortcuts + SCRIPT 规则。 +# +# ⚠️ 语义差异(务必知悉): +# Stash 文档的示例靠「规则顺序」实现国内放行——把 GEOIP,CN,DIRECT 排在 SCRIPT 规则之前。 +# 但覆写文件的 rules 数组是【前置】到原列表最前面的,所以这里的 SCRIPT 规则一定跑在 +# Sample.yaml 的所有规则之前,无法排到 CN 规则之后。 +# 结果:国内 UDP:443 也会被拒绝,回退 TCP。功能不受影响(QUIC 本就设计为可回退), +# 但国内 QUIC 的性能收益会失去——这是覆写机制的结构性限制,不是配置疏漏。 +# +# 若要完全保留「国内放行」,两条路: +# 1) 直接改 Sample.yaml 的规则顺序(不用覆写); +# 2) 若 Stash 的表达式支持目标 IP 的地理判断,可把条件写进 shortcut—— +# 文档未列出可用变量清单,需自行验证,故此处不臆造语法。 +script: + shortcuts: + # 文档示例用的是 dst_port == 443;Stash 自带示例还额外覆盖了 4483 / 9305 + # (部分 QUIC 变体端口),如需更激进的拦截可改成: + # network == 'udp' and (dst_port == 443 or dst_port == 4483 or dst_port == 9305) + quic: network == 'udp' and dst_port == 443 + +rules: + # no-track:不记录到连接日志,避免高频 UDP 拒绝刷屏 + - SCRIPT,quic,REJECT,no-track + + +# ============================================================================ +# 覆写解决不了、需要你确认或改动源配置的问题 +# ============================================================================ +# +# 1) rule-providers 的 `format:` 键【最高优先级待验证】 +# Stash 支持 MRS 规则集,behavior 限 domain 与 ipcidr——我们那 8 个 mrs 规则集 +# (Private / Reject / Phishing / Speedtest / Global / China 为 domain, +# China IP / LAN 为 ipcidr)正好全部落在支持范围内。 +# 但 Stash 用 behavior 取值区分格式(domain / domain-text / ipcidr / ipcidr-text / +# classical),文档中没有出现 mihomo 那个独立的 `format:` 键。我们 30 个 provider +# 每个都写了 format。若 Stash 忽略 format 而按 URL 或 behavior 推断,则 mrs 可能被 +# 当成文本解析而失败。请在 Stash 里实际加载一次,确认这 8 个规则集能正常生效。 +# (22 个 classical + format: yaml 的规则集风险低——Stash 的 classical 示例用的 +# 就是 .yaml 源,多余的 format 键忽略即可;官方仅提示 classical 解析效率较低。) +# +# 2) proxy-groups 的 `filter` +# 5 个地区组(🇭🇰 / 🇨🇳 / 🇸🇬 / 🇯🇵 / 🇺🇸)同时写了 use: [Server] 和 filter 正则, +# 靠 filter 从订阅里筛出对应地区节点。Stash 文档的组字段里没出现 filter。 +# 若不支持:这些组不会变空,而是会塞进 Server 里的【全部】节点——每个地区组都一样, +# 地区选择失效。覆写无法修复(要修就得把节点名写死)。请实测确认。 +# +# 3) 其余 mihomo 专属键 +# tun / sniffer 整块、dns 的 cache-algorithm(Stash 固定 LRU)、prefer-h3、 +# respect-rules、fake-ip-ttl / fake-ip-range6 / fake-ip-filter-mode、 +# direct-nameserver 系列,以及 unified-delay / tcp-concurrent / geodata-loader / +# global-ua / keep-alive-interval / ntp 等通用项,在 Stash 文档中均未出现。 +# 覆写只能增改、不能删键,所以这些原样保留;预期被忽略(Stash 作为 iOS/macOS +# VPN 扩展运行,tun 这类内核参数本就不由用户配置)。若 Stash 对未知顶层键 +# 严格校验导致加载失败,需要改的是源配置而非本文件。 +# +# 4) Mihomo.yaml 不要直接喂给 Stash +# 它依赖 YAML 锚点与 `<<:` 合并键,还有一个非 schema 的顶层 anchors: 键, +# 且用 include-all-providers 替代 use。请使用 Sample.yaml 作为 Stash 的基础配置。 From 9a07f2092dac024d30f028d7d68f89a1abd7dd96 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 09:12:48 +0000 Subject: [PATCH 02/28] feat(sync): generate Clash/Stash.stoverride from the Clash config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Stash override is now derived by sync-config.py instead of hand-written, so it tracks the source config automatically. It emits ONLY the deltas between Stash and mihomo — everything else is inherited from Clash/Sample.yaml: - dns.follow-rule: mihomo spells "resolve through the proxy rules" as a per-nameserver "#RULES" URL suffix; Stash has no such fragment (its "#" only carries options like h3=true) and uses a global follow-rule switch instead. The nameserver array is re-emitted under the #!replace marker so the invalid suffix is dropped rather than merged in. - nameserver-policy: General.yaml joins the NTP domains into one comma-separated key (mihomo-only). Stash keys are exact / wildcard / geosite: only, so that key would never match; it is split into separate keys. - QUIC: the AND/NOT/OR logic rule becomes Stash's script.shortcuts + SCRIPT rule. Policy name and ports are extracted from the source rule, not hardcoded. Override arrays are PREPENDED by Stash, which drives two design points: proxy-server-nameserver is emitted only when the base config lacks it (copying it verbatim would duplicate the entries), and the QUIC rule necessarily matches before the CN rules — so domestic UDP:443 is rejected too and falls back to TCP. That limitation is documented in the generated file rather than papered over. Declared as a "# Stash" platform block in sync-config.txt, added to the workflow's git add list (it would otherwise never be committed), and documented in both READMEs. Verified locally: generation is idempotent, output parses as YAML, and the #!replace marker survives. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/README.md | 12 ++- .github/scripts/sync-config.py | 160 ++++++++++++++++++++++++++++++ .github/scripts/sync-config.txt | 5 + .github/workflows/sync-config.yml | 4 +- Clash/Stash.stoverride | 114 ++++----------------- README.md | 2 +- 6 files changed, 196 insertions(+), 101 deletions(-) diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 6c8d7939a..80bf17873 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -55,7 +55,7 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do ## `sync-config.py` — 配置文件同步 **源**:`Surge/Profile.conf` -**目标**:`Clash/Sample.yaml`、`Clash/Mihomo.yaml`、`Clash/Script/Script.js`、`Clash/Script/MyScript.js`、`Clash/Script/MyScriptColor.js`、`Clash/Script/MyClashBox.js`、`Surge/Balloon.lcf`(Loon)、`Quantumult/Sample.conf`、`Surge/Surfboard.conf`、`sing-box/config.json` +**目标**:`Clash/Sample.yaml`、`Clash/Mihomo.yaml`、`Clash/Stash.stoverride`、`Clash/Script/Script.js`、`Clash/Script/MyScript.js`、`Clash/Script/MyScriptColor.js`、`Clash/Script/MyClashBox.js`、`Surge/Balloon.lcf`(Loon)、`Quantumult/Sample.conf`、`Surge/Surfboard.conf`、`sing-box/config.json` 各平台静态头部由 `sync-config/` 下的 ini 文件提供(支持 `<< path` / `<< https://url` 引用)。sing-box 完整配置以 `sync-config/sing-box.ini`(JSON 内容)为静态基座——仅保留 `sniff`/`hijack-dns`(sing-box 专属基础设施,Surge 无等价规则);`route.rules`/`route.rule_set` 其余全部(含 QUIC 拦截、SSH 直连、私有网络、CN/geo、各服务分流)从 `[Rule]` 生成后 splice 进哨兵位——自有清单用本仓库 `.srs`,Loyalsoldier/VirgilClyne 等外部规则集映射到 SagerNet 官方等价规则集。 @@ -65,6 +65,16 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do `<<: *Region, filter: *Filter<码>`(`include-all-providers` 与 `use:` 同走 mihomo 保序路径,功能一致)。 +`Clash/Stash.stoverride` 是 `Clash/Sample.yaml` 生成完毕后推导出的 Stash 覆写文件, +**只包含 Stash 与 mihomo 的差异项**,其余全部沿用 Sample.yaml,不重复输出等价配置: +mihomo 每条 nameserver 的 `#RULES` 后缀 → Stash 的全局 `dns.follow-rule`(并用 +`#!replace` 整体替换 nameserver 数组以去掉 Stash 无法识别的后缀);`nameserver-policy` +里逗号拼接的多域名单键(mihomo 专属)→ 拆成 Stash 认的独立键;`[Rule]` 里的 QUIC 逻辑 +规则 `AND,((NETWORK,UDP),(DST-PORT,…),(NOT,…))` → Stash 的 `script.shortcuts` + +`SCRIPT,quic,<策略>,no-track`(策略名与端口都从源规则提取)。差异项为空时对应段落不输出。 +注意 Stash 覆写的数组是**前置插入**,所以只在基础配置缺失时才补 `proxy-server-nameserver`, +避免条目变双份;也因此 QUIC 规则必然先于 CN 规则命中(见文件内注释说明的语义差异)。 + `Clash/Script/Script.js` 是 `Clash/Mihomo.yaml` 生成完毕后再解析出来的等效 mihomo 覆写 脚本(Enhance Script),供 Clash Verge Rev / FlClash / Bettbox 等客户端直接对任意订阅动态生成同一套策略组 / 规则 / 基础设置,无需依赖本仓库自身的 proxy-providers。它只读 Mihomo.yaml 的解析结果、 diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 85c7ec71a..317d5a1e6 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4111,6 +4111,165 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - print(f" {'✓ sing-box/config.json 已更新' if changed else '✓ sing-box/config.json 无变化'}") +# --------------------------------------------------------------------------- +# Stash 覆写(.stoverride) +# --------------------------------------------------------------------------- +# +# Stash 用「覆写文件」在基础配置之上打补丁,合并规则(官方): +# 标量同键 → 直接覆盖;字典同键 → 按键递归合并; +# 数组同键 → 覆写的数组【插入到原数组开头】; +# 键后加 `#!replace` 行注释 → 改为整体替换。 +# 因此本文件只产出「Stash 与 mihomo 的差异项」,其余全部沿用 Clash/Sample.yaml, +# 不重复输出任何等价配置。所有差异项均从生成好的 Clash 配置推导,源改了这里自动跟随。 + +# mihomo 的 nameserver 策略后缀(#RULES / #策略名)在 Stash 中不存在——Stash 的 `#` +# 片段只承载选项(如 h3=true)。保留 h3= 这类合法选项,其余后缀一律剥离。 +def _stash_clean_nameserver(server: str) -> str: + if "#" not in server: + return server + base, frag = server.split("#", 1) + return server if frag.startswith("h3=") else base + + +# 从 mihomo 逻辑规则中识别 QUIC 拦截规则,取出目标策略与被拦截端口。 +# 形如:AND,((NETWORK,UDP),(DST-PORT,443),(NOT,(...))),⛔️ REJECT +_QUIC_RULE_RE = re.compile(r"^AND,\(\(.*\)\),(?P.+)$") + + +def _stash_quic_from_rules(rules: list) -> tuple[str, list[str]] | None: + for rule in rules: + if not isinstance(rule, str) or not rule.startswith("AND,"): + continue + if "NETWORK,UDP" not in rule: + continue + ports = re.findall(r"\(DST-PORT,(\d+)\)", rule) + if not ports: + continue + m = _QUIC_RULE_RE.match(rule) + if m: + return m.group("policy").strip(), ports + return None + + +def _sync_stash(config: dict) -> None: + """生成 Stash 覆写:只输出与 mihomo 的差异项,从 Clash 产物推导。""" + out = config.get("Stash", {}).get("output") + clash_out = config.get("Clash", {}).get("output") + if not out or not clash_out: + return + base_path = REPO_ROOT / clash_out + if not base_path.exists(): + return + + print("\n── sync-config: Clash Sample.yaml → Stash .stoverride ──") + cfg = yaml.safe_load(base_path.read_text(encoding="utf-8")) or {} + dns = cfg.get("dns", {}) or {} + + lines: list[str] = [ + "name: HotKids Stash 适配", + "desc: Stash 与 mihomo 的差异补丁,基础配置请用 Clash/Sample.yaml。由 sync-config.py 自动生成,勿手改。", + "# Date: ", + "", + "# 覆写合并:标量覆盖 / 字典递归合并 / 数组前置插入;键后 `#!replace` 改为整体替换。", + "# 本文件只含差异项,其余沿用基础配置。", + "", + ] + + # ── DNS 差异 ── + ns_raw = [s for s in (dns.get("nameserver") or []) if isinstance(s, str)] + follow_rule = any("#RULES" in s for s in ns_raw) + ns_clean = [_stash_clean_nameserver(s) for s in ns_raw] + + # 逗号拼接多域名的 nameserver-policy 键是 mihomo 专属;Stash 只认 + # 「精确域名 / 通配域名 / geosite:」,拼接键会被当成字面域名永不命中。 + split_policy: dict[str, object] = {} + for key, val in (dns.get("nameserver-policy") or {}).items(): + if isinstance(key, str) and "," in key: + for one in (x.strip() for x in key.split(",")): + if one: + split_policy[one] = val + + psn = [s for s in (dns.get("proxy-server-nameserver") or []) if isinstance(s, str)] + + dns_lines: list[str] = [] + if follow_rule: + dns_lines += [ + " # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」,", + " # Stash 的等价物是全局开关 follow-rule。官方提示:多数场景无需开启", + " # (可能影响 CDN 优化并轻微增加延迟),如需 DNS 直连改为 false 即可。", + " follow-rule: true", + "", + " # 整体替换:原数组含 Stash 无法识别的 #RULES 后缀,前置合并会保留它。", + " nameserver: #!replace", + ] + dns_lines += [f" - '{s}'" for s in ns_clean] + dns_lines.append("") + # proxy-server-nameserver 只在基础配置缺失时才补:数组是前置插入的, + # 原样重复输出会让基础配置里的条目变成双份(并发查询翻倍,纯浪费)。 + if follow_rule and not psn: + dns_lines += [ + " # 开启 follow-rule 需要独立解析代理服务器域名,否则会递归查询;", + " # 基础配置未提供,这里补上。", + " proxy-server-nameserver:", + " - 223.5.5.5", + " - 'https://doh.pub/dns-query'", + "", + ] + if split_policy: + dns_lines += [ + " # 源配置把这些域名逗号拼成单键(mihomo 专属),按 Stash 语法拆为独立键。", + " nameserver-policy:", + ] + for key, val in split_policy.items(): + vals = val if isinstance(val, list) else [val] + rendered = ", ".join(str(v) for v in vals) + dns_lines.append(f" '{key}': [{rendered}]") + dns_lines.append("") + if dns_lines: + lines.append("dns:") + lines += dns_lines + + # ── QUIC 差异 ── + quic = _stash_quic_from_rules(cfg.get("rules") or []) + if quic: + policy, ports = quic + if len(ports) == 1: + expr = f"network == 'udp' and dst_port == {ports[0]}" + else: + joined = " or ".join(f"dst_port == {p}" for p in ports) + expr = f"network == 'udp' and ({joined})" + lines += [ + "# mihomo 用逻辑规则 AND/NOT/OR 表达「境外 QUIC 拦截、国内放行」,", + "# Stash 的等价物是 Script Shortcuts + SCRIPT 规则。", + "#", + "# ⚠️ 语义差异:Stash 靠「规则顺序」实现国内放行(CN 规则排在 SCRIPT 之前),", + "# 但覆写的 rules 数组是前置插入的,这条必然跑在所有规则之前,因此国内 UDP:" + f"{ports[0]} 也会被拒绝并回退 TCP。", + "# 功能不受影响(QUIC 本就可回退),但国内 QUIC 的性能收益会失去。", + "# 要完整保留国内放行,需直接调整基础配置的规则顺序,覆写机制无法表达。", + "script:", + " shortcuts:", + f" quic: {expr}", + "", + "rules:", + f" # no-track:不记入连接日志,避免高频 UDP 拒绝刷屏", + f" - SCRIPT,quic,{policy},no-track", + "", + ] + + body = "\n".join(lines).rstrip() + "\n" + changed = _write_stamped_if_changed(REPO_ROOT / out, body) + bits = [] + if follow_rule: + bits.append("dns.follow-rule") + if split_policy: + bits.append(f"nameserver-policy×{len(split_policy)}") + if quic: + bits.append("quic shortcut") + print(f" 差异项: {', '.join(bits) if bits else '无'}") + print(f" {'✓ ' + out + ' 已更新' if changed else '✓ ' + out + ' 无变化'}") + + # --------------------------------------------------------------------------- # 主函数 # --------------------------------------------------------------------------- @@ -4131,6 +4290,7 @@ def main() -> None: _GENERAL_INJECT = _build_general_inject(general_lines) _sync_clash(config, proxy_lines, group_lines, rule_lines) + _sync_stash(config) # 依赖 _sync_clash 的产物,必须排在其后 _sync_loon(config, proxy_lines, group_lines, rule_lines, surge_mitm_lines) _sync_qx(config, proxy_lines, group_lines, rule_lines, surge_mitm_lines) _sync_surfboard(config, proxy_lines, group_lines, rule_lines, general_lines, surge_src) diff --git a/.github/scripts/sync-config.txt b/.github/scripts/sync-config.txt index 68d0be932..abee2b499 100644 --- a/.github/scripts/sync-config.txt +++ b/.github/scripts/sync-config.txt @@ -60,6 +60,11 @@ rule-providers: private => Private geoip/cn => China IP +# Stash +# 覆写文件:只输出 Stash 与 mihomo 的差异项(DNS follow-rule / nameserver-policy 拆键 / +# QUIC Script Shortcut),其余沿用上面的 Clash 产物。无 Builtin,内容全部由脚本推导。 +>> Clash/Stash.stoverride + # Quantumult X >> Quantumult/Sample.conf # > Builtin diff --git a/.github/workflows/sync-config.yml b/.github/workflows/sync-config.yml index abe1706d3..a4901d867 100644 --- a/.github/workflows/sync-config.yml +++ b/.github/workflows/sync-config.yml @@ -34,10 +34,10 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Clash/Sample.yaml Clash/Mihomo.yaml Clash/Script/ Surge/Balloon.lcf Quantumult/Sample.conf Surge/Surfboard.conf sing-box/config.json + git add Clash/Sample.yaml Clash/Mihomo.yaml Clash/Stash.stoverride Clash/Script/ Surge/Balloon.lcf Quantumult/Sample.conf Surge/Surfboard.conf sing-box/config.json if git diff --cached --quiet; then echo "No changes to commit" else - git commit -m "chore: sync Profile.conf → Clash / Loon / QX / Surfboard / sing-box [auto]" + git commit -m "chore: sync Profile.conf → Clash / Stash / Loon / QX / Surfboard / sing-box [auto]" git push fi diff --git a/Clash/Stash.stoverride b/Clash/Stash.stoverride index f16e63ac2..77cbe7798 100644 --- a/Clash/Stash.stoverride +++ b/Clash/Stash.stoverride @@ -1,52 +1,21 @@ name: HotKids Stash 适配 -desc: 将 HotKids/Rules 的 mihomo 配置适配到 Stash:DNS 跟随规则改用 follow-rule、QUIC 拦截改用 Script Shortcuts。配合 Clash/Sample.yaml 使用。 +desc: Stash 与 mihomo 的差异补丁,基础配置请用 Clash/Sample.yaml。由 sync-config.py 自动生成,勿手改。 +# Date: 2026-08-28 17:11:33 -# ============================================================================ -# 覆写合并规则(Stash 官方): -# 标量(string/number/bool)同键 → 直接覆盖 -# 字典同键 → 按键递归合并 -# 数组同键 → 【覆写文件的数组插入到原数组的开头】 -# 键后加 `#!replace` 行注释 → 改为整体替换而非合并 -# 因此本文件中的 rules 会被前置到 Sample.yaml 规则列表最前面,顺序敏感项见下方说明。 -# ============================================================================ +# 覆写合并:标量覆盖 / 字典递归合并 / 数组前置插入;键后 `#!replace` 改为整体替换。 +# 本文件只含差异项,其余沿用基础配置。 - -# ── DNS ────────────────────────────────────────────────────────────────── -# -# 背景:Sample.yaml / General.yaml 用的是 mihomo 的「每条 nameserver 级」跟随规则语法 -# nameserver: ["https://1.1.1.1/dns-query#RULES"] -# Stash 没有这个后缀语义——它的 `#` 片段只用于 DNS 选项(如 #h3=true),`#RULES` -# 不是合法选项。Stash 的等价物是全局布尔开关 dns.follow-rule。 -# -# 官方警告:DNS 经代理转发可能破坏云服务商 CDN 全球优化(静态资源变慢),并轻微增加延迟, -# 「绝大部分场景下不需要开启」。同时存在递归查询问题,需满足以下之一: -# - 转发 DNS 的代理地址是 IP 而非域名 -# - DNS 服务器地址是 IP 而非域名 -# - 用 proxy-server-nameserver 为代理服务器域名配置独立解析 ← 我们满足这条 -# -# 若你更认同官方建议(DNS 直连、把 CDN 优化留给运营商),把 follow-rule 改成 false 即可, -# 其余配置无需改动。 dns: + # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」, + # Stash 的等价物是全局开关 follow-rule。官方提示:多数场景无需开启 + # (可能影响 CDN 优化并轻微增加延迟),如需 DNS 直连改为 false 即可。 follow-rule: true - # 必须整体替换:原数组里的 "https://1.1.1.1/dns-query#RULES" 在 Stash 下无效, - # 若用默认的「前置合并」它会被保留下来。 - # 保持与原配置一致的单上游,仅去掉 Stash 无法识别的 #RULES 后缀。 - # 如需冗余可再加一条(官方建议不超过 2 条,多了增加系统功耗)。 + # 整体替换:原数组含 Stash 无法识别的 #RULES 后缀,前置合并会保留它。 nameserver: #!replace - 'https://1.1.1.1/dns-query' - # 解析代理服务器域名的独立链路,不跟随代理规则——这既是我们原本的设计, - # 也是上面开启 follow-rule 的前置条件。(iOS/tvOS 3.6+,macOS 4.3+) - proxy-server-nameserver: - - 223.5.5.5 - - https://doh.pub/dns-query - - https://dns.alidns.com/dns-query - - # Stash 的 nameserver-policy 键只支持「精确域名 / 通配域名 / geosite:」三种形式。 - # General.yaml 里 NTP 那条把多个域名用逗号拼成单个键(mihomo 专属写法),在 Stash 下 - # 会被当成一个字面域名、永远匹配不上。这里按 Stash 语法拆成独立键补回。 - # (字典递归合并,原有的 geosite:private / geosite:cn 条目保持不变。) + # 源配置把这些域名逗号拼成单键(mihomo 专属),按 Stash 语法拆为独立键。 nameserver-policy: 'time.*.com': [223.5.5.5, 119.29.29.29] 'time.*.gov': [223.5.5.5, 119.29.29.29] @@ -55,66 +24,17 @@ dns: '+.pool.ntp.org': [223.5.5.5, 119.29.29.29] '*.ntp.org.cn': [223.5.5.5, 119.29.29.29] - -# ── QUIC 拦截 ──────────────────────────────────────────────────────────── -# -# 背景:Sample.yaml 用 mihomo 逻辑规则表达「境外 QUIC 拦截、国内放行」: -# AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT -# Stash 的官方做法是 Script Shortcuts + SCRIPT 规则。 +# mihomo 用逻辑规则 AND/NOT/OR 表达「境外 QUIC 拦截、国内放行」, +# Stash 的等价物是 Script Shortcuts + SCRIPT 规则。 # -# ⚠️ 语义差异(务必知悉): -# Stash 文档的示例靠「规则顺序」实现国内放行——把 GEOIP,CN,DIRECT 排在 SCRIPT 规则之前。 -# 但覆写文件的 rules 数组是【前置】到原列表最前面的,所以这里的 SCRIPT 规则一定跑在 -# Sample.yaml 的所有规则之前,无法排到 CN 规则之后。 -# 结果:国内 UDP:443 也会被拒绝,回退 TCP。功能不受影响(QUIC 本就设计为可回退), -# 但国内 QUIC 的性能收益会失去——这是覆写机制的结构性限制,不是配置疏漏。 -# -# 若要完全保留「国内放行」,两条路: -# 1) 直接改 Sample.yaml 的规则顺序(不用覆写); -# 2) 若 Stash 的表达式支持目标 IP 的地理判断,可把条件写进 shortcut—— -# 文档未列出可用变量清单,需自行验证,故此处不臆造语法。 +# ⚠️ 语义差异:Stash 靠「规则顺序」实现国内放行(CN 规则排在 SCRIPT 之前), +# 但覆写的 rules 数组是前置插入的,这条必然跑在所有规则之前,因此国内 UDP:443 也会被拒绝并回退 TCP。 +# 功能不受影响(QUIC 本就可回退),但国内 QUIC 的性能收益会失去。 +# 要完整保留国内放行,需直接调整基础配置的规则顺序,覆写机制无法表达。 script: shortcuts: - # 文档示例用的是 dst_port == 443;Stash 自带示例还额外覆盖了 4483 / 9305 - # (部分 QUIC 变体端口),如需更激进的拦截可改成: - # network == 'udp' and (dst_port == 443 or dst_port == 4483 or dst_port == 9305) quic: network == 'udp' and dst_port == 443 rules: - # no-track:不记录到连接日志,避免高频 UDP 拒绝刷屏 - - SCRIPT,quic,REJECT,no-track - - -# ============================================================================ -# 覆写解决不了、需要你确认或改动源配置的问题 -# ============================================================================ -# -# 1) rule-providers 的 `format:` 键【最高优先级待验证】 -# Stash 支持 MRS 规则集,behavior 限 domain 与 ipcidr——我们那 8 个 mrs 规则集 -# (Private / Reject / Phishing / Speedtest / Global / China 为 domain, -# China IP / LAN 为 ipcidr)正好全部落在支持范围内。 -# 但 Stash 用 behavior 取值区分格式(domain / domain-text / ipcidr / ipcidr-text / -# classical),文档中没有出现 mihomo 那个独立的 `format:` 键。我们 30 个 provider -# 每个都写了 format。若 Stash 忽略 format 而按 URL 或 behavior 推断,则 mrs 可能被 -# 当成文本解析而失败。请在 Stash 里实际加载一次,确认这 8 个规则集能正常生效。 -# (22 个 classical + format: yaml 的规则集风险低——Stash 的 classical 示例用的 -# 就是 .yaml 源,多余的 format 键忽略即可;官方仅提示 classical 解析效率较低。) -# -# 2) proxy-groups 的 `filter` -# 5 个地区组(🇭🇰 / 🇨🇳 / 🇸🇬 / 🇯🇵 / 🇺🇸)同时写了 use: [Server] 和 filter 正则, -# 靠 filter 从订阅里筛出对应地区节点。Stash 文档的组字段里没出现 filter。 -# 若不支持:这些组不会变空,而是会塞进 Server 里的【全部】节点——每个地区组都一样, -# 地区选择失效。覆写无法修复(要修就得把节点名写死)。请实测确认。 -# -# 3) 其余 mihomo 专属键 -# tun / sniffer 整块、dns 的 cache-algorithm(Stash 固定 LRU)、prefer-h3、 -# respect-rules、fake-ip-ttl / fake-ip-range6 / fake-ip-filter-mode、 -# direct-nameserver 系列,以及 unified-delay / tcp-concurrent / geodata-loader / -# global-ua / keep-alive-interval / ntp 等通用项,在 Stash 文档中均未出现。 -# 覆写只能增改、不能删键,所以这些原样保留;预期被忽略(Stash 作为 iOS/macOS -# VPN 扩展运行,tun 这类内核参数本就不由用户配置)。若 Stash 对未知顶层键 -# 严格校验导致加载失败,需要改的是源配置而非本文件。 -# -# 4) Mihomo.yaml 不要直接喂给 Stash -# 它依赖 YAML 锚点与 `<<:` 合并键,还有一个非 schema 的顶层 anchors: 键, -# 且用 include-all-providers 替代 use。请使用 Sample.yaml 作为 Stash 的基础配置。 + # no-track:不记入连接日志,避免高频 UDP 拒绝刷屏 + - SCRIPT,quic,⛔️ REJECT,no-track diff --git a/README.md b/README.md index 7fd337bb0..0cf85d6ab 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ | 目录 | 说明 | |---|---| | [`Surge/`](Surge/) | **单一来源**:托管配置 `Profile.conf`、规则源 `RULE-SET/`、sgmodule 模块 | -| [`Clash/`](Clash/) | 自动生成:`Sample.yaml` / `Mihomo.yaml`(锚点版)/ 规则集 / Enhance Script | +| [`Clash/`](Clash/) | 自动生成:`Sample.yaml` / `Mihomo.yaml`(锚点版)/ `Stash.stoverride`(Stash 差异覆写)/ 规则集 / Enhance Script | | [`Quantumult/`](Quantumult/) | 自动生成:QX 配置与规则;手动维护:图标库 `X/Images/` | | [`sing-box/`](sing-box/) | 自动生成:完整配置 `config.json`、规则集(`source/` 源码 + `rule-set/` 二进制) | | [`subconverter/`](subconverter/) | subconverter 订阅转换远程配置(已弃用,仅维护 emoji) | From 222db29e14f4a4344abcbaa8020600612146ab5a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 09:33:08 +0000 Subject: [PATCH 03/28] feat(sync): make Stash.stoverride a universal override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the generator so the override carries the full policy set instead of only the Stash-vs-mihomo deltas. Same role as Clash/Script/Script.js — apply the repo's groups, rule-sets and rules to ANY subscription — since Stash has no Enhance Script equivalent and uses .stoverride for this. Node membership no longer depends on this repo's own proxy-providers. Translation, all derived from the generated Clash config: - proxy-groups: use: [Server] becomes include-all: true so nodes come from the user's subscription; the region filters are kept verbatim to keep sorting by node name. REJECT-DROP is mapped to REJECT (Stash has no silent-drop builtin). - rule-providers: behavior + format are kept, which Stash requires explicitly. MRS is supported for domain/ipcidr, and all 8 .mrs sets here are exactly that. type/path are mihomo local-cache concerns and are dropped. - rules: emitted under "rules: #!replace" with the source comments and order intact. AND/OR/NOT logic rules become script.shortcuts plus SCRIPT rules, with network, port and policy read off the source rule (udp:443 -> quic, tcp:22 -> ssh). The two GEOSITE rules are dropped: Stash does not document that type and they duplicate the adjacent RULE-SET,China / RULE-SET,Global (geosite/cn.mrs and geolocation-!cn.mrs). - dns: unchanged from before — follow-rule for the #RULES suffix and the split nameserver-policy keys. The QUIC rule keeps a known semantic gap: mihomo excludes CN with NOT(GEOSITE,cn OR GEOIP,CN) and a Stash shortcut expression cannot express a geo test, so domestic UDP:443 is rejected too and falls back to TCP. Moving the rule below the CN rules would be worse — most foreign QUIC would be routed by an earlier service rule and never reach it, defeating the point of keeping QUIC off the proxy. The tradeoff is spelled out in the generated file. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/README.md | 30 ++- .github/scripts/sync-config.py | 301 ++++++++++++++++------ Clash/Stash.stoverride | 458 +++++++++++++++++++++++++++++++-- README.md | 2 +- 4 files changed, 683 insertions(+), 108 deletions(-) diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 80bf17873..f200fe415 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -65,15 +65,27 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do `<<: *Region, filter: *Filter<码>`(`include-all-providers` 与 `use:` 同走 mihomo 保序路径,功能一致)。 -`Clash/Stash.stoverride` 是 `Clash/Sample.yaml` 生成完毕后推导出的 Stash 覆写文件, -**只包含 Stash 与 mihomo 的差异项**,其余全部沿用 Sample.yaml,不重复输出等价配置: -mihomo 每条 nameserver 的 `#RULES` 后缀 → Stash 的全局 `dns.follow-rule`(并用 -`#!replace` 整体替换 nameserver 数组以去掉 Stash 无法识别的后缀);`nameserver-policy` -里逗号拼接的多域名单键(mihomo 专属)→ 拆成 Stash 认的独立键;`[Rule]` 里的 QUIC 逻辑 -规则 `AND,((NETWORK,UDP),(DST-PORT,…),(NOT,…))` → Stash 的 `script.shortcuts` + -`SCRIPT,quic,<策略>,no-track`(策略名与端口都从源规则提取)。差异项为空时对应段落不输出。 -注意 Stash 覆写的数组是**前置插入**,所以只在基础配置缺失时才补 `proxy-server-nameserver`, -避免条目变双份;也因此 QUIC 规则必然先于 CN 规则命中(见文件内注释说明的语义差异)。 +`Clash/Stash.stoverride` 是 `Clash/Sample.yaml` 生成完毕后推导出的 **Stash 通用覆写**, +与 `Clash/Script/Script.js` 同一定位——把本仓库整套策略组 / 规则集 / 规则套到**任意订阅**上 +(Script.js 面向支持 Enhance Script 的 Clash 客户端,Stash 不支持 JS,改用 `.stoverride`)。 +因此它不依赖本仓库自身的 proxy-providers:节点来自使用者的订阅。转译规则如下。 + +- **策略组**:`use: [Server]` → `include-all: true`(纳入订阅全部节点),地区组的 `filter` + 正则原样保留继续按节点名筛地区;`REJECT-DROP` → `REJECT`(Stash 无静默丢弃内置策略)。 +- **规则集**:保留 `behavior` + `format`(Stash 要求显式声明,MRS 支持 `domain`/`ipcidr`, + 本仓库 8 个 mrs 规则集正好全在此范围内);`type` / `path` 是 mihomo 本地缓存语义,略去。 +- **规则**:`rules: #!replace` 整体替换,保留源注释与顺序。`AND/OR/NOT` 逻辑规则 → + `script.shortcuts` + `SCRIPT,<名>,<策略>`(网络/端口/策略均从源规则提取,`udp:443`→`quic`、 + `tcp:22`→`ssh`);`GEOSITE` 规则丢弃——Stash 未文档化该类型,且它与相邻的 + `RULE-SET,China` / `RULE-SET,Global`(即 `geosite/cn.mrs`、`geolocation-!cn.mrs`)等价。 +- **DNS**:只覆盖写法不同的两处——`#RULES` 后缀 → 全局 `dns.follow-rule`(并 `#!replace` + 替换 nameserver 数组去掉该后缀);`nameserver-policy` 里逗号拼接的多域名单键(mihomo + 专属)拆成 Stash 认的独立键。 + +QUIC 那条存在**已知语义差异**:mihomo 版用 `NOT(GEOSITE,cn OR GEOIP,CN)` 排除国内,而 Stash +的 shortcut 表达式无法表达地理判断。生成结果保持它在规则首位,代价是国内 UDP:443 也被拒绝 +(回退 TCP);不下移到 CN 规则之后,是因为那样绝大多数境外 QUIC 会先被服务规则分流走、 +根本到不了这条,反而失去「QUIC 不走代理」的本意。文件内注释已写明该取舍。 `Clash/Script/Script.js` 是 `Clash/Mihomo.yaml` 生成完毕后再解析出来的等效 mihomo 覆写 脚本(Enhance Script),供 Clash Verge Rev / FlClash / Bettbox 等客户端直接对任意订阅动态生成同一套策略组 / diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 317d5a1e6..06f231640 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4151,8 +4151,147 @@ def _stash_quic_from_rules(rules: list) -> tuple[str, list[str]] | None: return None +def _yq(value) -> str: + """YAML 单引号标量(组名含 emoji/空格,统一加引号最稳)。""" + return "'" + str(value).replace("'", "''") + "'" + + +# mihomo 专属内置策略 → Stash 等价物。Stash 只有 REJECT,没有 REJECT-DROP +# (静默丢弃);映射成 REJECT 后行为是「立即拒绝」而非「丢弃」,语义相近。 +_STASH_BUILTIN_MAP = {"REJECT-DROP": "REJECT"} + + +def _stash_group_lines(groups: list) -> tuple[list[str], list[str]]: + """mihomo proxy-groups → Stash proxy-groups(保序)。返回 (行, 改动说明)。""" + lines: list[str] = [] + notes: list[str] = [] + for g in groups: + if not isinstance(g, dict) or "name" not in g: + continue + lines.append(f" - name: {_yq(g['name'])}") + lines.append(f" type: {g.get('type', 'select')}") + if g.get("icon"): + lines.append(f" icon: {g['icon']}") + # use:[Provider] 依赖本仓库自身的 proxy-providers;通用覆写要面向任意订阅, + # 改用 Stash 的 include-all 自动纳入订阅全部节点(filter 仍按原正则筛地区)。 + if g.get("use"): + lines.append(" include-all: true") + notes.append(f"{g['name']}: use→include-all") + if g.get("filter"): + lines.append(f" filter: {_yq(g['filter'])}") + for key in ("url", "interval", "tolerance", "lazy"): + if key in g: + lines.append(f" {key}: {g[key]}") + if g.get("hidden"): + lines.append(" hidden: true") + proxies = g.get("proxies") or [] + if proxies: + lines.append(" proxies:") + for p in proxies: + mapped = _STASH_BUILTIN_MAP.get(p, p) + if mapped != p: + notes.append(f"{g['name']}: {p}→{mapped}") + lines.append(f" - {_yq(mapped)}") + return lines, notes + + +def _stash_provider_lines(providers: dict) -> list[str]: + """mihomo rule-providers → Stash rule-providers。 + + Stash 要求显式声明 behavior + format,与我们现有字段一一对应;MRS 支持 + behavior 为 domain / ipcidr(我们的 8 个 mrs 规则集正好全在此范围内)。 + `type` 与 `path` 是 mihomo 的本地缓存语义,Stash 侧不需要,略去。 + """ + lines: list[str] = [] + for name, rp in providers.items(): + if not isinstance(rp, dict): + continue + lines.append(f" {_yq(name)}:") + for key in ("behavior", "format", "url", "interval"): + if key in rp: + lines.append(f" {key}: {rp[key]}") + return lines + + +# 规则行:` - TYPE,值...,策略[,flag]` +_RULE_LINE_RE = re.compile(r"^(\s*-\s*)(.+?)\s*$") + + +def _stash_rules_block(sample_text: str, shortcuts: dict[str, str]) -> tuple[list[str], list[str]]: + """提取 Sample.yaml 的 rules 块并译成 Stash 规则(保留原注释与分段)。 + + - AND/OR/NOT 逻辑规则 → Script Shortcut(表达式由调用方收集进 shortcuts) + - GEOSITE 规则 → 丢弃:Stash 未文档化该类型,且我们的 GEOSITE,cn / + GEOSITE,geolocation-!cn 与相邻的 RULE-SET,China / RULE-SET,Global + (即 geosite/cn.mrs、geolocation-!cn.mrs)等价,属冗余兜底。 + """ + lines = sample_text.splitlines() + try: + start = next(i for i, ln in enumerate(lines) if ln.rstrip() == "rules:") + except StopIteration: + return [], [] + + out: list[str] = [] + notes: list[str] = [] + for raw in lines[start + 1:]: + if raw and not raw.startswith((" ", "\t", "#")): + break # 下一个顶层键 + stripped = raw.strip() + if not stripped or stripped.startswith("#"): + out.append(raw.rstrip()) + continue + m = _RULE_LINE_RE.match(raw) + if not m: + continue + rule = m.group(2).strip().strip("'\"") + + if rule.startswith("AND,") or rule.startswith("OR,") or rule.startswith("NOT,"): + expr, policy, name = _stash_logic_to_shortcut(rule) + if expr is None: + notes.append(f"逻辑规则无法转译,已丢弃: {rule[:48]}…") + continue + shortcuts[name] = expr + extra = ",no-track" if name == "quic" else "" + out.append(f" - SCRIPT,{name},{policy}{extra}") + notes.append(f"AND→SCRIPT,{name}") + continue + + if rule.startswith("GEOSITE,"): + notes.append(f"丢弃 {rule.split(',')[0]},{rule.split(',')[1]}(与 RULE-SET 冗余)") + continue + + out.append(f" - {rule}") + return out, notes + + +def _stash_logic_to_shortcut(rule: str) -> tuple[str | None, str, str]: + """mihomo 逻辑规则 → (Stash 表达式, 策略, shortcut 名)。无法表达时表达式为 None。""" + m = _QUIC_RULE_RE.match(rule) + if not m: + return None, "", "" + policy = m.group("policy").strip() + ports = re.findall(r"\(DST-PORT,(\d+)\)", rule) + if not ports: + return None, "", "" + net = "udp" if "NETWORK,UDP" in rule else ("tcp" if "NETWORK,TCP" in rule else "") + if not net: + return None, "", "" + if len(ports) == 1: + expr = f"network == '{net}' and dst_port == {ports[0]}" + else: + expr = f"network == '{net}' and (" + " or ".join(f"dst_port == {p}" for p in ports) + ")" + known = {("udp", "443"): "quic", ("tcp", "22"): "ssh"} + name = known.get((net, ports[0])) or f"{net}{ports[0]}" + return expr, policy, name + + def _sync_stash(config: dict) -> None: - """生成 Stash 覆写:只输出与 mihomo 的差异项,从 Clash 产物推导。""" + """生成 Stash 通用覆写:把本仓库整套策略组/规则集/规则套到任意订阅上。 + + 与 Clash/Script/Script.js 同一定位(Script.js 面向支持 Enhance Script 的 + Clash 客户端,Stash 不支持 JS,改用 .stoverride),因此不依赖本仓库自身的 + proxy-providers:节点来自使用者订阅,由 include-all + filter 归入各组。 + """ out = config.get("Stash", {}).get("output") clash_out = config.get("Clash", {}).get("output") if not out or not clash_out: @@ -4162,26 +4301,20 @@ def _sync_stash(config: dict) -> None: return print("\n── sync-config: Clash Sample.yaml → Stash .stoverride ──") - cfg = yaml.safe_load(base_path.read_text(encoding="utf-8")) or {} + sample_text = base_path.read_text(encoding="utf-8") + cfg = yaml.safe_load(sample_text) or {} dns = cfg.get("dns", {}) or {} - lines: list[str] = [ - "name: HotKids Stash 适配", - "desc: Stash 与 mihomo 的差异补丁,基础配置请用 Clash/Sample.yaml。由 sync-config.py 自动生成,勿手改。", - "# Date: ", - "", - "# 覆写合并:标量覆盖 / 字典递归合并 / 数组前置插入;键后 `#!replace` 改为整体替换。", - "# 本文件只含差异项,其余沿用基础配置。", - "", - ] + shortcuts: dict[str, str] = {} + rule_lines, rule_notes = _stash_rules_block(sample_text, shortcuts) + group_lines, group_notes = _stash_group_lines(cfg.get("proxy-groups") or []) + provider_lines = _stash_provider_lines(cfg.get("rule-providers") or {}) - # ── DNS 差异 ── + # ── DNS:仅 Stash 与 mihomo 写法不同的两处 ── ns_raw = [s for s in (dns.get("nameserver") or []) if isinstance(s, str)] follow_rule = any("#RULES" in s for s in ns_raw) ns_clean = [_stash_clean_nameserver(s) for s in ns_raw] - - # 逗号拼接多域名的 nameserver-policy 键是 mihomo 专属;Stash 只认 - # 「精确域名 / 通配域名 / geosite:」,拼接键会被当成字面域名永不命中。 + psn = [s for s in (dns.get("proxy-server-nameserver") or []) if isinstance(s, str)] split_policy: dict[str, object] = {} for key, val in (dns.get("nameserver-policy") or {}).items(): if isinstance(key, str) and "," in key: @@ -4189,32 +4322,77 @@ def _sync_stash(config: dict) -> None: if one: split_policy[one] = val - psn = [s for s in (dns.get("proxy-server-nameserver") or []) if isinstance(s, str)] + L: list[str] = [ + "name: 🔰 HotKids Rules", + "desc: |-", + " HotKids 通用分流覆写 · Stash 版(由 Surge/Profile.conf 自动生成)", + " 规则集: 本仓库 RULE-SET(含 .mrs)· 每 24h 自动更新", + " 节点: include-all 自动纳入订阅全部节点,地区组按名称正则筛选", + "author: HotKids", + "# Date: ", + "", + "# 由 .github/scripts/sync-config.py 生成,请勿手改;改源头 Surge/Profile.conf。", + "# 覆写合并:标量覆盖 / 字典递归合并 / 数组前置插入;键后 `#!replace` 改为整体替换。", + "", + ] + + if group_lines: + L += [ + "# ════════════════════════════════════════════════", + "# 策略组:include-all 纳入订阅全部节点,filter 按名称筛地区", + "# ════════════════════════════════════════════════", + "proxy-groups:", + ] + group_lines + [""] + + if provider_lines: + L += [ + "# ════════════════════════════════════════════════", + "# 远程规则集(Stash 需显式声明 behavior + format)", + "# ════════════════════════════════════════════════", + "rule-providers:", + ] + provider_lines + [""] + + if shortcuts: + L += [ + "# ════════════════════════════════════════════════", + "# Script Shortcuts:替代 mihomo 的 AND/OR/NOT 逻辑规则", + "# ════════════════════════════════════════════════", + "script:", + " shortcuts:", + ] + for name, expr in shortcuts.items(): + L.append(f" {name}: {expr}") + L.append("") + + if rule_lines: + L += [ + "# ════════════════════════════════════════════════", + "# 分流规则:#!replace 整体替换,顺序即优先级", + "#", + "# ⚠️ QUIC 一条与源配置有语义差异:mihomo 版用 NOT(GEOSITE,cn OR GEOIP,CN)", + "# 排除国内,Stash 的 shortcut 表达式无法表达地理判断。这里保持它在首位,", + "# 代价是国内 UDP:443 也被拒绝(回退 TCP,功能不受影响)。", + "# 不把它挪到 CN 规则之后,是因为那样绝大多数境外 QUIC 会先被服务规则", + "# 分流走掉、根本到不了这条,反而彻底失去「QUIC 不走代理」的本意。", + "# ════════════════════════════════════════════════", + "rules: #!replace", + ] + rule_lines + [""] dns_lines: list[str] = [] if follow_rule: dns_lines += [ - " # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」,", - " # Stash 的等价物是全局开关 follow-rule。官方提示:多数场景无需开启", - " # (可能影响 CDN 优化并轻微增加延迟),如需 DNS 直连改为 false 即可。", + " # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」,Stash 的", + " # 等价物是全局开关 follow-rule。官方提示多数场景无需开启(可能影响 CDN", + " # 优化并轻微增加延迟),如需 DNS 直连改为 false 即可。", " follow-rule: true", "", - " # 整体替换:原数组含 Stash 无法识别的 #RULES 后缀,前置合并会保留它。", " nameserver: #!replace", - ] - dns_lines += [f" - '{s}'" for s in ns_clean] - dns_lines.append("") - # proxy-server-nameserver 只在基础配置缺失时才补:数组是前置插入的, - # 原样重复输出会让基础配置里的条目变成双份(并发查询翻倍,纯浪费)。 - if follow_rule and not psn: + ] + [f" - {_yq(s)}" for s in ns_clean] + [""] + if psn: dns_lines += [ - " # 开启 follow-rule 需要独立解析代理服务器域名,否则会递归查询;", - " # 基础配置未提供,这里补上。", - " proxy-server-nameserver:", - " - 223.5.5.5", - " - 'https://doh.pub/dns-query'", - "", - ] + " # 独立解析代理服务器域名,避免 follow-rule 下的递归查询。", + " proxy-server-nameserver: #!replace", + ] + [f" - {_yq(s)}" for s in psn] + [""] if split_policy: dns_lines += [ " # 源配置把这些域名逗号拼成单键(mihomo 专属),按 Stash 语法拆为独立键。", @@ -4222,51 +4400,24 @@ def _sync_stash(config: dict) -> None: ] for key, val in split_policy.items(): vals = val if isinstance(val, list) else [val] - rendered = ", ".join(str(v) for v in vals) - dns_lines.append(f" '{key}': [{rendered}]") + dns_lines.append(f" {_yq(key)}: [{', '.join(str(v) for v in vals)}]") dns_lines.append("") if dns_lines: - lines.append("dns:") - lines += dns_lines - - # ── QUIC 差异 ── - quic = _stash_quic_from_rules(cfg.get("rules") or []) - if quic: - policy, ports = quic - if len(ports) == 1: - expr = f"network == 'udp' and dst_port == {ports[0]}" - else: - joined = " or ".join(f"dst_port == {p}" for p in ports) - expr = f"network == 'udp' and ({joined})" - lines += [ - "# mihomo 用逻辑规则 AND/NOT/OR 表达「境外 QUIC 拦截、国内放行」,", - "# Stash 的等价物是 Script Shortcuts + SCRIPT 规则。", - "#", - "# ⚠️ 语义差异:Stash 靠「规则顺序」实现国内放行(CN 规则排在 SCRIPT 之前),", - "# 但覆写的 rules 数组是前置插入的,这条必然跑在所有规则之前,因此国内 UDP:" - f"{ports[0]} 也会被拒绝并回退 TCP。", - "# 功能不受影响(QUIC 本就可回退),但国内 QUIC 的性能收益会失去。", - "# 要完整保留国内放行,需直接调整基础配置的规则顺序,覆写机制无法表达。", - "script:", - " shortcuts:", - f" quic: {expr}", - "", - "rules:", - f" # no-track:不记入连接日志,避免高频 UDP 拒绝刷屏", - f" - SCRIPT,quic,{policy},no-track", - "", - ] - - body = "\n".join(lines).rstrip() + "\n" + L += [ + "# ════════════════════════════════════════════════", + "# DNS:仅覆盖 Stash 与 mihomo 写法不同之处", + "# ════════════════════════════════════════════════", + "dns:", + ] + dns_lines + + body = "\n".join(L).rstrip() + "\n" changed = _write_stamped_if_changed(REPO_ROOT / out, body) - bits = [] - if follow_rule: - bits.append("dns.follow-rule") - if split_policy: - bits.append(f"nameserver-policy×{len(split_policy)}") - if quic: - bits.append("quic shortcut") - print(f" 差异项: {', '.join(bits) if bits else '无'}") + print(f" groups={len(cfg.get('proxy-groups') or [])} | " + f"providers={len(cfg.get('rule-providers') or {})} | " + f"rules={sum(1 for x in rule_lines if x.lstrip().startswith('- '))} | " + f"shortcuts={len(shortcuts)}") + for note in dict.fromkeys(group_notes + rule_notes): + print(f" · {note}") print(f" {'✓ ' + out + ' 已更新' if changed else '✓ ' + out + ' 无变化'}") diff --git a/Clash/Stash.stoverride b/Clash/Stash.stoverride index 77cbe7798..37cd3c08b 100644 --- a/Clash/Stash.stoverride +++ b/Clash/Stash.stoverride @@ -1,20 +1,447 @@ -name: HotKids Stash 适配 -desc: Stash 与 mihomo 的差异补丁,基础配置请用 Clash/Sample.yaml。由 sync-config.py 自动生成,勿手改。 -# Date: 2026-08-28 17:11:33 +name: 🔰 HotKids Rules +desc: |- + HotKids 通用分流覆写 · Stash 版(由 Surge/Profile.conf 自动生成) + 规则集: 本仓库 RULE-SET(含 .mrs)· 每 24h 自动更新 + 节点: include-all 自动纳入订阅全部节点,地区组按名称正则筛选 +author: HotKids +# Date: 2026-08-28 17:32:14 +# 由 .github/scripts/sync-config.py 生成,请勿手改;改源头 Surge/Profile.conf。 # 覆写合并:标量覆盖 / 字典递归合并 / 数组前置插入;键后 `#!replace` 改为整体替换。 -# 本文件只含差异项,其余沿用基础配置。 +# ════════════════════════════════════════════════ +# 策略组:include-all 纳入订阅全部节点,filter 按名称筛地区 +# ════════════════════════════════════════════════ +proxy-groups: + - name: '🔰 Proxy' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Outbound.png + proxies: + - '🇭🇰 Hong Kong' + - '🇨🇳 Taiwan' + - '🇸🇬 Singapore' + - '🇯🇵 Japan' + - '🇺🇸 America' + - '🇺🇳 Server' + - '🔘 DIRECT' + - name: '🎬 Streaming' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Streaming.png + proxies: + - '🔰 Proxy' + - '🇭🇰 Hong Kong' + - '🇨🇳 Taiwan' + - '🇸🇬 Singapore' + - '🇯🇵 Japan' + - '🇺🇸 America' + - '🇺🇳 Server' + - name: '📺 CNTV' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/StreamingCN.png + proxies: + - '🔘 DIRECT' + - '🇨🇳 Taiwan' + - '🇭🇰 Hong Kong' + - name: '🍎 Apple' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Apple.png + proxies: + - '🔘 DIRECT' + - '🔰 Proxy' + - '🇺🇸 America' + - '🇯🇵 Japan' + - name: '🔍 Google' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Google.png + proxies: + - '🇺🇸 America' + - '🔰 Proxy' + - name: '☁️ OneDrive' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/OneDrive.png + proxies: + - '🔘 DIRECT' + - '🔰 Proxy' + - name: 'Ⓜ️ Microsoft' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Microsoft.png + proxies: + - '🔘 DIRECT' + - '🔰 Proxy' + - name: '📬 Telegram' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Telegram.png + proxies: + - '🔰 Proxy' + - '🇸🇬 Singapore' + - '🔘 DIRECT' + - name: '🤖 AIGC' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/ChatGPT.png + proxies: + - '🇺🇸 America' + - '🇸🇬 Singapore' + - '🔰 Proxy' + - name: '🪙 Crypto' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Cryptocurrency_3.png + proxies: + - '🇺🇸 America' + - '🔰 Proxy' + - '🔘 DIRECT' + - name: '💳 Finance' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Finance.png + proxies: + - '🇺🇸 America' + - '🔰 Proxy' + - '🔘 DIRECT' + - name: '📧 Mail' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Email.png + proxies: + - '🔰 Proxy' + - '🔘 DIRECT' + - name: '⏱️ Speedtest' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Speed.png + include-all: true + - name: '🚧 AdGuard' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Block.png + proxies: + - '🔘 DIRECT' + - '⛔️ REJECT' + - '📛 REJECT-DROP' + - name: '🔘 DIRECT' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Direct.png + hidden: true + proxies: + - 'DIRECT' + - name: '⛔️ REJECT' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Reject.png + hidden: true + proxies: + - 'REJECT' + - name: '📛 REJECT-DROP' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Reject.png + hidden: true + proxies: + - 'REJECT' + - name: '🇺🇳 Server' + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Club.png + include-all: true + - name: '🇭🇰 Hong Kong' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Hong_Kong.png + include-all: true + filter: '(?i)(?:🇭🇰|香港|Hong Kong|\b(?:HK|HKG)\d*\b)' + - name: '🇨🇳 Taiwan' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Taiwan.png + include-all: true + filter: '(?i)(?:🇨🇳|🇹🇼|台湾|Taiwan|\b(?:TW|TWN)\d*\b)' + - name: '🇸🇬 Singapore' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Singapore.png + include-all: true + filter: '(?i)(?:🇸🇬|新加坡|Singapore|\b(?:SG|SGP)\d*\b)' + - name: '🇯🇵 Japan' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Japan.png + include-all: true + filter: '(?i)(?:🇯🇵|日本|Japan|\b(?:JP|JPN)\d*\b)' + - name: '🇺🇸 America' + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/United_States.png + include-all: true + filter: '(?i)(?:🇺🇸|美国|United States|\b(?:US|USA)\d*\b)' + +# ════════════════════════════════════════════════ +# 远程规则集(Stash 需显式声明 behavior + format) +# ════════════════════════════════════════════════ +rule-providers: + 'Bypass': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Bypass.yaml + interval: 86400 + 'Reroute': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Reroute.yaml + interval: 86400 + 'Private': + behavior: domain + format: mrs + url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite/private.mrs + interval: 86400 + 'HTTPDNS': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/VirgilClyne/GetSomeFries@main/ruleset/HTTPDNS.Block.yaml + interval: 86400 + 'Reject': + behavior: domain + format: mrs + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Reject.mrs + interval: 86400 + 'AdBlock': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Block.yaml + interval: 86400 + 'Phishing': + behavior: domain + format: mrs + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Phishing.mrs + interval: 86400 + 'Bogus': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Bogus.yaml + interval: 86400 + 'Streaming_TW': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming_TW.yaml + interval: 86400 + 'Streaming_JP': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming_JP.yaml + interval: 86400 + 'Streaming_US': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming_US.yaml + interval: 86400 + 'Streaming': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming.yaml + interval: 86400 + 'CNTV': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/CNTV.yaml + interval: 86400 + 'Google AI Studio': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Gemini.yaml + interval: 86400 + 'AIGC': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/GenAI.yaml + interval: 86400 + 'Apple CN': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Apple%20CN.yaml + interval: 86400 + 'Apple': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Apple.yaml + interval: 86400 + 'Google': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Google.yaml + interval: 86400 + 'OneDrive': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/OneDrive.yaml + interval: 86400 + 'Microsoft': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Microsoft.yaml + interval: 86400 + 'Telegram': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Telegram.yaml + interval: 86400 + 'Crypto': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Crypto.yaml + interval: 86400 + 'Finance': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Finance.yaml + interval: 86400 + 'Spark': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Spark.yaml + interval: 86400 + 'Speedtest': + behavior: domain + format: mrs + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Speedtest.mrs + interval: 86400 + 'Global': + behavior: domain + format: mrs + url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite/geolocation-!cn.mrs + interval: 86400 + 'China': + behavior: domain + format: mrs + url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite/cn.mrs + interval: 86400 + 'China ASN': + behavior: classical + format: yaml + url: https://fastly.jsdelivr.net/gh/VirgilClyne/GetSomeFries@main/ruleset/ASN.China.yaml + interval: 86400 + 'China IP': + behavior: ipcidr + format: mrs + url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geoip/cn.mrs + interval: 86400 + 'LAN': + behavior: ipcidr + format: mrs + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/lancidr.mrs + interval: 86400 + +# ════════════════════════════════════════════════ +# Script Shortcuts:替代 mihomo 的 AND/OR/NOT 逻辑规则 +# ════════════════════════════════════════════════ +script: + shortcuts: + quic: network == 'udp' and dst_port == 443 + ssh: network == 'tcp' and dst_port == 22 + +# ════════════════════════════════════════════════ +# 分流规则:#!replace 整体替换,顺序即优先级 +# +# ⚠️ QUIC 一条与源配置有语义差异:mihomo 版用 NOT(GEOSITE,cn OR GEOIP,CN) +# 排除国内,Stash 的 shortcut 表达式无法表达地理判断。这里保持它在首位, +# 代价是国内 UDP:443 也被拒绝(回退 TCP,功能不受影响)。 +# 不把它挪到 CN 规则之后,是因为那样绝大多数境外 QUIC 会先被服务规则 +# 分流走掉、根本到不了这条,反而彻底失去「QUIC 不走代理」的本意。 +# ════════════════════════════════════════════════ +rules: #!replace + # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) + - SCRIPT,quic,⛔️ REJECT,no-track + + # 标准 SSH 端口 + - SCRIPT,ssh,🔘 DIRECT + + # Unbreak 后续规则修正,修复因规则拦截或分流不当导致的功能异常 + - RULE-SET,Bypass,🔘 DIRECT + - RULE-SET,Reroute,🔰 Proxy + + # Private 私有网络 + - RULE-SET,Private,🔘 DIRECT + + # HTTPDNS 请求/流量阻止 + - RULE-SET,HTTPDNS,🚧 AdGuard + + # Advertising 广告 + - RULE-SET,Reject,🚧 AdGuard + - RULE-SET,AdBlock,🚧 AdGuard + + # Phishing 钓鱼网站 + - RULE-SET,Phishing,🚧 AdGuard + + # Bogus IP NXDOMAIN 劫持/僵尸网络 C2 + - RULE-SET,Bogus,📛 REJECT-DROP,no-resolve + + # Global Area Network + # > Streaming by Region + # >> Streaming TW + - RULE-SET,Streaming_TW,🇨🇳 Taiwan + # >> Streaming JP + - RULE-SET,Streaming_JP,🇯🇵 Japan + # >> Streaming US + - RULE-SET,Streaming_US,🇺🇸 America + + # > Streaming + - RULE-SET,Streaming,🎬 Streaming + + # > CNTV(适用于 iQIYI Intl,WeTV,Bilibili 等大陆在港台东南亚提供服务的流媒体服务) + - RULE-SET,CNTV,📺 CNTV + + # Global 全球代理规则 + # > AIGC + - RULE-SET,Google AI Studio,🔍 Google + - RULE-SET,AIGC,🤖 AIGC + + # > Apple + # >> Apple Services + - RULE-SET,Apple CN,🔘 DIRECT + - RULE-SET,Apple,🍎 Apple + + # > Google + - RULE-SET,Google,🔍 Google + + # > Microsoft + - RULE-SET,OneDrive,☁️ OneDrive + - RULE-SET,Microsoft,Ⓜ️ Microsoft + + # > Telegram + - RULE-SET,Telegram,📬 Telegram + + # > Crypto + - RULE-SET,Crypto,🪙 Crypto + + # > Finance + - RULE-SET,Finance,💳 Finance + + # > Mail + - RULE-SET,Spark,📧 Mail + + # > Speedtest + - RULE-SET,Speedtest,⏱️ Speedtest + + # Global (DNS Cache Pollution) / (IP Blackhole) / (Region-Restricted Access Denied) / (Network Jitter) + - RULE-SET,Global,🔰 Proxy + + # China Area Network + - RULE-SET,China,🔘 DIRECT + - RULE-SET,China ASN,🔘 DIRECT,no-resolve + - RULE-SET,China IP,🔘 DIRECT,no-resolve + + # Local Area Network + - RULE-SET,LAN,🔘 DIRECT,no-resolve + + # GeoIP + - GEOIP,CN,🔘 DIRECT,no-resolve + + # Final + - MATCH,🔰 Proxy + + +# ════════════════════════════════════════════════ +# DNS:仅覆盖 Stash 与 mihomo 写法不同之处 +# ════════════════════════════════════════════════ dns: - # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」, - # Stash 的等价物是全局开关 follow-rule。官方提示:多数场景无需开启 - # (可能影响 CDN 优化并轻微增加延迟),如需 DNS 直连改为 false 即可。 + # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」,Stash 的 + # 等价物是全局开关 follow-rule。官方提示多数场景无需开启(可能影响 CDN + # 优化并轻微增加延迟),如需 DNS 直连改为 false 即可。 follow-rule: true - # 整体替换:原数组含 Stash 无法识别的 #RULES 后缀,前置合并会保留它。 nameserver: #!replace - 'https://1.1.1.1/dns-query' + # 独立解析代理服务器域名,避免 follow-rule 下的递归查询。 + proxy-server-nameserver: #!replace + - 'https://doh.pub/dns-query' + - 'https://dns.alidns.com/dns-query' + # 源配置把这些域名逗号拼成单键(mihomo 专属),按 Stash 语法拆为独立键。 nameserver-policy: 'time.*.com': [223.5.5.5, 119.29.29.29] @@ -23,18 +450,3 @@ dns: 'ntp.*.com': [223.5.5.5, 119.29.29.29] '+.pool.ntp.org': [223.5.5.5, 119.29.29.29] '*.ntp.org.cn': [223.5.5.5, 119.29.29.29] - -# mihomo 用逻辑规则 AND/NOT/OR 表达「境外 QUIC 拦截、国内放行」, -# Stash 的等价物是 Script Shortcuts + SCRIPT 规则。 -# -# ⚠️ 语义差异:Stash 靠「规则顺序」实现国内放行(CN 规则排在 SCRIPT 之前), -# 但覆写的 rules 数组是前置插入的,这条必然跑在所有规则之前,因此国内 UDP:443 也会被拒绝并回退 TCP。 -# 功能不受影响(QUIC 本就可回退),但国内 QUIC 的性能收益会失去。 -# 要完整保留国内放行,需直接调整基础配置的规则顺序,覆写机制无法表达。 -script: - shortcuts: - quic: network == 'udp' and dst_port == 443 - -rules: - # no-track:不记入连接日志,避免高频 UDP 拒绝刷屏 - - SCRIPT,quic,⛔️ REJECT,no-track diff --git a/README.md b/README.md index 0cf85d6ab..f572c9ecd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ | 目录 | 说明 | |---|---| | [`Surge/`](Surge/) | **单一来源**:托管配置 `Profile.conf`、规则源 `RULE-SET/`、sgmodule 模块 | -| [`Clash/`](Clash/) | 自动生成:`Sample.yaml` / `Mihomo.yaml`(锚点版)/ `Stash.stoverride`(Stash 差异覆写)/ 规则集 / Enhance Script | +| [`Clash/`](Clash/) | 自动生成:`Sample.yaml` / `Mihomo.yaml`(锚点版)/ `Stash.stoverride`(Stash 通用覆写)/ 规则集 / Enhance Script | | [`Quantumult/`](Quantumult/) | 自动生成:QX 配置与规则;手动维护:图标库 `X/Images/` | | [`sing-box/`](sing-box/) | 自动生成:完整配置 `config.json`、规则集(`source/` 源码 + `rule-set/` 二进制) | | [`subconverter/`](subconverter/) | subconverter 订阅转换远程配置(已弃用,仅维护 emoji) | From c40e7248d507c1119953d65b51e3418f85b9ae72 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 09:41:23 +0000 Subject: [PATCH 04/28] fix(sync): stop rewriting rules in the Stash override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read the official Stash docs (rule types, rule-set, proxy-groups, override) and four of the five assumptions the previous commit was built on turned out to be wrong — all in the direction of Stash supporting more than assumed: - AND / OR / NOT logic rules are documented, including nesting. The QUIC rule no longer becomes a script shortcut, so the semantic gap that commit described (domestic UDP:443 also rejected) is gone entirely — the CN carve-out NOT(GEOSITE,cn OR GEOIP,CN) is kept verbatim. - GEOSITE is a documented rule type (v2fly domain-list-community), so the two rules dropped as "redundant" are restored. - REJECT-DROP is a documented builtin alongside REJECT (silent drop vs immediate error), so mapping it to REJECT was a lossy rewrite; it is kept. - no-resolve is documented on GEOIP / IP-CIDR / IP-ASN and inside AND sub-rules. The rules block is now byte-identical to the source config — verified by comparing the parsed arrays, 36 == 36. The script/shortcuts section and the conversion helpers are gone. Also confirmed and reflected: rule-provider path is optional (Stash confines it to its own resource dir) so it stays omitted; include-all and filter are the documented way to build groups from a subscription; and the override merge semantics match what the generator relies on. The only remaining runtime caveat is that GEOSITE data is fetched from github.com on first use. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/README.md | 28 ++++----- .github/scripts/sync-config.py | 110 +++++---------------------------- Clash/Stash.stoverride | 27 +++----- 3 files changed, 39 insertions(+), 126 deletions(-) diff --git a/.github/scripts/README.md b/.github/scripts/README.md index f200fe415..260e9359e 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -70,22 +70,22 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do (Script.js 面向支持 Enhance Script 的 Clash 客户端,Stash 不支持 JS,改用 `.stoverride`)。 因此它不依赖本仓库自身的 proxy-providers:节点来自使用者的订阅。转译规则如下。 -- **策略组**:`use: [Server]` → `include-all: true`(纳入订阅全部节点),地区组的 `filter` - 正则原样保留继续按节点名筛地区;`REJECT-DROP` → `REJECT`(Stash 无静默丢弃内置策略)。 -- **规则集**:保留 `behavior` + `format`(Stash 要求显式声明,MRS 支持 `domain`/`ipcidr`, - 本仓库 8 个 mrs 规则集正好全在此范围内);`type` / `path` 是 mihomo 本地缓存语义,略去。 -- **规则**:`rules: #!replace` 整体替换,保留源注释与顺序。`AND/OR/NOT` 逻辑规则 → - `script.shortcuts` + `SCRIPT,<名>,<策略>`(网络/端口/策略均从源规则提取,`udp:443`→`quic`、 - `tcp:22`→`ssh`);`GEOSITE` 规则丢弃——Stash 未文档化该类型,且它与相邻的 - `RULE-SET,China` / `RULE-SET,Global`(即 `geosite/cn.mrs`、`geolocation-!cn.mrs`)等价。 +- **策略组**:`use: [Server]` → `include-all: true`(引用订阅全部代理与远程代理集),地区组的 + `filter` 正则原样保留继续按节点名筛地区。其余字段(`icon` / `proxies` / `hidden`)不变。 +- **规则集**:保留 `behavior` + `format`(Stash 需显式声明;MRS 支持 `behavior` 为 + `domain` / `ipcidr`,本仓库 8 个 mrs 规则集正好全在此范围内);`type` 是 mihomo 专属, + `path` 是本地缓存语义(Stash 会限制在自身资源目录内),二者均略去。 +- **规则**:`rules: #!replace` 整体替换,与源配置**逐条完全一致**,仅保留原注释与顺序。 + 无需任何改写——Stash 的规则类型是 Clash Premium 超集,我们用到的 `RULE-SET` / `GEOIP` / + `GEOSITE` / `MATCH` / `no-resolve`,以及 `AND` / `OR` / `NOT` 逻辑规则(含嵌套)和内置策略 + `REJECT` / `REJECT-DROP`,官方文档均明确支持。 - **DNS**:只覆盖写法不同的两处——`#RULES` 后缀 → 全局 `dns.follow-rule`(并 `#!replace` - 替换 nameserver 数组去掉该后缀);`nameserver-policy` 里逗号拼接的多域名单键(mihomo - 专属)拆成 Stash 认的独立键。 + 替换 nameserver 数组去掉该后缀,Stash 的 `#` 片段只承载 `h3=true` 这类选项); + `nameserver-policy` 里逗号拼接的多域名单键(mihomo 专属)拆成 Stash 认的独立键 + (官方只支持精确域名 / 通配域名 / `geosite:` 三种键形式)。 -QUIC 那条存在**已知语义差异**:mihomo 版用 `NOT(GEOSITE,cn OR GEOIP,CN)` 排除国内,而 Stash -的 shortcut 表达式无法表达地理判断。生成结果保持它在规则首位,代价是国内 UDP:443 也被拒绝 -(回退 TCP);不下移到 CN 规则之后,是因为那样绝大多数境外 QUIC 会先被服务规则分流走、 -根本到不了这条,反而失去「QUIC 不走代理」的本意。文件内注释已写明该取舍。 +唯一需要留意的运行时依赖:`GEOSITE` 的 domain-list-community 数据不随 Stash 分发,首次使用时 +按需从 github.com 拉取,需保证当时的配置能正常访问 github.com。 `Clash/Script/Script.js` 是 `Clash/Mihomo.yaml` 生成完毕后再解析出来的等效 mihomo 覆写 脚本(Enhance Script),供 Clash Verge Rev / FlClash / Bettbox 等客户端直接对任意订阅动态生成同一套策略组 / diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 06f231640..0b8c4346b 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4131,36 +4131,11 @@ def _stash_clean_nameserver(server: str) -> str: return server if frag.startswith("h3=") else base -# 从 mihomo 逻辑规则中识别 QUIC 拦截规则,取出目标策略与被拦截端口。 -# 形如:AND,((NETWORK,UDP),(DST-PORT,443),(NOT,(...))),⛔️ REJECT -_QUIC_RULE_RE = re.compile(r"^AND,\(\(.*\)\),(?P.+)$") - - -def _stash_quic_from_rules(rules: list) -> tuple[str, list[str]] | None: - for rule in rules: - if not isinstance(rule, str) or not rule.startswith("AND,"): - continue - if "NETWORK,UDP" not in rule: - continue - ports = re.findall(r"\(DST-PORT,(\d+)\)", rule) - if not ports: - continue - m = _QUIC_RULE_RE.match(rule) - if m: - return m.group("policy").strip(), ports - return None - - def _yq(value) -> str: """YAML 单引号标量(组名含 emoji/空格,统一加引号最稳)。""" return "'" + str(value).replace("'", "''") + "'" -# mihomo 专属内置策略 → Stash 等价物。Stash 只有 REJECT,没有 REJECT-DROP -# (静默丢弃);映射成 REJECT 后行为是「立即拒绝」而非「丢弃」,语义相近。 -_STASH_BUILTIN_MAP = {"REJECT-DROP": "REJECT"} - - def _stash_group_lines(groups: list) -> tuple[list[str], list[str]]: """mihomo proxy-groups → Stash proxy-groups(保序)。返回 (行, 改动说明)。""" lines: list[str] = [] @@ -4184,14 +4159,13 @@ def _stash_group_lines(groups: list) -> tuple[list[str], list[str]]: lines.append(f" {key}: {g[key]}") if g.get("hidden"): lines.append(" hidden: true") + # REJECT / REJECT-DROP 都是 Stash 内置策略(前者立即报错,后者静默丢弃), + # 原样保留即可,无需映射。 proxies = g.get("proxies") or [] if proxies: lines.append(" proxies:") for p in proxies: - mapped = _STASH_BUILTIN_MAP.get(p, p) - if mapped != p: - notes.append(f"{g['name']}: {p}→{mapped}") - lines.append(f" - {_yq(mapped)}") + lines.append(f" - {_yq(p)}") return lines, notes @@ -4217,13 +4191,12 @@ def _stash_provider_lines(providers: dict) -> list[str]: _RULE_LINE_RE = re.compile(r"^(\s*-\s*)(.+?)\s*$") -def _stash_rules_block(sample_text: str, shortcuts: dict[str, str]) -> tuple[list[str], list[str]]: - """提取 Sample.yaml 的 rules 块并译成 Stash 规则(保留原注释与分段)。 +def _stash_rules_block(sample_text: str) -> tuple[list[str], list[str]]: + """提取 Sample.yaml 的 rules 块(保留原注释、分段与顺序)。 - - AND/OR/NOT 逻辑规则 → Script Shortcut(表达式由调用方收集进 shortcuts) - - GEOSITE 规则 → 丢弃:Stash 未文档化该类型,且我们的 GEOSITE,cn / - GEOSITE,geolocation-!cn 与相邻的 RULE-SET,China / RULE-SET,Global - (即 geosite/cn.mrs、geolocation-!cn.mrs)等价,属冗余兜底。 + 规则本身无需转译:Stash 的规则类型是 Clash Premium 超集,我们用到的 + RULE-SET / GEOIP / GEOSITE / MATCH / no-resolve 以及 AND / OR / NOT 逻辑规则 + (含嵌套)官方文档均有明确支持,内置策略 REJECT / REJECT-DROP 亦然。 """ lines = sample_text.splitlines() try: @@ -4244,47 +4217,13 @@ def _stash_rules_block(sample_text: str, shortcuts: dict[str, str]) -> tuple[lis if not m: continue rule = m.group(2).strip().strip("'\"") - - if rule.startswith("AND,") or rule.startswith("OR,") or rule.startswith("NOT,"): - expr, policy, name = _stash_logic_to_shortcut(rule) - if expr is None: - notes.append(f"逻辑规则无法转译,已丢弃: {rule[:48]}…") - continue - shortcuts[name] = expr - extra = ",no-track" if name == "quic" else "" - out.append(f" - SCRIPT,{name},{policy}{extra}") - notes.append(f"AND→SCRIPT,{name}") - continue - - if rule.startswith("GEOSITE,"): - notes.append(f"丢弃 {rule.split(',')[0]},{rule.split(',')[1]}(与 RULE-SET 冗余)") - continue - + # GEOSITE 数据不随 Stash 分发,首次使用时按需从 github.com 拉取,提示一次即可 + if rule.startswith("GEOSITE,") and not any("GEOSITE" in n for n in notes): + notes.append("GEOSITE 首次使用需能访问 github.com(数据不随 Stash 分发)") out.append(f" - {rule}") return out, notes -def _stash_logic_to_shortcut(rule: str) -> tuple[str | None, str, str]: - """mihomo 逻辑规则 → (Stash 表达式, 策略, shortcut 名)。无法表达时表达式为 None。""" - m = _QUIC_RULE_RE.match(rule) - if not m: - return None, "", "" - policy = m.group("policy").strip() - ports = re.findall(r"\(DST-PORT,(\d+)\)", rule) - if not ports: - return None, "", "" - net = "udp" if "NETWORK,UDP" in rule else ("tcp" if "NETWORK,TCP" in rule else "") - if not net: - return None, "", "" - if len(ports) == 1: - expr = f"network == '{net}' and dst_port == {ports[0]}" - else: - expr = f"network == '{net}' and (" + " or ".join(f"dst_port == {p}" for p in ports) + ")" - known = {("udp", "443"): "quic", ("tcp", "22"): "ssh"} - name = known.get((net, ports[0])) or f"{net}{ports[0]}" - return expr, policy, name - - def _sync_stash(config: dict) -> None: """生成 Stash 通用覆写:把本仓库整套策略组/规则集/规则套到任意订阅上。 @@ -4305,8 +4244,7 @@ def _sync_stash(config: dict) -> None: cfg = yaml.safe_load(sample_text) or {} dns = cfg.get("dns", {}) or {} - shortcuts: dict[str, str] = {} - rule_lines, rule_notes = _stash_rules_block(sample_text, shortcuts) + rule_lines, rule_notes = _stash_rules_block(sample_text) group_lines, group_notes = _stash_group_lines(cfg.get("proxy-groups") or []) provider_lines = _stash_provider_lines(cfg.get("rule-providers") or {}) @@ -4352,28 +4290,13 @@ def _sync_stash(config: dict) -> None: "rule-providers:", ] + provider_lines + [""] - if shortcuts: - L += [ - "# ════════════════════════════════════════════════", - "# Script Shortcuts:替代 mihomo 的 AND/OR/NOT 逻辑规则", - "# ════════════════════════════════════════════════", - "script:", - " shortcuts:", - ] - for name, expr in shortcuts.items(): - L.append(f" {name}: {expr}") - L.append("") - if rule_lines: L += [ "# ════════════════════════════════════════════════", "# 分流规则:#!replace 整体替换,顺序即优先级", - "#", - "# ⚠️ QUIC 一条与源配置有语义差异:mihomo 版用 NOT(GEOSITE,cn OR GEOIP,CN)", - "# 排除国内,Stash 的 shortcut 表达式无法表达地理判断。这里保持它在首位,", - "# 代价是国内 UDP:443 也被拒绝(回退 TCP,功能不受影响)。", - "# 不把它挪到 CN 规则之后,是因为那样绝大多数境外 QUIC 会先被服务规则", - "# 分流走掉、根本到不了这条,反而彻底失去「QUIC 不走代理」的本意。", + "# 与源配置逐条一致——Stash 支持 AND/OR/NOT 逻辑规则(含嵌套)、GEOSITE、", + "# no-resolve 及 REJECT-DROP,无需改写。", + "# 注意 GEOSITE 数据不随 Stash 分发,首次使用时按需从 github.com 拉取。", "# ════════════════════════════════════════════════", "rules: #!replace", ] + rule_lines + [""] @@ -4414,8 +4337,7 @@ def _sync_stash(config: dict) -> None: changed = _write_stamped_if_changed(REPO_ROOT / out, body) print(f" groups={len(cfg.get('proxy-groups') or [])} | " f"providers={len(cfg.get('rule-providers') or {})} | " - f"rules={sum(1 for x in rule_lines if x.lstrip().startswith('- '))} | " - f"shortcuts={len(shortcuts)}") + f"rules={sum(1 for x in rule_lines if x.lstrip().startswith('- '))}") for note in dict.fromkeys(group_notes + rule_notes): print(f" · {note}") print(f" {'✓ ' + out + ' 已更新' if changed else '✓ ' + out + ' 无变化'}") diff --git a/Clash/Stash.stoverride b/Clash/Stash.stoverride index 37cd3c08b..07e9f7469 100644 --- a/Clash/Stash.stoverride +++ b/Clash/Stash.stoverride @@ -4,7 +4,7 @@ desc: |- 规则集: 本仓库 RULE-SET(含 .mrs)· 每 24h 自动更新 节点: include-all 自动纳入订阅全部节点,地区组按名称正则筛选 author: HotKids -# Date: 2026-08-28 17:32:14 +# Date: 2026-08-28 17:40:31 # 由 .github/scripts/sync-config.py 生成,请勿手改;改源头 Surge/Profile.conf。 # 覆写合并:标量覆盖 / 字典递归合并 / 数组前置插入;键后 `#!replace` 改为整体替换。 @@ -130,7 +130,7 @@ proxy-groups: icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Reject.png hidden: true proxies: - - 'REJECT' + - 'REJECT-DROP' - name: '🇺🇳 Server' type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Club.png @@ -316,29 +316,18 @@ rule-providers: url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/lancidr.mrs interval: 86400 -# ════════════════════════════════════════════════ -# Script Shortcuts:替代 mihomo 的 AND/OR/NOT 逻辑规则 -# ════════════════════════════════════════════════ -script: - shortcuts: - quic: network == 'udp' and dst_port == 443 - ssh: network == 'tcp' and dst_port == 22 - # ════════════════════════════════════════════════ # 分流规则:#!replace 整体替换,顺序即优先级 -# -# ⚠️ QUIC 一条与源配置有语义差异:mihomo 版用 NOT(GEOSITE,cn OR GEOIP,CN) -# 排除国内,Stash 的 shortcut 表达式无法表达地理判断。这里保持它在首位, -# 代价是国内 UDP:443 也被拒绝(回退 TCP,功能不受影响)。 -# 不把它挪到 CN 规则之后,是因为那样绝大多数境外 QUIC 会先被服务规则 -# 分流走掉、根本到不了这条,反而彻底失去「QUIC 不走代理」的本意。 +# 与源配置逐条一致——Stash 支持 AND/OR/NOT 逻辑规则(含嵌套)、GEOSITE、 +# no-resolve 及 REJECT-DROP,无需改写。 +# 注意 GEOSITE 数据不随 Stash 分发,首次使用时按需从 github.com 拉取。 # ════════════════════════════════════════════════ rules: #!replace # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) - - SCRIPT,quic,⛔️ REJECT,no-track + - AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT # 标准 SSH 端口 - - SCRIPT,ssh,🔘 DIRECT + - AND,((DST-PORT,22),(NETWORK,TCP)),🔘 DIRECT # Unbreak 后续规则修正,修复因规则拦截或分流不当导致的功能异常 - RULE-SET,Bypass,🔘 DIRECT @@ -419,7 +408,9 @@ rules: #!replace - RULE-SET,LAN,🔘 DIRECT,no-resolve # GeoIP + - GEOSITE,cn,🔘 DIRECT - GEOIP,CN,🔘 DIRECT,no-resolve + - GEOSITE,geolocation-!cn,🔰 Proxy # Final - MATCH,🔰 Proxy From 2460281c13b0e022b720aec46ff12f454f29be52 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 11:29:43 +0000 Subject: [PATCH 05/28] refactor(sync): make Stash.stoverride a transform of Sample.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites the generator so the override is what Mihomo.yaml is — a second-order rendering of Clash/Sample.yaml — instead of a file built around another repo's structure. Sample.yaml is transcribed line by line, comments and layout included, and only the points where Stash genuinely differs from mihomo are changed, so the result is a complete config that can be imported into Stash directly rather than a patch that only works on top of our own config. The differences it rewrites: - Drops mihomo-only top-level keys and blocks, together with their leading comments: the listener/control keys, ipv6, the geo-data source keys, unified-delay, tcp-concurrent, find-process-mode, global-ua, keep-alive-interval, profile, ntp, sniffer, tun and the empty proxies placeholder. Stash owns these itself or has no equivalent. - dns: keeps only the five sub-keys Stash documents, adds follow-rule (mihomo spells this as a #RULES suffix on each nameserver, Stash as a global switch) and re-emits nameserver under #!replace so the suffix is dropped rather than merged in. The comma-joined NTP policy key is split into one key per domain, since Stash keys are exact / wildcard / geosite: only. - providers: drops the mihomo-only "type" and renames proxy-providers "header" to "headers", which is the spelling Stash documents. Everything else — hosts, mode, log-level, all 23 groups, all 30 rule-providers and all 36 rules — is carried through untouched; verified by comparing the parsed structures against Sample.yaml. Generation is idempotent and the file header keeps the repo's # Date stamping convention. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 367 ++++++++++------------ Clash/Stash.stoverride | 540 +++++++++++++++++++++++---------- 2 files changed, 552 insertions(+), 355 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 0b8c4346b..f4db181da 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4115,233 +4115,198 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - # Stash 覆写(.stoverride) # --------------------------------------------------------------------------- # -# Stash 用「覆写文件」在基础配置之上打补丁,合并规则(官方): -# 标量同键 → 直接覆盖;字典同键 → 按键递归合并; -# 数组同键 → 覆写的数组【插入到原数组开头】; -# 键后加 `#!replace` 行注释 → 改为整体替换。 -# 因此本文件只产出「Stash 与 mihomo 的差异项」,其余全部沿用 Clash/Sample.yaml, -# 不重复输出任何等价配置。所有差异项均从生成好的 Clash 配置推导,源改了这里自动跟随。 - -# mihomo 的 nameserver 策略后缀(#RULES / #策略名)在 Stash 中不存在——Stash 的 `#` -# 片段只承载选项(如 h3=true)。保留 h3= 这类合法选项,其余后缀一律剥离。 +# Clash/Stash.stoverride 是 Clash/Sample.yaml 的二次转换产物(与 Clash/Mihomo.yaml +# 同一定位):整份配置原样转录,只在 Stash 与 mihomo 真正有差异的点上改写,因此 +# 可以直接作为覆写文件导入 Stash 使用。差异点仅以下四类,其余逐行照搬(含注释与排版)。 + +# 1) mihomo 专属的顶层键 / 整块——Stash 文档中不存在,且多为 Stash 由 App 自身掌管 +# 的能力(监听端口、TUN、嗅探、geo 数据源等),连同其前置注释一并略去。 +_STASH_DROP_TOP = { + "mixed-port", "allow-lan", "bind-address", "ipv6", "external-controller", + "unified-delay", "tcp-concurrent", "find-process-mode", "geodata-loader", + "global-ua", "keep-alive-interval", "geo-auto-update", "geo-update-interval", + "geox-url", "profile", "ntp", "sniffer", "tun", "proxies", +} + +# 2) dns 块内 Stash 支持的子键(其余为 mihomo 专属,略去) +_STASH_DNS_KEEP = { + "default-nameserver", "nameserver", "nameserver-policy", + "proxy-server-nameserver", "fake-ip-filter", +} + +_TOP_KEY_RE = re.compile(r"^([A-Za-z][\w-]*):") +_SUB_KEY_RE = re.compile(r"^(\s+)(['\"]?)([^:'\"]+)\2\s*:") + + def _stash_clean_nameserver(server: str) -> str: + """mihomo 的 nameserver 策略后缀(#RULES / #策略名)在 Stash 中不存在——Stash 的 + `#` 片段只承载选项(如 h3=true)。保留 h3= 这类合法选项,其余后缀一律剥离。""" if "#" not in server: return server base, frag = server.split("#", 1) return server if frag.startswith("h3=") else base -def _yq(value) -> str: - """YAML 单引号标量(组名含 emoji/空格,统一加引号最稳)。""" - return "'" + str(value).replace("'", "''") + "'" +def _sync_stash(config: dict) -> None: + """Clash/Sample.yaml → Clash/Stash.stoverride(只改 Stash 与 mihomo 的差异点)。""" + out_path = config.get("Stash", {}).get("output") + clash_out = config.get("Clash", {}).get("output") + if not out_path or not clash_out: + return + base_path = REPO_ROOT / clash_out + if not base_path.exists(): + return + print("\n── sync-config: Clash Sample.yaml → Stash .stoverride ──") + src = base_path.read_text(encoding="utf-8").splitlines() -def _stash_group_lines(groups: list) -> tuple[list[str], list[str]]: - """mihomo proxy-groups → Stash proxy-groups(保序)。返回 (行, 改动说明)。""" - lines: list[str] = [] - notes: list[str] = [] - for g in groups: - if not isinstance(g, dict) or "name" not in g: - continue - lines.append(f" - name: {_yq(g['name'])}") - lines.append(f" type: {g.get('type', 'select')}") - if g.get("icon"): - lines.append(f" icon: {g['icon']}") - # use:[Provider] 依赖本仓库自身的 proxy-providers;通用覆写要面向任意订阅, - # 改用 Stash 的 include-all 自动纳入订阅全部节点(filter 仍按原正则筛地区)。 - if g.get("use"): - lines.append(" include-all: true") - notes.append(f"{g['name']}: use→include-all") - if g.get("filter"): - lines.append(f" filter: {_yq(g['filter'])}") - for key in ("url", "interval", "tolerance", "lazy"): - if key in g: - lines.append(f" {key}: {g[key]}") - if g.get("hidden"): - lines.append(" hidden: true") - # REJECT / REJECT-DROP 都是 Stash 内置策略(前者立即报错,后者静默丢弃), - # 原样保留即可,无需映射。 - proxies = g.get("proxies") or [] - if proxies: - lines.append(" proxies:") - for p in proxies: - lines.append(f" - {_yq(p)}") - return lines, notes - - -def _stash_provider_lines(providers: dict) -> list[str]: - """mihomo rule-providers → Stash rule-providers。 - - Stash 要求显式声明 behavior + format,与我们现有字段一一对应;MRS 支持 - behavior 为 domain / ipcidr(我们的 8 个 mrs 规则集正好全在此范围内)。 - `type` 与 `path` 是 mihomo 的本地缓存语义,Stash 侧不需要,略去。 - """ - lines: list[str] = [] - for name, rp in providers.items(): - if not isinstance(rp, dict): + out: list[str] = [] + buf: list[str] = [] # 待决的注释 / 空行(跟随其后的键一起保留或丢弃) + top = "" # 当前顶层键 + keep_top = True + dns_keep = True # dns 块内当前子键是否保留 + policy_split: list[str] | None = None # 逗号拼接键待展开的域名 + policy_val: list[str] = [] # 该键的值行 + changes: list[str] = [] + + def flush() -> None: + out.extend(buf) + buf.clear() + + # 文件头(首个顶层键之前的注释/空行,含 # Clash / # Date / # Author / # 通用设置) + # 始终保留:它不属于任何键,不能跟着被略去的首个键一起丢掉。 + first_key = next((i for i, l in enumerate(src) if _TOP_KEY_RE.match(l)), 0) + header = [l.rstrip() for l in src[:first_key]] + if header and header[0].startswith("# Clash"): + header[0] = "# Stash" + # 紧贴首个键的那段注释是该键的说明(首个键必然是被略去的 mixed-port), + # 随它一起去掉,避免留下孤儿注释;靠空行分隔的分区标题(# 通用设置)保留。 + while header and header[-1].lstrip().startswith("#"): + header.pop() + out.extend(header) + + for raw in src[first_key:]: + line = raw.rstrip() + stripped = line.strip() + + # ── 逗号拼接的 nameserver-policy 键:收集值行后按域名展开 ── + if policy_split is not None: + if stripped and not stripped.startswith("#") and len(line) - len(line.lstrip()) >= 6: + policy_val.append(line) + continue + for dom in policy_split: + out.append(f' "{dom}":') + out.extend(policy_val) + policy_split, policy_val = None, [] + # 落到下面继续处理当前行 + + m_top = _TOP_KEY_RE.match(line) + if m_top: + top = m_top.group(1) + keep_top = top not in _STASH_DROP_TOP + if not keep_top: + buf.clear() + changes.append(f"略去 {top}") + continue + flush() + out.append(line) + if top == "dns": + # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」, + # Stash 的等价物是全局开关 follow-rule。 + out += [ + " # DNS 查询跟随规则出站(mihomo 用 nameserver 的 #RULES 后缀表达,", + " # Stash 为全局开关)。官方提示多数场景无需开启:DNS 经代理转发可能", + " # 破坏云服务商 CDN 优化并轻微增加延迟。如需 DNS 直连改为 false。", + " # 下方 proxy-server-nameserver 已为代理服务器域名提供独立解析,", + " # 满足官方要求的前置条件之一(避免递归查询)。", + " follow-rule: true", + ] + changes.append("dns: #RULES → follow-rule") continue - lines.append(f" {_yq(name)}:") - for key in ("behavior", "format", "url", "interval"): - if key in rp: - lines.append(f" {key}: {rp[key]}") - return lines - - -# 规则行:` - TYPE,值...,策略[,flag]` -_RULE_LINE_RE = re.compile(r"^(\s*-\s*)(.+?)\s*$") + if not stripped or stripped.startswith("#"): + buf.append(line) + continue -def _stash_rules_block(sample_text: str) -> tuple[list[str], list[str]]: - """提取 Sample.yaml 的 rules 块(保留原注释、分段与顺序)。 + if not keep_top: + buf.clear() + continue - 规则本身无需转译:Stash 的规则类型是 Clash Premium 超集,我们用到的 - RULE-SET / GEOIP / GEOSITE / MATCH / no-resolve 以及 AND / OR / NOT 逻辑规则 - (含嵌套)官方文档均有明确支持,内置策略 REJECT / REJECT-DROP 亦然。 - """ - lines = sample_text.splitlines() - try: - start = next(i for i, ln in enumerate(lines) if ln.rstrip() == "rules:") - except StopIteration: - return [], [] + indent = len(line) - len(line.lstrip()) + m_sub = _SUB_KEY_RE.match(line) - out: list[str] = [] - notes: list[str] = [] - for raw in lines[start + 1:]: - if raw and not raw.startswith((" ", "\t", "#")): - break # 下一个顶层键 - stripped = raw.strip() - if not stripped or stripped.startswith("#"): - out.append(raw.rstrip()) + # ── dns:按 Stash 支持的子键过滤 ── + if top == "dns" and indent == 2 and m_sub: + key = m_sub.group(3).strip() + dns_keep = key in _STASH_DNS_KEEP + if not dns_keep: + buf.clear() + continue + flush() + if key == "nameserver": + # 整体替换:原数组含 Stash 无法识别的 #RULES 后缀, + # 覆写默认的「前置插入」会把它保留下来。 + out.append(" nameserver: #!replace") + else: + out.append(line) continue - m = _RULE_LINE_RE.match(raw) - if not m: + if top == "dns" and indent > 2 and not dns_keep: + buf.clear() continue - rule = m.group(2).strip().strip("'\"") - # GEOSITE 数据不随 Stash 分发,首次使用时按需从 github.com 拉取,提示一次即可 - if rule.startswith("GEOSITE,") and not any("GEOSITE" in n for n in notes): - notes.append("GEOSITE 首次使用需能访问 github.com(数据不随 Stash 分发)") - out.append(f" - {rule}") - return out, notes + # nameserver 条目:剥掉 #RULES 后缀 + if top == "dns" and dns_keep and stripped.startswith("- ") and "#RULES" in line: + flush() + val = stripped[2:].strip().strip("'\"") + out.append(f' - "{_stash_clean_nameserver(val)}"') + changes.append("nameserver 去 #RULES 后缀") + continue -def _sync_stash(config: dict) -> None: - """生成 Stash 通用覆写:把本仓库整套策略组/规则集/规则套到任意订阅上。 + # nameserver-policy:逗号拼接多域名的单键是 mihomo 专属;Stash 只认 + # 「精确域名 / 通配域名 / geosite:」,拼接键会被当成字面域名永不命中。 + if top == "dns" and indent == 4 and m_sub and "," in m_sub.group(3): + flush() + policy_split = [d.strip() for d in m_sub.group(3).split(",") if d.strip()] + policy_val = [] + changes.append(f"nameserver-policy 拆键 ×{len(policy_split)}") + continue - 与 Clash/Script/Script.js 同一定位(Script.js 面向支持 Enhance Script 的 - Clash 客户端,Stash 不支持 JS,改用 .stoverride),因此不依赖本仓库自身的 - proxy-providers:节点来自使用者订阅,由 include-all + filter 归入各组。 - """ - out = config.get("Stash", {}).get("output") - clash_out = config.get("Clash", {}).get("output") - if not out or not clash_out: - return - base_path = REPO_ROOT / clash_out - if not base_path.exists(): - return + # ── provider:type 是 mihomo 专属;header 在 Stash 中为 headers ── + if top in ("proxy-providers", "rule-providers"): + if m_sub and m_sub.group(3).strip() == "type": + buf.clear() + continue + if m_sub and m_sub.group(3).strip() == "header": + flush() + out.append(line.replace("header:", "headers:", 1)) + changes.append("proxy-providers: header → headers") + continue - print("\n── sync-config: Clash Sample.yaml → Stash .stoverride ──") - sample_text = base_path.read_text(encoding="utf-8") - cfg = yaml.safe_load(sample_text) or {} - dns = cfg.get("dns", {}) or {} - - rule_lines, rule_notes = _stash_rules_block(sample_text) - group_lines, group_notes = _stash_group_lines(cfg.get("proxy-groups") or []) - provider_lines = _stash_provider_lines(cfg.get("rule-providers") or {}) - - # ── DNS:仅 Stash 与 mihomo 写法不同的两处 ── - ns_raw = [s for s in (dns.get("nameserver") or []) if isinstance(s, str)] - follow_rule = any("#RULES" in s for s in ns_raw) - ns_clean = [_stash_clean_nameserver(s) for s in ns_raw] - psn = [s for s in (dns.get("proxy-server-nameserver") or []) if isinstance(s, str)] - split_policy: dict[str, object] = {} - for key, val in (dns.get("nameserver-policy") or {}).items(): - if isinstance(key, str) and "," in key: - for one in (x.strip() for x in key.split(",")): - if one: - split_policy[one] = val + flush() + out.append(line) - L: list[str] = [ - "name: 🔰 HotKids Rules", + if policy_split is not None: + for dom in policy_split: + out.append(f' "{dom}":') + out.extend(policy_val) + + # 在文件头的 # Author 之后补上生成说明与覆写元数据 + insert_at = next((i for i, l in enumerate(out) if l.startswith("# Author:")), 0) + 1 + out[insert_at:insert_at] = [ + "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", + "", + "name: HotKids", "desc: |-", - " HotKids 通用分流覆写 · Stash 版(由 Surge/Profile.conf 自动生成)", - " 规则集: 本仓库 RULE-SET(含 .mrs)· 每 24h 自动更新", - " 节点: include-all 自动纳入订阅全部节点,地区组按名称正则筛选", + " HotKids 规则配置 · Stash 覆写", + " 由 Surge/Profile.conf 经 Clash/Sample.yaml 转译,仅改写 Stash 与 mihomo 的差异点", "author: HotKids", - "# Date: ", - "", - "# 由 .github/scripts/sync-config.py 生成,请勿手改;改源头 Surge/Profile.conf。", - "# 覆写合并:标量覆盖 / 字典递归合并 / 数组前置插入;键后 `#!replace` 改为整体替换。", - "", ] - if group_lines: - L += [ - "# ════════════════════════════════════════════════", - "# 策略组:include-all 纳入订阅全部节点,filter 按名称筛地区", - "# ════════════════════════════════════════════════", - "proxy-groups:", - ] + group_lines + [""] - - if provider_lines: - L += [ - "# ════════════════════════════════════════════════", - "# 远程规则集(Stash 需显式声明 behavior + format)", - "# ════════════════════════════════════════════════", - "rule-providers:", - ] + provider_lines + [""] - - if rule_lines: - L += [ - "# ════════════════════════════════════════════════", - "# 分流规则:#!replace 整体替换,顺序即优先级", - "# 与源配置逐条一致——Stash 支持 AND/OR/NOT 逻辑规则(含嵌套)、GEOSITE、", - "# no-resolve 及 REJECT-DROP,无需改写。", - "# 注意 GEOSITE 数据不随 Stash 分发,首次使用时按需从 github.com 拉取。", - "# ════════════════════════════════════════════════", - "rules: #!replace", - ] + rule_lines + [""] - - dns_lines: list[str] = [] - if follow_rule: - dns_lines += [ - " # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」,Stash 的", - " # 等价物是全局开关 follow-rule。官方提示多数场景无需开启(可能影响 CDN", - " # 优化并轻微增加延迟),如需 DNS 直连改为 false 即可。", - " follow-rule: true", - "", - " nameserver: #!replace", - ] + [f" - {_yq(s)}" for s in ns_clean] + [""] - if psn: - dns_lines += [ - " # 独立解析代理服务器域名,避免 follow-rule 下的递归查询。", - " proxy-server-nameserver: #!replace", - ] + [f" - {_yq(s)}" for s in psn] + [""] - if split_policy: - dns_lines += [ - " # 源配置把这些域名逗号拼成单键(mihomo 专属),按 Stash 语法拆为独立键。", - " nameserver-policy:", - ] - for key, val in split_policy.items(): - vals = val if isinstance(val, list) else [val] - dns_lines.append(f" {_yq(key)}: [{', '.join(str(v) for v in vals)}]") - dns_lines.append("") - if dns_lines: - L += [ - "# ════════════════════════════════════════════════", - "# DNS:仅覆盖 Stash 与 mihomo 写法不同之处", - "# ════════════════════════════════════════════════", - "dns:", - ] + dns_lines - - body = "\n".join(L).rstrip() + "\n" - changed = _write_stamped_if_changed(REPO_ROOT / out, body) - print(f" groups={len(cfg.get('proxy-groups') or [])} | " - f"providers={len(cfg.get('rule-providers') or {})} | " - f"rules={sum(1 for x in rule_lines if x.lstrip().startswith('- '))}") - for note in dict.fromkeys(group_notes + rule_notes): + body = "\n".join(out).rstrip() + "\n" + changed = _write_stamped_if_changed(REPO_ROOT / out_path, body) + for note in dict.fromkeys(changes): print(f" · {note}") - print(f" {'✓ ' + out + ' 已更新' if changed else '✓ ' + out + ' 无变化'}") - + print(f" {'✓ ' + out_path + ' 已更新' if changed else '✓ ' + out_path + ' 无变化'}") # --------------------------------------------------------------------------- # 主函数 diff --git a/Clash/Stash.stoverride b/Clash/Stash.stoverride index 07e9f7469..81b276c59 100644 --- a/Clash/Stash.stoverride +++ b/Clash/Stash.stoverride @@ -1,328 +1,587 @@ -name: 🔰 HotKids Rules +# Stash +# Date: 2026-08-28 19:29:18 +# Author: @HotKids +# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 + +name: HotKids desc: |- - HotKids 通用分流覆写 · Stash 版(由 Surge/Profile.conf 自动生成) - 规则集: 本仓库 RULE-SET(含 .mrs)· 每 24h 自动更新 - 节点: include-all 自动纳入订阅全部节点,地区组按名称正则筛选 + HotKids 规则配置 · Stash 覆写 + 由 Surge/Profile.conf 经 Clash/Sample.yaml 转译,仅改写 Stash 与 mihomo 的差异点 author: HotKids -# Date: 2026-08-28 17:40:31 -# 由 .github/scripts/sync-config.py 生成,请勿手改;改源头 Surge/Profile.conf。 -# 覆写合并:标量覆盖 / 字典递归合并 / 数组前置插入;键后 `#!replace` 改为整体替换。 +# 通用设置 + + +# 代理模式:rule(规则)/ global(全局)/ direct(直连) +mode: rule + +# 日志等级:silent / error / warning / info / debug +log-level: info + +# ── Hosts ── + +# 静态域名映射,优先级高于 DNS 解析 +hosts: + 'localhost': 127.0.0.1 + +# ── DNS ── + +dns: + # DNS 查询跟随规则出站(mihomo 用 nameserver 的 #RULES 后缀表达, + # Stash 为全局开关)。官方提示多数场景无需开启:DNS 经代理转发可能 + # 破坏云服务商 CDN 优化并轻微增加延迟。如需 DNS 直连改为 false。 + # 下方 proxy-server-nameserver 已为代理服务器域名提供独立解析, + # 满足官方要求的前置条件之一(避免递归查询)。 + follow-rule: true + + # 引导 DNS:仅用于解析 nameserver 服务器的域名 + # 只能填纯 IP 地址 + default-nameserver: + - 223.5.5.5 + - 119.29.29.29 + fake-ip-filter: + # 本地域名 + - "*.lan" + - "+.lan" + - "*.local" + - "*.localdomain" + - "*.home.arpa" + - "*.localhost" + - "WORKGROUP" + # NTP 时间同步 + - "time.*.com" + - "time.*.gov" + - "time.*.apple.com" + - "ntp.*.com" + - "+.pool.ntp.org" + - "*.ntp.org.cn" + # STUN / TURN(WebRTC / 游戏打洞) + - "+.stun.*" + - "*.stun.*.*" + - "*.turn.twilio.com" + - "*.stun.twilio.com" + - "stun.syncthing.net" + # 游戏平台 + - "*.srv.nintendo.net" + - "xbox.*.microsoft.com" + - "xbox.*.*.microsoft.com" + - "*.xboxlive.com" + # Steam + - "*.cm.steampowered.com" + - "*.steamcontent.com" + # Battle.net / Blizzard + - "*.battlenet.com.cn" + - "*.battlenet.com" + - "*.blzstatic.cn" + - "*.battle.net" + # 网络连通性探测(多平台) + - "*.msftncsi.com" + - "*.msftconnecttest.com" + - "connectivitycheck.gstatic.com" + - "connectivitycheck.android.com" + - "connectivitycheck.platform.hicloud.com" + - "connect.rom.miui.com" + - "captive.apple.com" + - "network-test.debian.org" + - "detectportal.firefox.com" + - "lens.l.google.com" + # 推送通知 + - "+.push.apple.com" + - "+.market.xiaomi.com" + # VPN 覆盖网络 + - "*.tailscale.com" + - "*.zerotier.com" + # 媒体服务 + - "*.spotify.com" + - "+.music.126.net" + - "*.mcdn.bilivideo.cn" + # 其他需要真实 IP 解析的域名 + - "localhost.*.qq.com" + + # 主 DNS:经代理查询干净结果,防止境外域名请求泄露至国内 DNS 服务商 + nameserver: #!replace + - "https://1.1.1.1/dns-query" + + # 分域名 DNS 策略:按声明顺序匹配,先窄后宽 + nameserver-policy: + # 内网/本地域名交给系统解析器(路由器/DHCP 下发的 DNS),可解析局域网设备名 + "geosite:private": + - system + # NTP 校时域名用裸 IP UDP 解析:时间偏差会让 DoH 证书验证失败,校时链路须独立于任何 TLS/代理 + "time.*.com": + - 223.5.5.5 + - 119.29.29.29 + "time.*.gov": + - 223.5.5.5 + - 119.29.29.29 + "time.*.apple.com": + - 223.5.5.5 + - 119.29.29.29 + "ntp.*.com": + - 223.5.5.5 + - 119.29.29.29 + "+.pool.ntp.org": + - 223.5.5.5 + - 119.29.29.29 + "*.ntp.org.cn": + - 223.5.5.5 + - 119.29.29.29 + # 国内域名直接用国内 DoH 就近解析,不绕代理(geosite 数据规则段已在用,零额外开销) + "geosite:cn": + - https://doh.pub/dns-query + - https://dns.alidns.com/dns-query + + # 代理节点域名解析:国内 DoH,节点 IP 不会被 CN DNS 污染,速度更快 + proxy-server-nameserver: + - https://doh.pub/dns-query + - https://dns.alidns.com/dns-query + + +# 服务器订阅配置 +proxy-providers: + Server: + path: ./Provider/Proxy/Server.yaml + url: https://sub.hotkids.me + interval: 3600 + proxy: DIRECT + headers: + User-Agent: + - "Clash/v1.18.0" + - "mihomo/1.18.3" + health-check: + enable: true + url: 'http://cp.cloudflare.com/generate_204' + interval: 600 + timeout: 5000 + expected-status: 204 -# ════════════════════════════════════════════════ -# 策略组:include-all 纳入订阅全部节点,filter 按名称筛地区 -# ════════════════════════════════════════════════ proxy-groups: - - name: '🔰 Proxy' + # Global + # name: "GLOBAL" + # type: select + # icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Inbound.png + # include-all: true + + # Proxy + - name: "🔰 Proxy" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Outbound.png proxies: - - '🇭🇰 Hong Kong' - - '🇨🇳 Taiwan' - - '🇸🇬 Singapore' - - '🇯🇵 Japan' - - '🇺🇸 America' - - '🇺🇳 Server' - - '🔘 DIRECT' - - name: '🎬 Streaming' + - 🇭🇰 Hong Kong + - 🇨🇳 Taiwan + - 🇸🇬 Singapore + - 🇯🇵 Japan + - 🇺🇸 America + - 🇺🇳 Server + - 🔘 DIRECT + + # Streaming Global + - name: "🎬 Streaming" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Streaming.png proxies: - - '🔰 Proxy' - - '🇭🇰 Hong Kong' - - '🇨🇳 Taiwan' - - '🇸🇬 Singapore' - - '🇯🇵 Japan' - - '🇺🇸 America' - - '🇺🇳 Server' - - name: '📺 CNTV' + - 🔰 Proxy + - 🇭🇰 Hong Kong + - 🇨🇳 Taiwan + - 🇸🇬 Singapore + - 🇯🇵 Japan + - 🇺🇸 America + - 🇺🇳 Server + + # CNTV APAC + - name: "📺 CNTV" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/StreamingCN.png proxies: - - '🔘 DIRECT' - - '🇨🇳 Taiwan' - - '🇭🇰 Hong Kong' - - name: '🍎 Apple' + - 🔘 DIRECT + - 🇨🇳 Taiwan + - 🇭🇰 Hong Kong + + # Apple + # > Apple Services + - name: "🍎 Apple" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Apple.png proxies: - - '🔘 DIRECT' - - '🔰 Proxy' - - '🇺🇸 America' - - '🇯🇵 Japan' - - name: '🔍 Google' + - 🔘 DIRECT + - 🔰 Proxy + - 🇺🇸 America + - 🇯🇵 Japan + + # Google + - name: "🔍 Google" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Google.png proxies: - - '🇺🇸 America' - - '🔰 Proxy' - - name: '☁️ OneDrive' + - 🇺🇸 America + - 🔰 Proxy + + # Microsoft + # > OneDrive + - name: "☁️ OneDrive" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/OneDrive.png proxies: - - '🔘 DIRECT' - - '🔰 Proxy' - - name: 'Ⓜ️ Microsoft' + - 🔘 DIRECT + - 🔰 Proxy + + # > Microsoft Services + - name: "Ⓜ️ Microsoft" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Microsoft.png proxies: - - '🔘 DIRECT' - - '🔰 Proxy' - - name: '📬 Telegram' + - 🔘 DIRECT + - 🔰 Proxy + + # Telegram + - name: "📬 Telegram" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Telegram.png proxies: - - '🔰 Proxy' - - '🇸🇬 Singapore' - - '🔘 DIRECT' - - name: '🤖 AIGC' + - 🔰 Proxy + - 🇸🇬 Singapore + - 🔘 DIRECT + + # AIGC + - name: "🤖 AIGC" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/ChatGPT.png proxies: - - '🇺🇸 America' - - '🇸🇬 Singapore' - - '🔰 Proxy' - - name: '🪙 Crypto' + - 🇺🇸 America + - 🇸🇬 Singapore + - 🔰 Proxy + + # Crypto + - name: "🪙 Crypto" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Cryptocurrency_3.png proxies: - - '🇺🇸 America' - - '🔰 Proxy' - - '🔘 DIRECT' - - name: '💳 Finance' + - 🇺🇸 America + - 🔰 Proxy + - 🔘 DIRECT + + # Finance + - name: "💳 Finance" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Finance.png proxies: - - '🇺🇸 America' - - '🔰 Proxy' - - '🔘 DIRECT' - - name: '📧 Mail' + - 🇺🇸 America + - 🔰 Proxy + - 🔘 DIRECT + + # Mail + - name: "📧 Mail" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Email.png proxies: - - '🔰 Proxy' - - '🔘 DIRECT' - - name: '⏱️ Speedtest' + - 🔰 Proxy + - 🔘 DIRECT + + # Speedtest + - name: "⏱️ Speedtest" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Speed.png - include-all: true - - name: '🚧 AdGuard' + use: + - Server + + # Adblock + - name: "🚧 AdGuard" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Block.png proxies: - - '🔘 DIRECT' - - '⛔️ REJECT' - - '📛 REJECT-DROP' - - name: '🔘 DIRECT' + - 🔘 DIRECT + - ⛔️ REJECT + - 📛 REJECT-DROP + + # DIRECT + - name: "🔘 DIRECT" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Direct.png hidden: true proxies: - - 'DIRECT' - - name: '⛔️ REJECT' + - DIRECT + + # REJECT + - name: "⛔️ REJECT" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Reject.png hidden: true proxies: - - 'REJECT' - - name: '📛 REJECT-DROP' + - REJECT + + # REJECT-DROP + - name: "📛 REJECT-DROP" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Reject.png hidden: true proxies: - - 'REJECT-DROP' - - name: '🇺🇳 Server' + - REJECT-DROP + + # Nodes + - name: "🇺🇳 Server" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Club.png - include-all: true - - name: '🇭🇰 Hong Kong' + use: + - Server + + # Area + - name: "🇭🇰 Hong Kong" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Hong_Kong.png - include-all: true + use: + - Server filter: '(?i)(?:🇭🇰|香港|Hong Kong|\b(?:HK|HKG)\d*\b)' - - name: '🇨🇳 Taiwan' + + - name: "🇨🇳 Taiwan" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Taiwan.png - include-all: true + use: + - Server filter: '(?i)(?:🇨🇳|🇹🇼|台湾|Taiwan|\b(?:TW|TWN)\d*\b)' - - name: '🇸🇬 Singapore' + + - name: "🇸🇬 Singapore" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Singapore.png - include-all: true + use: + - Server filter: '(?i)(?:🇸🇬|新加坡|Singapore|\b(?:SG|SGP)\d*\b)' - - name: '🇯🇵 Japan' + + - name: "🇯🇵 Japan" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Japan.png - include-all: true + use: + - Server filter: '(?i)(?:🇯🇵|日本|Japan|\b(?:JP|JPN)\d*\b)' - - name: '🇺🇸 America' + + - name: "🇺🇸 America" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/United_States.png - include-all: true + use: + - Server filter: '(?i)(?:🇺🇸|美国|United States|\b(?:US|USA)\d*\b)' -# ════════════════════════════════════════════════ -# 远程规则集(Stash 需显式声明 behavior + format) -# ════════════════════════════════════════════════ + +# 关于 Rule Provider 请查阅:https://wiki.metacubex.one/en/config/rule-providers/ + rule-providers: - 'Bypass': +# name: # Provider 名称 +# type: http # http 或 file +# behavior: classical # 或 ipcidr、domain +# path: # 文件路径 +# url: # 只有当类型为 HTTP 时才可用,您不需要在本地空间中创建新文件。 +# interval: # 自动更新间隔,仅在类型为 HTTP 时可用 + Bypass: behavior: classical format: yaml + path: ./Provider/RuleSet/Bypass.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Bypass.yaml interval: 86400 - 'Reroute': + + Reroute: behavior: classical format: yaml + path: ./Provider/RuleSet/Reroute.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Reroute.yaml interval: 86400 - 'Private': + + Private: behavior: domain format: mrs + path: ./Provider/RuleSet/Private.mrs url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite/private.mrs interval: 86400 - 'HTTPDNS': + + HTTPDNS: behavior: classical format: yaml + path: ./Provider/RuleSet/HTTPDNS.yaml url: https://fastly.jsdelivr.net/gh/VirgilClyne/GetSomeFries@main/ruleset/HTTPDNS.Block.yaml interval: 86400 - 'Reject': + + Reject: behavior: domain format: mrs + path: ./Provider/RuleSet/Reject.mrs url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Reject.mrs interval: 86400 - 'AdBlock': + + AdBlock: behavior: classical format: yaml + path: ./Provider/RuleSet/AdBlock.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Block.yaml interval: 86400 - 'Phishing': + + Phishing: behavior: domain format: mrs + path: ./Provider/RuleSet/Phishing.mrs url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Phishing.mrs interval: 86400 - 'Bogus': + + Bogus: behavior: classical format: yaml + path: ./Provider/RuleSet/Bogus.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Bogus.yaml interval: 86400 - 'Streaming_TW': + + Streaming_TW: behavior: classical format: yaml + path: ./Provider/RuleSet/Streaming_TW.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming_TW.yaml interval: 86400 - 'Streaming_JP': + + Streaming_JP: behavior: classical format: yaml + path: ./Provider/RuleSet/Streaming_JP.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming_JP.yaml interval: 86400 - 'Streaming_US': + + Streaming_US: behavior: classical format: yaml + path: ./Provider/RuleSet/Streaming_US.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming_US.yaml interval: 86400 - 'Streaming': + + Streaming: behavior: classical format: yaml + path: ./Provider/RuleSet/Streaming.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming.yaml interval: 86400 - 'CNTV': + + CNTV: behavior: classical format: yaml + path: ./Provider/RuleSet/CNTV.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/CNTV.yaml interval: 86400 - 'Google AI Studio': + + Google AI Studio: behavior: classical format: yaml + path: ./Provider/RuleSet/Google_AI_Studio.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Gemini.yaml interval: 86400 - 'AIGC': + + AIGC: behavior: classical format: yaml + path: ./Provider/RuleSet/AIGC.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/GenAI.yaml interval: 86400 - 'Apple CN': + + Apple CN: behavior: classical format: yaml + path: ./Provider/RuleSet/Apple_CN.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Apple%20CN.yaml interval: 86400 - 'Apple': + + Apple: behavior: classical format: yaml + path: ./Provider/RuleSet/Apple.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Apple.yaml interval: 86400 - 'Google': + + Google: behavior: classical format: yaml + path: ./Provider/RuleSet/Google.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Google.yaml interval: 86400 - 'OneDrive': + + OneDrive: behavior: classical format: yaml + path: ./Provider/RuleSet/OneDrive.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/OneDrive.yaml interval: 86400 - 'Microsoft': + + Microsoft: behavior: classical format: yaml + path: ./Provider/RuleSet/Microsoft.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Microsoft.yaml interval: 86400 - 'Telegram': + + Telegram: behavior: classical format: yaml + path: ./Provider/RuleSet/Telegram.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Telegram.yaml interval: 86400 - 'Crypto': + + Crypto: behavior: classical format: yaml + path: ./Provider/RuleSet/Crypto.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Crypto.yaml interval: 86400 - 'Finance': + + Finance: behavior: classical format: yaml + path: ./Provider/RuleSet/Finance.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Finance.yaml interval: 86400 - 'Spark': + + Spark: behavior: classical format: yaml + path: ./Provider/RuleSet/Spark.yaml url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Spark.yaml interval: 86400 - 'Speedtest': + + Speedtest: behavior: domain format: mrs + path: ./Provider/RuleSet/Speedtest.mrs url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Speedtest.mrs interval: 86400 - 'Global': + + Global: behavior: domain format: mrs + path: ./Provider/RuleSet/Global.mrs url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite/geolocation-!cn.mrs interval: 86400 - 'China': + + China: behavior: domain format: mrs + path: ./Provider/RuleSet/China.mrs url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite/cn.mrs interval: 86400 - 'China ASN': + + China ASN: behavior: classical format: yaml + path: ./Provider/RuleSet/China_ASN.yaml url: https://fastly.jsdelivr.net/gh/VirgilClyne/GetSomeFries@main/ruleset/ASN.China.yaml interval: 86400 - 'China IP': + + China IP: behavior: ipcidr format: mrs + path: ./Provider/RuleSet/China_IP.mrs url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geoip/cn.mrs interval: 86400 - 'LAN': + + LAN: behavior: ipcidr format: mrs + path: ./Provider/RuleSet/LAN.mrs url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/lancidr.mrs interval: 86400 -# ════════════════════════════════════════════════ -# 分流规则:#!replace 整体替换,顺序即优先级 -# 与源配置逐条一致——Stash 支持 AND/OR/NOT 逻辑规则(含嵌套)、GEOSITE、 -# no-resolve 及 REJECT-DROP,无需改写。 -# 注意 GEOSITE 数据不随 Stash 分发,首次使用时按需从 github.com 拉取。 -# ════════════════════════════════════════════════ -rules: #!replace +# 规则 +rules: # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) - AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT @@ -414,30 +673,3 @@ rules: #!replace # Final - MATCH,🔰 Proxy - - -# ════════════════════════════════════════════════ -# DNS:仅覆盖 Stash 与 mihomo 写法不同之处 -# ════════════════════════════════════════════════ -dns: - # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」,Stash 的 - # 等价物是全局开关 follow-rule。官方提示多数场景无需开启(可能影响 CDN - # 优化并轻微增加延迟),如需 DNS 直连改为 false 即可。 - follow-rule: true - - nameserver: #!replace - - 'https://1.1.1.1/dns-query' - - # 独立解析代理服务器域名,避免 follow-rule 下的递归查询。 - proxy-server-nameserver: #!replace - - 'https://doh.pub/dns-query' - - 'https://dns.alidns.com/dns-query' - - # 源配置把这些域名逗号拼成单键(mihomo 专属),按 Stash 语法拆为独立键。 - nameserver-policy: - 'time.*.com': [223.5.5.5, 119.29.29.29] - 'time.*.gov': [223.5.5.5, 119.29.29.29] - 'time.*.apple.com': [223.5.5.5, 119.29.29.29] - 'ntp.*.com': [223.5.5.5, 119.29.29.29] - '+.pool.ntp.org': [223.5.5.5, 119.29.29.29] - '*.ntp.org.cn': [223.5.5.5, 119.29.29.29] From 5fcfdc8687b95001a6e3740c92de39456696fd63 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 11:34:16 +0000 Subject: [PATCH 06/28] fix(sync): drop invented metadata keys from the Stash override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generator was inserting name/desc/author YAML keys into the output. Those keys do not exist in Clash/Sample.yaml and are not a Stash-vs-mihomo difference, so adding them went beyond transcribing the source — and author: duplicated the existing "# Author: @HotKids" header comment. The override keeps only the source header plus the one generated-by notice; .stoverride name/desc are display-only and optional, so Stash falls back to the filename. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 9 ++------- Clash/Stash.stoverride | 8 +------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index f4db181da..79a2f0f42 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4290,16 +4290,11 @@ def flush() -> None: out.append(f' "{dom}":') out.extend(policy_val) - # 在文件头的 # Author 之后补上生成说明与覆写元数据 + # 在文件头的 # Author 之后补一行生成说明(覆写的 name/desc 仅用于展示, + # 源文件没有这些键,不属于差异点,不自行添加) insert_at = next((i for i, l in enumerate(out) if l.startswith("# Author:")), 0) + 1 out[insert_at:insert_at] = [ "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", - "", - "name: HotKids", - "desc: |-", - " HotKids 规则配置 · Stash 覆写", - " 由 Surge/Profile.conf 经 Clash/Sample.yaml 转译,仅改写 Stash 与 mihomo 的差异点", - "author: HotKids", ] body = "\n".join(out).rstrip() + "\n" diff --git a/Clash/Stash.stoverride b/Clash/Stash.stoverride index 81b276c59..15ea093d8 100644 --- a/Clash/Stash.stoverride +++ b/Clash/Stash.stoverride @@ -1,14 +1,8 @@ # Stash -# Date: 2026-08-28 19:29:18 +# Date: 2026-08-28 19:33:58 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 -name: HotKids -desc: |- - HotKids 规则配置 · Stash 覆写 - 由 Surge/Profile.conf 经 Clash/Sample.yaml 转译,仅改写 Stash 与 mihomo 的差异点 -author: HotKids - # 通用设置 From 506e579f5fd020ece4271244057a930f79b015d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 11:35:50 +0000 Subject: [PATCH 07/28] chore(sync): move Stash.stoverride under Clash/Script/ Puts the override next to the other generated client artifacts. All four references follow: the sync-config.txt platform block output path, the sync-config workflow's git add list (Clash/Script/ already covers the new path, so the separate entry is dropped), and both READMEs. The stale platform-block comment describing the old diff-only model is rewritten to match what the generator now does. Safe to live there: the Script-directory prune in sync-config.py only globs *.js and only unlinks files carrying the generation marker, so a .stoverride is never touched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/README.md | 50 ++++++++++++++++------------- .github/scripts/sync-config.txt | 6 ++-- .github/workflows/sync-config.yml | 2 +- Clash/{ => Script}/Stash.stoverride | 0 README.md | 2 +- 5 files changed, 33 insertions(+), 27 deletions(-) rename Clash/{ => Script}/Stash.stoverride (100%) diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 260e9359e..9deaa2712 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -55,7 +55,7 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do ## `sync-config.py` — 配置文件同步 **源**:`Surge/Profile.conf` -**目标**:`Clash/Sample.yaml`、`Clash/Mihomo.yaml`、`Clash/Stash.stoverride`、`Clash/Script/Script.js`、`Clash/Script/MyScript.js`、`Clash/Script/MyScriptColor.js`、`Clash/Script/MyClashBox.js`、`Surge/Balloon.lcf`(Loon)、`Quantumult/Sample.conf`、`Surge/Surfboard.conf`、`sing-box/config.json` +**目标**:`Clash/Sample.yaml`、`Clash/Mihomo.yaml`、`Clash/Script/Stash.stoverride`、`Clash/Script/Script.js`、`Clash/Script/MyScript.js`、`Clash/Script/MyScriptColor.js`、`Clash/Script/MyClashBox.js`、`Surge/Balloon.lcf`(Loon)、`Quantumult/Sample.conf`、`Surge/Surfboard.conf`、`sing-box/config.json` 各平台静态头部由 `sync-config/` 下的 ini 文件提供(支持 `<< path` / `<< https://url` 引用)。sing-box 完整配置以 `sync-config/sing-box.ini`(JSON 内容)为静态基座——仅保留 `sniff`/`hijack-dns`(sing-box 专属基础设施,Surge 无等价规则);`route.rules`/`route.rule_set` 其余全部(含 QUIC 拦截、SSH 直连、私有网络、CN/geo、各服务分流)从 `[Rule]` 生成后 splice 进哨兵位——自有清单用本仓库 `.srs`,Loyalsoldier/VirgilClyne 等外部规则集映射到 SagerNet 官方等价规则集。 @@ -65,27 +65,33 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do `<<: *Region, filter: *Filter<码>`(`include-all-providers` 与 `use:` 同走 mihomo 保序路径,功能一致)。 -`Clash/Stash.stoverride` 是 `Clash/Sample.yaml` 生成完毕后推导出的 **Stash 通用覆写**, -与 `Clash/Script/Script.js` 同一定位——把本仓库整套策略组 / 规则集 / 规则套到**任意订阅**上 -(Script.js 面向支持 Enhance Script 的 Clash 客户端,Stash 不支持 JS,改用 `.stoverride`)。 -因此它不依赖本仓库自身的 proxy-providers:节点来自使用者的订阅。转译规则如下。 - -- **策略组**:`use: [Server]` → `include-all: true`(引用订阅全部代理与远程代理集),地区组的 - `filter` 正则原样保留继续按节点名筛地区。其余字段(`icon` / `proxies` / `hidden`)不变。 -- **规则集**:保留 `behavior` + `format`(Stash 需显式声明;MRS 支持 `behavior` 为 - `domain` / `ipcidr`,本仓库 8 个 mrs 规则集正好全在此范围内);`type` 是 mihomo 专属, - `path` 是本地缓存语义(Stash 会限制在自身资源目录内),二者均略去。 -- **规则**:`rules: #!replace` 整体替换,与源配置**逐条完全一致**,仅保留原注释与顺序。 - 无需任何改写——Stash 的规则类型是 Clash Premium 超集,我们用到的 `RULE-SET` / `GEOIP` / - `GEOSITE` / `MATCH` / `no-resolve`,以及 `AND` / `OR` / `NOT` 逻辑规则(含嵌套)和内置策略 - `REJECT` / `REJECT-DROP`,官方文档均明确支持。 -- **DNS**:只覆盖写法不同的两处——`#RULES` 后缀 → 全局 `dns.follow-rule`(并 `#!replace` - 替换 nameserver 数组去掉该后缀,Stash 的 `#` 片段只承载 `h3=true` 这类选项); - `nameserver-policy` 里逗号拼接的多域名单键(mihomo 专属)拆成 Stash 认的独立键 - (官方只支持精确域名 / 通配域名 / `geosite:` 三种键形式)。 - -唯一需要留意的运行时依赖:`GEOSITE` 的 domain-list-community 数据不随 Stash 分发,首次使用时 -按需从 github.com 拉取,需保证当时的配置能正常访问 github.com。 +`Clash/Script/Stash.stoverride` 是 `Clash/Sample.yaml` 的二次转换产物,与 `Clash/Mihomo.yaml` +同一定位:整份配置逐行转录(含注释与排版),**只改写 Stash 与 mihomo 真正有差异的点**,因此 +可直接作为覆写文件导入 Stash 使用。差异点仅以下四类: + +- **略去 mihomo 专属的顶层键 / 整块**(连同其前置注释):监听与控制面(`mixed-port` / + `allow-lan` / `bind-address` / `external-controller`)、`ipv6`、geo 数据源(`geodata-loader` / + `geox-url` / `geo-auto-update` / `geo-update-interval`)、`unified-delay` / `tcp-concurrent` / + `find-process-mode` / `global-ua` / `keep-alive-interval`,以及 `profile` / `ntp` / `sniffer` / + `tun` 和空占位 `proxies`。这些能力在 Stash 由 App 自身掌管或无对应项。 +- **DNS 子键过滤**:只保留 Stash 文档支持的 `default-nameserver` / `nameserver` / + `nameserver-policy` / `proxy-server-nameserver` / `fake-ip-filter`,其余 14 个 mihomo 专属键 + (`enhanced-mode` / `fake-ip-range` / `cache-algorithm` / `direct-nameserver` 等)略去。 +- **DNS 写法转换**:mihomo 用每条 nameserver 的 `#RULES` 后缀表达「跟随规则」,Stash 是全局 + 开关 → 补 `follow-rule: true`,并用 `#!replace` 整体替换 nameserver 数组以去掉该后缀(Stash 的 + `#` 片段只承载 `h3=true` 这类选项);`nameserver-policy` 里逗号拼接的多域名单键是 mihomo 专属, + 按 Stash 语法拆成独立键(官方只支持精确域名 / 通配域名 / `geosite:`)。 +- **Provider 字段**:去掉 mihomo 专属的 `type`;proxy-providers 的 `header` 改为 Stash 文档 + 拼写的 `headers`。 + +其余内容——`hosts` / `mode` / `log-level`、23 个策略组(含 `use: [Server]` 与地区 `filter`)、 +30 个规则集、36 条规则——全部原样保留:Stash 的规则类型是 Clash Premium 超集,我们用到的 +`RULE-SET` / `GEOIP` / `GEOSITE` / `MATCH` / `no-resolve`、`AND` / `OR` / `NOT` 逻辑规则(含嵌套) +及内置策略 `REJECT` / `REJECT-DROP`,官方文档均明确支持。 + +两点需留意:`GEOSITE` 的 domain-list-community 数据不随 Stash 分发,首次使用时按需从 github.com +拉取;`format: mrs` 的 MRS 支持有官方说明(限 `behavior` 为 `domain` / `ipcidr`,本仓库 8 个 mrs +规则集正好全在此范围内),但格式表未列出该 `format` 取值,沿用 mihomo 写法,需实测确认。 `Clash/Script/Script.js` 是 `Clash/Mihomo.yaml` 生成完毕后再解析出来的等效 mihomo 覆写 脚本(Enhance Script),供 Clash Verge Rev / FlClash / Bettbox 等客户端直接对任意订阅动态生成同一套策略组 / diff --git a/.github/scripts/sync-config.txt b/.github/scripts/sync-config.txt index abee2b499..4160a2f17 100644 --- a/.github/scripts/sync-config.txt +++ b/.github/scripts/sync-config.txt @@ -61,9 +61,9 @@ rule-providers: geoip/cn => China IP # Stash -# 覆写文件:只输出 Stash 与 mihomo 的差异项(DNS follow-rule / nameserver-policy 拆键 / -# QUIC Script Shortcut),其余沿用上面的 Clash 产物。无 Builtin,内容全部由脚本推导。 ->> Clash/Stash.stoverride +# 覆写文件:由上面的 Clash 产物二次转换(同 Mihomo.yaml 定位),整份逐行转录, +# 只改写 Stash 与 mihomo 的差异点。无 Builtin,内容全部由脚本推导。 +>> Clash/Script/Stash.stoverride # Quantumult X >> Quantumult/Sample.conf diff --git a/.github/workflows/sync-config.yml b/.github/workflows/sync-config.yml index a4901d867..0252d3698 100644 --- a/.github/workflows/sync-config.yml +++ b/.github/workflows/sync-config.yml @@ -34,7 +34,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Clash/Sample.yaml Clash/Mihomo.yaml Clash/Stash.stoverride Clash/Script/ Surge/Balloon.lcf Quantumult/Sample.conf Surge/Surfboard.conf sing-box/config.json + git add Clash/Sample.yaml Clash/Mihomo.yaml Clash/Script/ Surge/Balloon.lcf Quantumult/Sample.conf Surge/Surfboard.conf sing-box/config.json if git diff --cached --quiet; then echo "No changes to commit" else diff --git a/Clash/Stash.stoverride b/Clash/Script/Stash.stoverride similarity index 100% rename from Clash/Stash.stoverride rename to Clash/Script/Stash.stoverride diff --git a/README.md b/README.md index f572c9ecd..a30364e8f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ | 目录 | 说明 | |---|---| | [`Surge/`](Surge/) | **单一来源**:托管配置 `Profile.conf`、规则源 `RULE-SET/`、sgmodule 模块 | -| [`Clash/`](Clash/) | 自动生成:`Sample.yaml` / `Mihomo.yaml`(锚点版)/ `Stash.stoverride`(Stash 通用覆写)/ 规则集 / Enhance Script | +| [`Clash/`](Clash/) | 自动生成:`Sample.yaml` / `Mihomo.yaml`(锚点版)/ 规则集 / Enhance Script 与 `Script/Stash.stoverride`(Stash 覆写) | | [`Quantumult/`](Quantumult/) | 自动生成:QX 配置与规则;手动维护:图标库 `X/Images/` | | [`sing-box/`](sing-box/) | 自动生成:完整配置 `config.json`、规则集(`source/` 源码 + `rule-set/` 二进制) | | [`subconverter/`](subconverter/) | subconverter 订阅转换远程配置(已弃用,仅维护 emoji) | From 07fad010e22027f871b21634f0311713a8fc7bc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 11:37:13 +0000 Subject: [PATCH 08/28] fix(sync): don't stack blank lines across dropped keys in the Stash override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropping a mihomo-only key also drops its comment block, but the blank line before it and the blank line after it then landed next to each other, producing gaps the source does not have — most visibly right under "# 通用设置", where Sample.yaml has a single blank line and the override had two. The flush now discards leading blanks from the pending buffer when the output already ends on one, so blank runs that exist in Sample.yaml are preserved and only the ones created by a removal collapse. Output now has the same blank-line profile as the source. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 4 ++++ Clash/Script/Stash.stoverride | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 79a2f0f42..6f32afa9e 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4170,6 +4170,10 @@ def _sync_stash(config: dict) -> None: changes: list[str] = [] def flush() -> None: + # 略去某个键时它两侧的空行会跨过删除点叠在一起;这里丢掉与已输出空行相邻的 + # 前导空行,避免出现源文件没有的空行堆积(源本身的空行结构保持不变)。 + while buf and not buf[0].strip() and out and not out[-1].strip(): + buf.pop(0) out.extend(buf) buf.clear() diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 15ea093d8..215936919 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,11 +1,10 @@ # Stash -# Date: 2026-08-28 19:33:58 +# Date: 2026-08-28 19:36:56 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 # 通用设置 - # 代理模式:rule(规则)/ global(全局)/ direct(直连) mode: rule From 1c343ca77b4aa265288a922fddb20f9c3fd497f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 11:40:21 +0000 Subject: [PATCH 09/28] chore(sync): label the Stash override header "# Stash for Android" Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 2 +- Clash/Script/Stash.stoverride | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 6f32afa9e..a0568616a 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4182,7 +4182,7 @@ def flush() -> None: first_key = next((i for i, l in enumerate(src) if _TOP_KEY_RE.match(l)), 0) header = [l.rstrip() for l in src[:first_key]] if header and header[0].startswith("# Clash"): - header[0] = "# Stash" + header[0] = "# Stash for Android" # 紧贴首个键的那段注释是该键的说明(首个键必然是被略去的 mixed-port), # 随它一起去掉,避免留下孤儿注释;靠空行分隔的分区标题(# 通用设置)保留。 while header and header[-1].lstrip().startswith("#"): diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 215936919..6cf7ab910 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,5 +1,5 @@ -# Stash -# Date: 2026-08-28 19:36:56 +# Stash for Android +# Date: 2026-08-28 19:40:13 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 From b93600ad8405facf1c52813bd4317250bd8bb5b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 11:48:20 +0000 Subject: [PATCH 10/28] feat(sync): generate MyStash.stoverride from the same overlay as MyScript.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reuses Enhanced/myscript.overlay.json for the Stash side: an overlay that declares stash_output alongside output now also produces a Stash override with the same personal differences layered on. Only myscript.overlay.json declares it. The overlay's difference declarations are format-agnostic, but _apply_overlay works on parsed structures for Script.js while the Stash side is a text-level transcription that has to keep Sample.yaml's comments and layout, so the directives are re-implemented over text in _stash_apply_overlay. Anything not implemented there raises rather than being skipped, so a personal difference is never silently dropped. Two directives needed a decision because they have no static equivalent: - disabled_by_default is a Script.js runtime toggle; the group is pruned instead — its rules, the references to it in other groups' candidate lists, and any rule-provider left unreferenced afterwards all go with it. - extra_pool_groups are filled at runtime from config.proxies in Script.js; a static file needs an explicit node source, so they are emitted as use: [Server] + filter, matching how the base region groups are written. Result for myscript.overlay.json: 29 groups (23 - Telegram + 7 relay/region), 29 rule-providers (Telegram's pruned) and 42 rules (36 - 1 + 7 inserted), with the same blank-line profile as the source. Generation is idempotent. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/README.md | 15 +- .github/scripts/sync-config.py | 196 +++++ .../Enhanced/myscript.overlay.json | 2 + Clash/Script/MyStash.stoverride | 718 ++++++++++++++++++ 4 files changed, 930 insertions(+), 1 deletion(-) create mode 100644 Clash/Script/MyStash.stoverride diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 9deaa2712..a517ad9c5 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -55,7 +55,7 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do ## `sync-config.py` — 配置文件同步 **源**:`Surge/Profile.conf` -**目标**:`Clash/Sample.yaml`、`Clash/Mihomo.yaml`、`Clash/Script/Stash.stoverride`、`Clash/Script/Script.js`、`Clash/Script/MyScript.js`、`Clash/Script/MyScriptColor.js`、`Clash/Script/MyClashBox.js`、`Surge/Balloon.lcf`(Loon)、`Quantumult/Sample.conf`、`Surge/Surfboard.conf`、`sing-box/config.json` +**目标**:`Clash/Sample.yaml`、`Clash/Mihomo.yaml`、`Clash/Script/Stash.stoverride`、`Clash/Script/MyStash.stoverride`、`Clash/Script/Script.js`、`Clash/Script/MyScript.js`、`Clash/Script/MyScriptColor.js`、`Clash/Script/MyClashBox.js`、`Surge/Balloon.lcf`(Loon)、`Quantumult/Sample.conf`、`Surge/Surfboard.conf`、`sing-box/config.json` 各平台静态头部由 `sync-config/` 下的 ini 文件提供(支持 `<< path` / `<< https://url` 引用)。sing-box 完整配置以 `sync-config/sing-box.ini`(JSON 内容)为静态基座——仅保留 `sniff`/`hijack-dns`(sing-box 专属基础设施,Surge 无等价规则);`route.rules`/`route.rule_set` 其余全部(含 QUIC 拦截、SSH 直连、私有网络、CN/geo、各服务分流)从 `[Rule]` 生成后 splice 进哨兵位——自有清单用本仓库 `.srs`,Loyalsoldier/VirgilClyne 等外部规则集映射到 SagerNet 官方等价规则集。 @@ -93,6 +93,19 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do 拉取;`format: mrs` 的 MRS 支持有官方说明(限 `behavior` 为 `domain` / `ipcidr`,本仓库 8 个 mrs 规则集正好全在此范围内),但格式表未列出该 `format` 取值,沿用 mihomo 写法,需实测确认。 +`Clash/Script/MyStash.stoverride` 是 Stash 的私人定制版,复用 `Enhanced/` 下的同一份 +overlay:只要 overlay 里除 `output` 外再声明一个 `stash_output`,就会在 Stash 基座上叠加同样 +的私人差异(目前只有 `myscript.overlay.json` 声明了)。overlay 的差异声明本身与输出格式无关, +但 `_apply_overlay` 面向解析后的结构、供 Script.js 使用,而 Stash 侧是文本级转译(要保住 +Sample.yaml 的注释与排版),因此这些指令在 `_stash_apply_overlay` 里按文本重新实现,**遇到尚未 +实现的指令直接报错**,避免私人差异被静默丢掉。 + +其中 `disabled_by_default` 没有静态等价物——它是 Script.js 的运行时开关(`ruleOptionsEnable`), +YAML 覆写没有「默认关但可开」这种状态。因此按声明**整组剪掉**:删组、删以它为落点的规则、删 +其余分组候选里对它的引用,并清掉因此不再被任何 `RULE-SET` 引用的规则集。`extra_pool_groups` +的新增池组在 Script.js 里靠运行时过滤 `config.proxies` 填充,静态 YAML 必须显式写节点来源, +统一按基座地区组的写法输出 `use: [Server]` + `filter`。 + `Clash/Script/Script.js` 是 `Clash/Mihomo.yaml` 生成完毕后再解析出来的等效 mihomo 覆写 脚本(Enhance Script),供 Clash Verge Rev / FlClash / Bettbox 等客户端直接对任意订阅动态生成同一套策略组 / 规则 / 基础设置,无需依赖本仓库自身的 proxy-providers。它只读 Mihomo.yaml 的解析结果、 diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index a0568616a..38bcfe771 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4138,6 +4138,11 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - _SUB_KEY_RE = re.compile(r"^(\s+)(['\"]?)([^:'\"]+)\2\s*:") +def _yq(value) -> str: + """YAML 单引号标量(组名 / filter 正则含 emoji、空格、反斜杠,统一加引号最稳)。""" + return "'" + str(value).replace("'", "''") + "'" + + def _stash_clean_nameserver(server: str) -> str: """mihomo 的 nameserver 策略后缀(#RULES / #策略名)在 Stash 中不存在——Stash 的 `#` 片段只承载选项(如 h3=true)。保留 h3= 这类合法选项,其余后缀一律剥离。""" @@ -4307,6 +4312,197 @@ def flush() -> None: print(f" · {note}") print(f" {'✓ ' + out_path + ' 已更新' if changed else '✓ ' + out_path + ' 无变化'}") + _sync_stash_overlays(out) + + +# ── Enhanced/*.overlay.json → Stash 私人定制版 ──────────────────────────── +# +# overlay 声明的是「相对基座的私人差异」,本身与输出格式无关(_apply_overlay 面向 +# 解析后的 groups/rules 结构,供 Script.js 使用)。Stash 侧是文本级转译(要保住 +# Sample.yaml 的注释与排版),因此这里按同一份 overlay 在文本层实现对应改写。 +# 只有声明了 stash_output 的 overlay 才会产出 .stoverride;未实现的指令直接报错, +# 避免私人差异被静默丢掉。 +_STASH_OVERLAY_OK = { + "_comment", "output", "stash_output", "extends", + "disabled_by_default", "rules_insert", "group_overrides", + "group_proxies_insert", "extra_pool_groups", +} + + +def _stash_group_spans(lines: list[str]) -> dict[str, tuple[int, int]]: + """定位 proxy-groups 块内每个组的行区间 {组名: (起, 止)}(止为开区间)。""" + try: + start = lines.index("proxy-groups:") + except ValueError: + return {} + end = next((i for i in range(start + 1, len(lines)) + if lines[i] and not lines[i].startswith((" ", "#"))), len(lines)) + spans: dict[str, tuple[int, int]] = {} + cur, cur_start = None, None + for i in range(start + 1, end): + m = re.match(r"^ - name:\s*(.+?)\s*$", lines[i]) + if m: + if cur is not None: + spans[cur] = (cur_start, i) + cur, cur_start = m.group(1).strip().strip("'\""), i + if cur is not None: + spans[cur] = (cur_start, end) + return spans + + +def _stash_render_group(g: dict) -> list[str]: + """按 Sample.yaml 的字段顺序渲染一个新增策略组。""" + out = [f" - name: {_yq(g['name'])}", f" type: {g.get('type', 'select')}"] + if g.get("icon"): + out.append(f" icon: {g['icon']}") + # 池组的节点来源:与基座地区组写法一致,从本仓库 provider 里按 filter 筛 + out.append(" use:") + out.append(" - Server") + for key in ("interval", "tolerance", "lazy"): + if key in g: + out.append(f" {key}: {g[key]}") + if g.get("hidden"): + out.append(" hidden: true") + if g.get("filter"): + out.append(f" filter: {_yq(g['filter'])}") + return out + + +def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[str]: + """把一份 overlay 的差异叠加到已转译好的 Stash 文本上(就地返回新列表)。""" + unknown = set(overlay) - _STASH_OVERLAY_OK + if unknown: + raise ValueError( + f"{label}: Stash 转译尚未实现这些 overlay 指令 {sorted(unknown)};" + f"请在 _stash_apply_overlay 中补齐,避免私人差异被静默丢掉" + ) + lines = list(lines) + notes: list[str] = [] + + # 1) group_overrides:改写既有组的字段(filter 为 null 表示删掉该行) + for name, patch in (overlay.get("group_overrides") or {}).items(): + span = _stash_group_spans(lines).get(name) + if span is None: + raise ValueError(f"{label}: group_overrides 引用了不存在的分组 {name!r}") + s, e = span + for key, val in patch.items(): + idx = next((i for i in range(s, e) + if re.match(rf"^ {re.escape(key)}:", lines[i])), None) + if val is None: + if idx is not None: + del lines[idx] + continue + rendered = (f" {key}: {_yq(val)}" if key == "filter" + else f" {key}: {'true' if val is True else val}") + if idx is not None: + lines[idx] = rendered + else: + # 插到 name/type 之后,保持字段顺序稳定 + lines.insert(s + 2, rendered) + notes.append(f"{name}: 覆盖 {'/'.join(patch)}") + + # 2) group_proxies_insert:在候选列表里紧邻锚点插入 + for name, spec in (overlay.get("group_proxies_insert") or {}).items(): + span = _stash_group_spans(lines).get(name) + if span is None: + raise ValueError(f"{label}: group_proxies_insert 引用了不存在的分组 {name!r}") + s, e = span + anchor = spec.get("after") or spec.get("before") + idx = next((i for i in range(s, e) + if lines[i].strip().strip("-").strip().strip("'\"") == anchor), None) + if idx is None: + raise ValueError(f"{label}: {name} 的候选里找不到锚点 {anchor!r}") + at = idx + 1 if spec.get("after") else idx + lines[at:at] = [f" - {_yq(p)}" for p in spec["insert"]] + notes.append(f"{name}: 候选插入 {len(spec['insert'])} 项") + + # 3) extra_pool_groups:整组新增,插到锚点组之后 + for g in (overlay.get("extra_pool_groups") or []): + spans = _stash_group_spans(lines) + anchor = g.get("insert_after") + if anchor not in spans: + raise ValueError(f"{label}: extra_pool_groups 的锚点分组 {anchor!r} 不存在") + at = spans[anchor][1] + lines[at:at] = _stash_render_group(g) + notes.append(f"新增分组 {g['name']}") + + # 4) rules_insert:在锚点规则前/后插入 + for spec in (overlay.get("rules_insert") or []): + anchor = spec.get("after") or spec.get("before") + idx = next((i for i, l in enumerate(lines) + if l.startswith(" - ") and anchor in l), None) + if idx is None: + raise ValueError(f"{label}: rules_insert 找不到锚点规则 {anchor!r}") + at = idx + 1 if spec.get("after") else idx + lines[at:at] = [f" - {r}" for r in spec["rules"]] + notes.append(f"规则插入 {len(spec['rules'])} 条") + + # 5) disabled_by_default:静态配置没有运行时开关,按声明整组剪掉—— + # 删组、删以它为落点的规则、删其余组候选里对它的引用, + # 最后清掉因此不再被任何 RULE-SET 引用的规则集。 + for name in (overlay.get("disabled_by_default") or []): + spans = _stash_group_spans(lines) + if name not in spans: + raise ValueError(f"{label}: disabled_by_default 引用了不存在的分组 {name!r}") + s, e = spans[name] + # 组前的注释行一并删掉 + while s > 0 and lines[s - 1].lstrip().startswith("#"): + s -= 1 + del lines[s:e] + lines = [l for l in lines + if not (l.startswith(" - ") and l.rstrip().endswith(name)) + and not (l.strip().startswith("- ") and l.strip().strip("-").strip().strip("'\"") == name)] + notes.append(f"剪掉分组 {name}(含其规则与候选引用)") + + # 清理不再被引用的规则集 + used = {m.group(1) for l in lines if (m := re.match(r"^ - RULE-SET,([^,]+),", l))} + try: + rp = lines.index("rule-providers:") + rp_end = next((i for i in range(rp + 1, len(lines)) + if lines[i] and not lines[i].startswith((" ", "#"))), len(lines)) + except ValueError: + rp, rp_end = -1, -1 + if rp >= 0: + kept, i, dropped = [], rp + 1, [] + while i < rp_end: + m = re.match(r"^ (['\"]?)([^:'\"]+)\1:\s*$", lines[i]) + if m: + nm = m.group(2).strip() + j = i + 1 + while j < rp_end and (not lines[j].strip() or lines[j].startswith(" ")): + j += 1 + if nm not in used: + dropped.append(nm) + else: + kept.extend(lines[i:j]) + i = j + continue + kept.append(lines[i]) + i += 1 + if dropped: + lines[rp + 1:rp_end] = kept + notes.append(f"清理无引用规则集 {', '.join(dropped)}") + + for n in notes: + print(f" · {n}") + return lines + + +def _sync_stash_overlays(base_lines: list[str]) -> None: + """为声明了 stash_output 的 overlay 各产出一份 Stash 定制版覆写。""" + enhanced = REPO_ROOT / ".github" / "scripts" / "sync-config" / "Enhanced" + for path in sorted(enhanced.glob("*.overlay.json")): + overlay = json.loads(path.read_text(encoding="utf-8")) + target = overlay.get("stash_output") + if not target: + continue + print(f" ── overlay: {path.name} → {target} ──") + lines = _stash_apply_overlay(base_lines, overlay, path.name) + body = "\n".join(lines).rstrip() + "\n" + changed = _write_stamped_if_changed(REPO_ROOT / target, body) + print(f" {'✓ ' + target + ' 已更新' if changed else '✓ ' + target + ' 无变化'}") + + # --------------------------------------------------------------------------- # 主函数 # --------------------------------------------------------------------------- diff --git a/.github/scripts/sync-config/Enhanced/myscript.overlay.json b/.github/scripts/sync-config/Enhanced/myscript.overlay.json index 82ef157d2..7667fb2b9 100644 --- a/.github/scripts/sync-config/Enhanced/myscript.overlay.json +++ b/.github/scripts/sync-config/Enhanced/myscript.overlay.json @@ -3,6 +3,8 @@ "output": "Clash/Script/MyScript.js", + "stash_output": "Clash/Script/MyStash.stoverride", + "disabled_by_default": ["📬 Telegram"], "rules_insert": [ diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride new file mode 100644 index 000000000..a2a5c2311 --- /dev/null +++ b/Clash/Script/MyStash.stoverride @@ -0,0 +1,718 @@ +# Stash for Android +# Date: 2026-08-28 19:47:10 +# Author: @HotKids +# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 + +# 通用设置 + +# 代理模式:rule(规则)/ global(全局)/ direct(直连) +mode: rule + +# 日志等级:silent / error / warning / info / debug +log-level: info + +# ── Hosts ── + +# 静态域名映射,优先级高于 DNS 解析 +hosts: + 'localhost': 127.0.0.1 + +# ── DNS ── + +dns: + # DNS 查询跟随规则出站(mihomo 用 nameserver 的 #RULES 后缀表达, + # Stash 为全局开关)。官方提示多数场景无需开启:DNS 经代理转发可能 + # 破坏云服务商 CDN 优化并轻微增加延迟。如需 DNS 直连改为 false。 + # 下方 proxy-server-nameserver 已为代理服务器域名提供独立解析, + # 满足官方要求的前置条件之一(避免递归查询)。 + follow-rule: true + + # 引导 DNS:仅用于解析 nameserver 服务器的域名 + # 只能填纯 IP 地址 + default-nameserver: + - 223.5.5.5 + - 119.29.29.29 + fake-ip-filter: + # 本地域名 + - "*.lan" + - "+.lan" + - "*.local" + - "*.localdomain" + - "*.home.arpa" + - "*.localhost" + - "WORKGROUP" + # NTP 时间同步 + - "time.*.com" + - "time.*.gov" + - "time.*.apple.com" + - "ntp.*.com" + - "+.pool.ntp.org" + - "*.ntp.org.cn" + # STUN / TURN(WebRTC / 游戏打洞) + - "+.stun.*" + - "*.stun.*.*" + - "*.turn.twilio.com" + - "*.stun.twilio.com" + - "stun.syncthing.net" + # 游戏平台 + - "*.srv.nintendo.net" + - "xbox.*.microsoft.com" + - "xbox.*.*.microsoft.com" + - "*.xboxlive.com" + # Steam + - "*.cm.steampowered.com" + - "*.steamcontent.com" + # Battle.net / Blizzard + - "*.battlenet.com.cn" + - "*.battlenet.com" + - "*.blzstatic.cn" + - "*.battle.net" + # 网络连通性探测(多平台) + - "*.msftncsi.com" + - "*.msftconnecttest.com" + - "connectivitycheck.gstatic.com" + - "connectivitycheck.android.com" + - "connectivitycheck.platform.hicloud.com" + - "connect.rom.miui.com" + - "captive.apple.com" + - "network-test.debian.org" + - "detectportal.firefox.com" + - "lens.l.google.com" + # 推送通知 + - "+.push.apple.com" + - "+.market.xiaomi.com" + # VPN 覆盖网络 + - "*.tailscale.com" + - "*.zerotier.com" + # 媒体服务 + - "*.spotify.com" + - "+.music.126.net" + - "*.mcdn.bilivideo.cn" + # 其他需要真实 IP 解析的域名 + - "localhost.*.qq.com" + + # 主 DNS:经代理查询干净结果,防止境外域名请求泄露至国内 DNS 服务商 + nameserver: #!replace + - "https://1.1.1.1/dns-query" + + # 分域名 DNS 策略:按声明顺序匹配,先窄后宽 + nameserver-policy: + # 内网/本地域名交给系统解析器(路由器/DHCP 下发的 DNS),可解析局域网设备名 + "geosite:private": + - system + # NTP 校时域名用裸 IP UDP 解析:时间偏差会让 DoH 证书验证失败,校时链路须独立于任何 TLS/代理 + "time.*.com": + - 223.5.5.5 + - 119.29.29.29 + "time.*.gov": + - 223.5.5.5 + - 119.29.29.29 + "time.*.apple.com": + - 223.5.5.5 + - 119.29.29.29 + "ntp.*.com": + - 223.5.5.5 + - 119.29.29.29 + "+.pool.ntp.org": + - 223.5.5.5 + - 119.29.29.29 + "*.ntp.org.cn": + - 223.5.5.5 + - 119.29.29.29 + # 国内域名直接用国内 DoH 就近解析,不绕代理(geosite 数据规则段已在用,零额外开销) + "geosite:cn": + - https://doh.pub/dns-query + - https://dns.alidns.com/dns-query + + # 代理节点域名解析:国内 DoH,节点 IP 不会被 CN DNS 污染,速度更快 + proxy-server-nameserver: + - https://doh.pub/dns-query + - https://dns.alidns.com/dns-query + + +# 服务器订阅配置 +proxy-providers: + Server: + path: ./Provider/Proxy/Server.yaml + url: https://sub.hotkids.me + interval: 3600 + proxy: DIRECT + headers: + User-Agent: + - "Clash/v1.18.0" + - "mihomo/1.18.3" + health-check: + enable: true + url: 'http://cp.cloudflare.com/generate_204' + interval: 600 + timeout: 5000 + expected-status: 204 + +proxy-groups: + # Global + # name: "GLOBAL" + # type: select + # icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Inbound.png + # include-all: true + + # Proxy + - name: "🔰 Proxy" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Outbound.png + proxies: + - 🇭🇰 Hong Kong + - 🇨🇳 Taiwan + - 🇸🇬 Singapore + - 🇯🇵 Japan + - 🇺🇸 America + - '🇬🇧 England' + - '🇩🇪 Germany' + - 🇺🇳 Server + - 🔘 DIRECT + + # Streaming Global + - name: "🎬 Streaming" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Streaming.png + proxies: + - 🔰 Proxy + - 🇭🇰 Hong Kong + - 🇨🇳 Taiwan + - 🇸🇬 Singapore + - 🇯🇵 Japan + - 🇺🇸 America + - '🇬🇧 England' + - '🇩🇪 Germany' + - 🇺🇳 Server + + # CNTV APAC + - name: "📺 CNTV" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/StreamingCN.png + proxies: + - 🔘 DIRECT + - 🇨🇳 Taiwan + - 🇭🇰 Hong Kong + + # Apple + # > Apple Services + - name: "🍎 Apple" + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Apple.png + proxies: + - 🔘 DIRECT + - 🔰 Proxy + - 🇺🇸 America + - 🇯🇵 Japan + + # Google + - name: "🔍 Google" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Google.png + proxies: + - 🇺🇸 America + - 🔰 Proxy + + # Microsoft + # > OneDrive + - name: "☁️ OneDrive" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/OneDrive.png + proxies: + - 🔘 DIRECT + - 🔰 Proxy + + # > Microsoft Services + - name: "Ⓜ️ Microsoft" + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Microsoft.png + proxies: + - 🔘 DIRECT + - 🔰 Proxy + + - name: "🤖 AIGC" + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/ChatGPT.png + proxies: + - 🇺🇸 America + - 🇸🇬 Singapore + - 🔰 Proxy + + # Crypto + - name: "🪙 Crypto" + type: select + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Cryptocurrency_3.png + proxies: + - '🇩🇪 Germany' + - 🇺🇸 America + - 🔰 Proxy + - 🔘 DIRECT + + # Finance + - name: "💳 Finance" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Finance.png + proxies: + - 🇺🇸 America + - '🇩🇪 Germany' + - 🔰 Proxy + - 🔘 DIRECT + + # Mail + - name: "📧 Mail" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Email.png + proxies: + - 🔰 Proxy + - 🔘 DIRECT + + # Speedtest + - name: "⏱️ Speedtest" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Speed.png + use: + - Server + + # Adblock + - name: "🚧 AdGuard" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Block.png + proxies: + - 🔘 DIRECT + - ⛔️ REJECT + - 📛 REJECT-DROP + + # DIRECT + - name: "🔘 DIRECT" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Direct.png + hidden: true + proxies: + - DIRECT + + # REJECT + - name: "⛔️ REJECT" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Reject.png + hidden: true + proxies: + - REJECT + + # REJECT-DROP + - name: "📛 REJECT-DROP" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Reject.png + hidden: true + proxies: + - REJECT-DROP + + # Nodes + - name: '🇸🇱 Relay' + type: url-test + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + use: + - Server + tolerance: 50 + hidden: true + filter: '(?i)^(?=.*(?:GoMaMi|Neburst|Pro))' + - name: '🇭🇰 HK Relay' + type: fallback + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + use: + - Server + hidden: true + filter: '(?i)^(?=.*\b(?:HK|HKG)\d*\b)(?=.*(?:GoMaMi|Pro))' + - name: '🇨🇳 TW Relay' + type: fallback + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + use: + - Server + hidden: true + filter: '(?i)^(?=.*\b(?:TW|TWN)\d*\b)(?=.*Neburst)' + - name: '🇯🇵 JP Relay' + type: fallback + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + use: + - Server + hidden: true + filter: '(?i)^(?=.*\b(?:JP|JPN)\d*\b)(?=.*Pro)' + - name: '🇺🇸 US Relay' + type: fallback + icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + use: + - Server + hidden: true + filter: '(?i)^(?=.*\b(?:US|USA)\d*\b)(?=.*(?:GoMaMi|Pro))' + - name: "🇺🇳 Server" + type: select + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Club.png + use: + - Server + + # Area + - name: "🇭🇰 Hong Kong" + type: fallback + hidden: true + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/HK.png + use: + - Server + filter: '(?i)^(?=.*\b(?:HK|HKG)\d*\b)(?!.*GoMaMi)(?!.*Pro)' + + - name: "🇨🇳 Taiwan" + type: fallback + hidden: true + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/TW.png + use: + - Server + filter: '(?i)^(?=.*\b(?:TW|TWN)\d*\b)(?!.*Neburst)(?!.*Pro)' + + - name: "🇸🇬 Singapore" + type: fallback + hidden: true + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/SG.png + use: + - Server + filter: '(?i)^(?=.*\b(?:SG|SGP)\d*\b)(?!.*Neburst)(?!.*Pro)' + + - name: "🇯🇵 Japan" + type: fallback + hidden: true + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/JP.png + use: + - Server + filter: '(?i)^(?=.*\b(?:JP|JPN)\d*\b)(?!.*Pro)' + + - name: "🇺🇸 America" + type: fallback + hidden: true + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/US.png + use: + - Server + filter: '(?i)^(?=.*\b(?:US|USA)\d*\b)(?!.*GoMaMi)(?!.*Pro)' + + +# 关于 Rule Provider 请查阅:https://wiki.metacubex.one/en/config/rule-providers/ + + - name: '🇬🇧 England' + type: fallback + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/UK.png + use: + - Server + hidden: true + filter: '(?i)^(?=.*\b(?:UK|GBR)\d*\b)' + - name: '🇩🇪 Germany' + type: fallback + icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/DE.png + use: + - Server + hidden: true + filter: '(?i)^(?=.*\b(?:DE|DEU)\d*\b)' +rule-providers: +# name: # Provider 名称 +# type: http # http 或 file +# behavior: classical # 或 ipcidr、domain +# path: # 文件路径 +# url: # 只有当类型为 HTTP 时才可用,您不需要在本地空间中创建新文件。 +# interval: # 自动更新间隔,仅在类型为 HTTP 时可用 + Bypass: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Bypass.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Bypass.yaml + interval: 86400 + + Reroute: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Reroute.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Reroute.yaml + interval: 86400 + + Private: + behavior: domain + format: mrs + path: ./Provider/RuleSet/Private.mrs + url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite/private.mrs + interval: 86400 + + HTTPDNS: + behavior: classical + format: yaml + path: ./Provider/RuleSet/HTTPDNS.yaml + url: https://fastly.jsdelivr.net/gh/VirgilClyne/GetSomeFries@main/ruleset/HTTPDNS.Block.yaml + interval: 86400 + + Reject: + behavior: domain + format: mrs + path: ./Provider/RuleSet/Reject.mrs + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Reject.mrs + interval: 86400 + + AdBlock: + behavior: classical + format: yaml + path: ./Provider/RuleSet/AdBlock.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Block.yaml + interval: 86400 + + Phishing: + behavior: domain + format: mrs + path: ./Provider/RuleSet/Phishing.mrs + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Phishing.mrs + interval: 86400 + + Bogus: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Bogus.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Bogus.yaml + interval: 86400 + + Streaming_TW: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Streaming_TW.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming_TW.yaml + interval: 86400 + + Streaming_JP: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Streaming_JP.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming_JP.yaml + interval: 86400 + + Streaming_US: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Streaming_US.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming_US.yaml + interval: 86400 + + Streaming: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Streaming.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Streaming.yaml + interval: 86400 + + CNTV: + behavior: classical + format: yaml + path: ./Provider/RuleSet/CNTV.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/CNTV.yaml + interval: 86400 + + Google AI Studio: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Google_AI_Studio.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Gemini.yaml + interval: 86400 + + AIGC: + behavior: classical + format: yaml + path: ./Provider/RuleSet/AIGC.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/GenAI.yaml + interval: 86400 + + Apple CN: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Apple_CN.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Apple%20CN.yaml + interval: 86400 + + Apple: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Apple.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Apple.yaml + interval: 86400 + + Google: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Google.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Google.yaml + interval: 86400 + + OneDrive: + behavior: classical + format: yaml + path: ./Provider/RuleSet/OneDrive.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/OneDrive.yaml + interval: 86400 + + Microsoft: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Microsoft.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Microsoft.yaml + interval: 86400 + + Crypto: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Crypto.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Crypto.yaml + interval: 86400 + + Finance: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Finance.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Finance.yaml + interval: 86400 + + Spark: + behavior: classical + format: yaml + path: ./Provider/RuleSet/Spark.yaml + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Spark.yaml + interval: 86400 + + Speedtest: + behavior: domain + format: mrs + path: ./Provider/RuleSet/Speedtest.mrs + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/Speedtest.mrs + interval: 86400 + + Global: + behavior: domain + format: mrs + path: ./Provider/RuleSet/Global.mrs + url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite/geolocation-!cn.mrs + interval: 86400 + + China: + behavior: domain + format: mrs + path: ./Provider/RuleSet/China.mrs + url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite/cn.mrs + interval: 86400 + + China ASN: + behavior: classical + format: yaml + path: ./Provider/RuleSet/China_ASN.yaml + url: https://fastly.jsdelivr.net/gh/VirgilClyne/GetSomeFries@main/ruleset/ASN.China.yaml + interval: 86400 + + China IP: + behavior: ipcidr + format: mrs + path: ./Provider/RuleSet/China_IP.mrs + url: https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geoip/cn.mrs + interval: 86400 + + LAN: + behavior: ipcidr + format: mrs + path: ./Provider/RuleSet/LAN.mrs + url: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Clash/RuleSet/lancidr.mrs + interval: 86400 + +# 规则 +rules: + # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) + - AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT + + # 标准 SSH 端口 + - AND,((DST-PORT,22),(NETWORK,TCP)),🔘 DIRECT + + # Unbreak 后续规则修正,修复因规则拦截或分流不当导致的功能异常 + - RULE-SET,Bypass,🔘 DIRECT + - RULE-SET,Reroute,🔰 Proxy + + # Private 私有网络 + - RULE-SET,Private,🔘 DIRECT + + # HTTPDNS 请求/流量阻止 + - RULE-SET,HTTPDNS,🚧 AdGuard + + # Advertising 广告 + - RULE-SET,Reject,🚧 AdGuard + - RULE-SET,AdBlock,🚧 AdGuard + + # Phishing 钓鱼网站 + - RULE-SET,Phishing,🚧 AdGuard + + # Bogus IP NXDOMAIN 劫持/僵尸网络 C2 + - RULE-SET,Bogus,📛 REJECT-DROP,no-resolve + + # Global Area Network + # > Streaming by Region + # >> Streaming TW + - RULE-SET,Streaming_TW,🇨🇳 Taiwan + # >> Streaming JP + - RULE-SET,Streaming_JP,🇯🇵 Japan + # >> Streaming US + - RULE-SET,Streaming_US,🇺🇸 America + + # > Streaming + - RULE-SET,Streaming,🎬 Streaming + + # > CNTV(适用于 iQIYI Intl,WeTV,Bilibili 等大陆在港台东南亚提供服务的流媒体服务) + - RULE-SET,CNTV,📺 CNTV + + # Global 全球代理规则 + # > AIGC + - RULE-SET,Google AI Studio,🔍 Google + - RULE-SET,AIGC,🤖 AIGC + + # > Apple + # >> Apple Services + - RULE-SET,Apple CN,🔘 DIRECT + - RULE-SET,Apple,🍎 Apple + + # > Google + - RULE-SET,Google,🔍 Google + + # > Microsoft + - RULE-SET,OneDrive,☁️ OneDrive + - RULE-SET,Microsoft,Ⓜ️ Microsoft + + # > Telegram + - IP-ASN,44907,🇸🇬 Singapore,no-resolve + - IP-ASN,62014,🇸🇬 Singapore,no-resolve + - IP-ASN,59930,🇺🇸 America,no-resolve + - IP-ASN,62041,🇩🇪 Germany,no-resolve + - IP-ASN,211157,🇩🇪 Germany,no-resolve + - IP-CIDR,5.28.192.0/18,🇩🇪 Germany,no-resolve + - IP-CIDR,109.239.140.0/24,🇩🇪 Germany,no-resolve + + # > Crypto + - RULE-SET,Crypto,🪙 Crypto + + # > Finance + - RULE-SET,Finance,💳 Finance + + # > Mail + - RULE-SET,Spark,📧 Mail + + # > Speedtest + - RULE-SET,Speedtest,⏱️ Speedtest + + # Global (DNS Cache Pollution) / (IP Blackhole) / (Region-Restricted Access Denied) / (Network Jitter) + - RULE-SET,Global,🔰 Proxy + + # China Area Network + - RULE-SET,China,🔘 DIRECT + - RULE-SET,China ASN,🔘 DIRECT,no-resolve + - RULE-SET,China IP,🔘 DIRECT,no-resolve + + # Local Area Network + - RULE-SET,LAN,🔘 DIRECT,no-resolve + + # GeoIP + - GEOSITE,cn,🔘 DIRECT + - GEOIP,CN,🔘 DIRECT,no-resolve + - GEOSITE,geolocation-!cn,🔰 Proxy + + # Final + - MATCH,🔰 Proxy From 9425b7249915fd557924560a20fc3bd609646551 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 12:08:14 +0000 Subject: [PATCH 11/28] style(sync): match Sample.yaml formatting in overlay-added lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overlay pass wrote its own YAML style: single-quoted group names and candidate entries, and its own field order, so inserted lines did not match the transcribed ones around them — visible as quoted '🇬🇧 England' sitting between unquoted neighbours in a proxies list. Now follows the source exactly: names double-quoted, candidate entries unquoted, filters single-quoted, fields ordered name → type → icon → hidden → use/proxies → benchmark params → filter (group_overrides inserts a new field at its correct slot rather than after type), and a blank line between groups. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 35 ++++++++++++++----- Clash/Script/MyStash.stoverride | 59 ++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 38bcfe771..ca948a278 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4351,23 +4351,32 @@ def _stash_group_spans(lines: list[str]) -> dict[str, tuple[int, int]]: def _stash_render_group(g: dict) -> list[str]: - """按 Sample.yaml 的字段顺序渲染一个新增策略组。""" - out = [f" - name: {_yq(g['name'])}", f" type: {g.get('type', 'select')}"] + """按 Sample.yaml 的引号风格与字段顺序渲染一个新增策略组。 + + 源风格:name 用双引号、proxies 条目不加引号、filter 用单引号; + 字段顺序 name → type → icon → hidden → use/proxies → 测速参数 → filter。 + """ + out = [f' - name: "{g["name"]}"', f" type: {g.get('type', 'select')}"] if g.get("icon"): out.append(f" icon: {g['icon']}") + if g.get("hidden"): + out.append(" hidden: true") # 池组的节点来源:与基座地区组写法一致,从本仓库 provider 里按 filter 筛 - out.append(" use:") - out.append(" - Server") + out += [" use:", " - Server"] for key in ("interval", "tolerance", "lazy"): if key in g: out.append(f" {key}: {g[key]}") - if g.get("hidden"): - out.append(" hidden: true") if g.get("filter"): out.append(f" filter: {_yq(g['filter'])}") + out.append("") # 组间空行,与 Sample.yaml 一致 return out +# 组内字段的规范顺序(与 Sample.yaml 一致),group_overrides 新增字段时按此定位 +_STASH_FIELD_ORDER = ["name", "type", "icon", "hidden", "use", "proxies", + "interval", "tolerance", "lazy", "filter"] + + def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[str]: """把一份 overlay 的差异叠加到已转译好的 Stash 文本上(就地返回新列表)。""" unknown = set(overlay) - _STASH_OVERLAY_OK @@ -4397,8 +4406,16 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st if idx is not None: lines[idx] = rendered else: - # 插到 name/type 之后,保持字段顺序稳定 - lines.insert(s + 2, rendered) + # 按 Sample.yaml 的字段顺序插到第一个「应排在它之后」的字段前 + rank = _STASH_FIELD_ORDER.index(key) if key in _STASH_FIELD_ORDER else len(_STASH_FIELD_ORDER) + at = e + for i in range(s, e): + m2 = re.match(r"^ ([\w-]+):", lines[i]) + if m2 and m2.group(1) in _STASH_FIELD_ORDER \ + and _STASH_FIELD_ORDER.index(m2.group(1)) > rank: + at = i + break + lines.insert(at, rendered) notes.append(f"{name}: 覆盖 {'/'.join(patch)}") # 2) group_proxies_insert:在候选列表里紧邻锚点插入 @@ -4413,7 +4430,7 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st if idx is None: raise ValueError(f"{label}: {name} 的候选里找不到锚点 {anchor!r}") at = idx + 1 if spec.get("after") else idx - lines[at:at] = [f" - {_yq(p)}" for p in spec["insert"]] + lines[at:at] = [f" - {p}" for p in spec["insert"]] notes.append(f"{name}: 候选插入 {len(spec['insert'])} 项") # 3) extra_pool_groups:整组新增,插到锚点组之后 diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index a2a5c2311..f5d76c265 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,5 +1,5 @@ # Stash for Android -# Date: 2026-08-28 19:47:10 +# Date: 2026-08-28 20:07:59 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 @@ -165,8 +165,8 @@ proxy-groups: - 🇸🇬 Singapore - 🇯🇵 Japan - 🇺🇸 America - - '🇬🇧 England' - - '🇩🇪 Germany' + - 🇬🇧 England + - 🇩🇪 Germany - 🇺🇳 Server - 🔘 DIRECT @@ -181,8 +181,8 @@ proxy-groups: - 🇸🇬 Singapore - 🇯🇵 Japan - 🇺🇸 America - - '🇬🇧 England' - - '🇩🇪 Germany' + - 🇬🇧 England + - 🇩🇪 Germany - 🇺🇳 Server # CNTV APAC @@ -243,7 +243,7 @@ proxy-groups: type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Cryptocurrency_3.png proxies: - - '🇩🇪 Germany' + - 🇩🇪 Germany - 🇺🇸 America - 🔰 Proxy - 🔘 DIRECT @@ -254,7 +254,7 @@ proxy-groups: icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Finance.png proxies: - 🇺🇸 America - - '🇩🇪 Germany' + - 🇩🇪 Germany - 🔰 Proxy - 🔘 DIRECT @@ -307,42 +307,47 @@ proxy-groups: - REJECT-DROP # Nodes - - name: '🇸🇱 Relay' + - name: "🇸🇱 Relay" type: url-test icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + hidden: true use: - Server tolerance: 50 - hidden: true filter: '(?i)^(?=.*(?:GoMaMi|Neburst|Pro))' - - name: '🇭🇰 HK Relay' + + - name: "🇭🇰 HK Relay" type: fallback icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + hidden: true use: - Server - hidden: true filter: '(?i)^(?=.*\b(?:HK|HKG)\d*\b)(?=.*(?:GoMaMi|Pro))' - - name: '🇨🇳 TW Relay' + + - name: "🇨🇳 TW Relay" type: fallback icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + hidden: true use: - Server - hidden: true filter: '(?i)^(?=.*\b(?:TW|TWN)\d*\b)(?=.*Neburst)' - - name: '🇯🇵 JP Relay' + + - name: "🇯🇵 JP Relay" type: fallback icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + hidden: true use: - Server - hidden: true filter: '(?i)^(?=.*\b(?:JP|JPN)\d*\b)(?=.*Pro)' - - name: '🇺🇸 US Relay' + + - name: "🇺🇸 US Relay" type: fallback icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png + hidden: true use: - Server - hidden: true filter: '(?i)^(?=.*\b(?:US|USA)\d*\b)(?=.*(?:GoMaMi|Pro))' + - name: "🇺🇳 Server" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Club.png @@ -352,40 +357,40 @@ proxy-groups: # Area - name: "🇭🇰 Hong Kong" type: fallback - hidden: true icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/HK.png + hidden: true use: - Server filter: '(?i)^(?=.*\b(?:HK|HKG)\d*\b)(?!.*GoMaMi)(?!.*Pro)' - name: "🇨🇳 Taiwan" type: fallback - hidden: true icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/TW.png + hidden: true use: - Server filter: '(?i)^(?=.*\b(?:TW|TWN)\d*\b)(?!.*Neburst)(?!.*Pro)' - name: "🇸🇬 Singapore" type: fallback - hidden: true icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/SG.png + hidden: true use: - Server filter: '(?i)^(?=.*\b(?:SG|SGP)\d*\b)(?!.*Neburst)(?!.*Pro)' - name: "🇯🇵 Japan" type: fallback - hidden: true icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/JP.png + hidden: true use: - Server filter: '(?i)^(?=.*\b(?:JP|JPN)\d*\b)(?!.*Pro)' - name: "🇺🇸 America" type: fallback - hidden: true icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/US.png + hidden: true use: - Server filter: '(?i)^(?=.*\b(?:US|USA)\d*\b)(?!.*GoMaMi)(?!.*Pro)' @@ -393,20 +398,22 @@ proxy-groups: # 关于 Rule Provider 请查阅:https://wiki.metacubex.one/en/config/rule-providers/ - - name: '🇬🇧 England' + - name: "🇬🇧 England" type: fallback icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/UK.png + hidden: true use: - Server - hidden: true filter: '(?i)^(?=.*\b(?:UK|GBR)\d*\b)' - - name: '🇩🇪 Germany' + + - name: "🇩🇪 Germany" type: fallback icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/DE.png + hidden: true use: - Server - hidden: true filter: '(?i)^(?=.*\b(?:DE|DEU)\d*\b)' + rule-providers: # name: # Provider 名称 # type: http # http 或 file From 5aa89c051ef02266ec77d11fd081918bd9f07792 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 12:39:56 +0000 Subject: [PATCH 12/28] fix(sync): mark proxy-groups and rules #!replace in the Stash overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the override docs, same-named arrays are inserted BEFORE the base config's array rather than replacing it. Without a marker this override produced "our config plus the base config": our 36 rules ran first and left the base's rules unreachable behind our MATCH, and all of the base's own proxy-groups stayed in the list alongside ours. The two arrays that decide routing and policy now carry #!replace so they are authoritative. The dict-typed keys keep the documented recursive merge, which is what we want: rule-providers, proxy-providers, dns and hosts merge with the base, so an airport's own private DNS and rule-sets survive. Also fixes the span lookups the marker broke — proxy-groups:/rule-providers: are now matched by prefix instead of exact line, which the overlay pass needs to locate group and provider blocks. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 21 +++++++++++---------- Clash/Script/MyStash.stoverride | 6 +++--- Clash/Script/Stash.stoverride | 6 +++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index ca948a278..2e88832fc 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4129,6 +4129,8 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - } # 2) dns 块内 Stash 支持的子键(其余为 mihomo 专属,略去) +_STASH_REPLACE_TOP = {"proxy-groups", "rules"} + _STASH_DNS_KEEP = { "default-nameserver", "nameserver", "nameserver-policy", "proxy-server-nameserver", "fake-ip-filter", @@ -4218,7 +4220,9 @@ def flush() -> None: changes.append(f"略去 {top}") continue flush() - out.append(line) + out.append(f"{line} #!replace" if top in _STASH_REPLACE_TOP else line) + if top in _STASH_REPLACE_TOP: + changes.append(f"{top}: 加 #!replace(数组默认前置插入,会与基础配置混合)") if top == "dns": # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」, # Stash 的等价物是全局开关 follow-rule。 @@ -4331,9 +4335,9 @@ def flush() -> None: def _stash_group_spans(lines: list[str]) -> dict[str, tuple[int, int]]: """定位 proxy-groups 块内每个组的行区间 {组名: (起, 止)}(止为开区间)。""" - try: - start = lines.index("proxy-groups:") - except ValueError: + # 键行可能带 #!replace 行内标记,按前缀匹配而不是全等 + start = next((i for i, l in enumerate(lines) if re.match(r"^proxy-groups:", l)), -1) + if start < 0: return {} end = next((i for i in range(start + 1, len(lines)) if lines[i] and not lines[i].startswith((" ", "#"))), len(lines)) @@ -4473,12 +4477,9 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st # 清理不再被引用的规则集 used = {m.group(1) for l in lines if (m := re.match(r"^ - RULE-SET,([^,]+),", l))} - try: - rp = lines.index("rule-providers:") - rp_end = next((i for i in range(rp + 1, len(lines)) - if lines[i] and not lines[i].startswith((" ", "#"))), len(lines)) - except ValueError: - rp, rp_end = -1, -1 + rp = next((i for i, l in enumerate(lines) if re.match(r"^rule-providers:", l)), -1) + rp_end = next((i for i in range(rp + 1, len(lines)) + if lines[i] and not lines[i].startswith((" ", "#"))), len(lines)) if rp >= 0 else -1 if rp >= 0: kept, i, dropped = [], rp + 1, [] while i < rp_end: diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index f5d76c265..aaa2e51e3 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,5 +1,5 @@ # Stash for Android -# Date: 2026-08-28 20:07:59 +# Date: 2026-08-28 20:39:31 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 @@ -148,7 +148,7 @@ proxy-providers: timeout: 5000 expected-status: 204 -proxy-groups: +proxy-groups: #!replace # Global # name: "GLOBAL" # type: select @@ -625,7 +625,7 @@ rule-providers: interval: 86400 # 规则 -rules: +rules: #!replace # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) - AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 6cf7ab910..fdedca72d 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,5 +1,5 @@ # Stash for Android -# Date: 2026-08-28 19:40:13 +# Date: 2026-08-28 20:39:05 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 @@ -148,7 +148,7 @@ proxy-providers: timeout: 5000 expected-status: 204 -proxy-groups: +proxy-groups: #!replace # Global # name: "GLOBAL" # type: select @@ -574,7 +574,7 @@ rule-providers: interval: 86400 # 规则 -rules: +rules: #!replace # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) - AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT From 9fe9431f36b2bb2bb6730d6d6ad6749709dce978 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 12:45:37 +0000 Subject: [PATCH 13/28] fix(sync): drop all #!replace markers from the Stash overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base config these overrides are applied to contains only proxies — no dns, proxy-groups or rules to merge with — so every marker was a no-op. Removing them also restores the transcription rule: apart from the Stash-vs-mihomo difference points, the output stays line-for-line identical to Sample.yaml, and #!replace was neither a difference point nor something the base config needed. The #RULES suffix is still stripped from the nameserver entries; that happens during generation, so the suffix never reaches the file and needed no marker to begin with. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 13 ++----------- Clash/Script/MyStash.stoverride | 8 ++++---- Clash/Script/Stash.stoverride | 8 ++++---- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 2e88832fc..6180f1bcd 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4129,8 +4129,6 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - } # 2) dns 块内 Stash 支持的子键(其余为 mihomo 专属,略去) -_STASH_REPLACE_TOP = {"proxy-groups", "rules"} - _STASH_DNS_KEEP = { "default-nameserver", "nameserver", "nameserver-policy", "proxy-server-nameserver", "fake-ip-filter", @@ -4220,9 +4218,7 @@ def flush() -> None: changes.append(f"略去 {top}") continue flush() - out.append(f"{line} #!replace" if top in _STASH_REPLACE_TOP else line) - if top in _STASH_REPLACE_TOP: - changes.append(f"{top}: 加 #!replace(数组默认前置插入,会与基础配置混合)") + out.append(line) if top == "dns": # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」, # Stash 的等价物是全局开关 follow-rule。 @@ -4256,12 +4252,7 @@ def flush() -> None: buf.clear() continue flush() - if key == "nameserver": - # 整体替换:原数组含 Stash 无法识别的 #RULES 后缀, - # 覆写默认的「前置插入」会把它保留下来。 - out.append(" nameserver: #!replace") - else: - out.append(line) + out.append(line) continue if top == "dns" and indent > 2 and not dns_keep: buf.clear() diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index aaa2e51e3..4e05676d0 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,5 +1,5 @@ # Stash for Android -# Date: 2026-08-28 20:39:31 +# Date: 2026-08-28 20:45:24 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 @@ -92,7 +92,7 @@ dns: - "localhost.*.qq.com" # 主 DNS:经代理查询干净结果,防止境外域名请求泄露至国内 DNS 服务商 - nameserver: #!replace + nameserver: - "https://1.1.1.1/dns-query" # 分域名 DNS 策略:按声明顺序匹配,先窄后宽 @@ -148,7 +148,7 @@ proxy-providers: timeout: 5000 expected-status: 204 -proxy-groups: #!replace +proxy-groups: # Global # name: "GLOBAL" # type: select @@ -625,7 +625,7 @@ rule-providers: interval: 86400 # 规则 -rules: #!replace +rules: # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) - AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index fdedca72d..0b1218827 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,5 +1,5 @@ # Stash for Android -# Date: 2026-08-28 20:39:05 +# Date: 2026-08-28 20:45:24 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 @@ -92,7 +92,7 @@ dns: - "localhost.*.qq.com" # 主 DNS:经代理查询干净结果,防止境外域名请求泄露至国内 DNS 服务商 - nameserver: #!replace + nameserver: - "https://1.1.1.1/dns-query" # 分域名 DNS 策略:按声明顺序匹配,先窄后宽 @@ -148,7 +148,7 @@ proxy-providers: timeout: 5000 expected-status: 204 -proxy-groups: #!replace +proxy-groups: # Global # name: "GLOBAL" # type: select @@ -574,7 +574,7 @@ rule-providers: interval: 86400 # 规则 -rules: #!replace +rules: # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) - AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT From 308eb5b9c4227be4763362c9e3369806cda2a75e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 12:49:58 +0000 Subject: [PATCH 14/28] feat(sync): add the override name/desc display fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The override docs name these as the conventional fields for a .stoverride's title and description, both display-only. Each file gets its own so the two do not collide in Stash's override list: name comes from the output filename (Stash / MyStash) and desc says where the content comes from, with the overlay variant naming the overlay it layers on. Kept to those two fields on one line each — the earlier attempt also emitted an author key that duplicated the "# Author: @HotKids" header comment. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 11 +++++++++++ Clash/Script/MyStash.stoverride | 5 ++++- Clash/Script/Stash.stoverride | 5 ++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 6180f1bcd..0c3ac20bb 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4299,6 +4299,10 @@ def flush() -> None: insert_at = next((i for i, l in enumerate(out) if l.startswith("# Author:")), 0) + 1 out[insert_at:insert_at] = [ "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", + "", + # name / desc 是覆写文件的惯例字段,仅用于在 Stash 覆写列表中展示 + f"name: {Path(out_path).stem}", + "desc: HotKids 规则配置 · 由 Surge/Profile.conf 转译", ] body = "\n".join(out).rstrip() + "\n" @@ -4383,6 +4387,13 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st lines = list(lines) notes: list[str] = [] + # name / desc 仅用于展示;改成本份定制版自己的,避免与基座同名 + for i, l in enumerate(lines): + if l.startswith("name: "): + lines[i] = f"name: {Path(overlay['stash_output']).stem}" + elif l.startswith("desc: "): + lines[i] = f"desc: HotKids 规则配置 · 叠加 {label} 的私人差异" + # 1) group_overrides:改写既有组的字段(filter 为 null 表示删掉该行) for name, patch in (overlay.get("group_overrides") or {}).items(): span = _stash_group_spans(lines).get(name) diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index 4e05676d0..c1f525298 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,8 +1,11 @@ # Stash for Android -# Date: 2026-08-28 20:45:24 +# Date: 2026-08-28 20:49:44 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 +name: MyStash +desc: HotKids 规则配置 · 叠加 myscript.overlay.json 的私人差异 + # 通用设置 # 代理模式:rule(规则)/ global(全局)/ direct(直连) diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 0b1218827..de22032d6 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,8 +1,11 @@ # Stash for Android -# Date: 2026-08-28 20:45:24 +# Date: 2026-08-28 20:49:44 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 +name: Stash +desc: HotKids 规则配置 · 由 Surge/Profile.conf 转译 + # 通用设置 # 代理模式:rule(规则)/ global(全局)/ direct(直连) From d5cb649af9097c69cce15d9c3c85693b3864a95d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 12:52:24 +0000 Subject: [PATCH 15/28] style(sync): give the Stash overrides a proper name/desc/author block Follows the shape of a published .stoverride: an emoji-prefixed name, a multi-line desc that says what the rule sources and the routing strategy are rather than describing the generator, and an author line. The overlay variant gets its own name and calls out the overlay it layers on plus the extra fallback/relay groups, so the two are distinguishable in Stash's override list. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 27 ++++++++++++++++++--------- Clash/Script/MyStash.stoverride | 10 +++++++--- Clash/Script/Stash.stoverride | 10 +++++++--- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 0c3ac20bb..23dd64db7 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4300,9 +4300,13 @@ def flush() -> None: out[insert_at:insert_at] = [ "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", "", - # name / desc 是覆写文件的惯例字段,仅用于在 Stash 覆写列表中展示 - f"name: {Path(out_path).stem}", - "desc: HotKids 规则配置 · 由 Surge/Profile.conf 转译", + # name / desc / author 仅用于在 Stash 覆写列表中展示 + "name: 🔰 HotKids Rules", + "desc: |-", + " HotKids 规则配置 · Stash 版(由 Surge/Profile.conf 转译)", + " 规则集: 本仓库 RULE-SET(含 .mrs 二进制)· 每 24h 自动更新", + " 策略: 广告拦截 + 国内直连,境外按服务与地区分流", + "author: '@HotKids'", ] body = "\n".join(out).rstrip() + "\n" @@ -4387,12 +4391,17 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st lines = list(lines) notes: list[str] = [] - # name / desc 仅用于展示;改成本份定制版自己的,避免与基座同名 - for i, l in enumerate(lines): - if l.startswith("name: "): - lines[i] = f"name: {Path(overlay['stash_output']).stem}" - elif l.startswith("desc: "): - lines[i] = f"desc: HotKids 规则配置 · 叠加 {label} 的私人差异" + # 展示字段改成本份定制版自己的,避免与基座在覆写列表里同名 + name_i = next((i for i, l in enumerate(lines) if l.startswith("name: ")), None) + if name_i is not None: + lines[name_i] = f"name: 🔰 HotKids Rules · {Path(overlay['stash_output']).stem}" + end = next(i for i in range(name_i + 1, len(lines)) if lines[i].startswith("author: ")) + lines[name_i + 1:end] = [ + "desc: |-", + f" HotKids 规则配置 · Stash 版(由 Surge/Profile.conf 转译,叠加 {label})", + " 规则集: 本仓库 RULE-SET(含 .mrs 二进制)· 每 24h 自动更新", + " 策略: 广告拦截 + 国内直连,境外按服务与地区分流;地区组 fallback + 中转链", + ] # 1) group_overrides:改写既有组的字段(filter 为 null 表示删掉该行) for name, patch in (overlay.get("group_overrides") or {}).items(): diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index c1f525298..f1a9567e3 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,10 +1,14 @@ # Stash for Android -# Date: 2026-08-28 20:49:44 +# Date: 2026-08-28 20:52:11 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 -name: MyStash -desc: HotKids 规则配置 · 叠加 myscript.overlay.json 的私人差异 +name: 🔰 HotKids Rules · MyStash +desc: |- + HotKids 规则配置 · Stash 版(由 Surge/Profile.conf 转译,叠加 myscript.overlay.json) + 规则集: 本仓库 RULE-SET(含 .mrs 二进制)· 每 24h 自动更新 + 策略: 广告拦截 + 国内直连,境外按服务与地区分流;地区组 fallback + 中转链 +author: '@HotKids' # 通用设置 diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index de22032d6..67c79639b 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,10 +1,14 @@ # Stash for Android -# Date: 2026-08-28 20:49:44 +# Date: 2026-08-28 20:52:11 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 -name: Stash -desc: HotKids 规则配置 · 由 Surge/Profile.conf 转译 +name: 🔰 HotKids Rules +desc: |- + HotKids 规则配置 · Stash 版(由 Surge/Profile.conf 转译) + 规则集: 本仓库 RULE-SET(含 .mrs 二进制)· 每 24h 自动更新 + 策略: 广告拦截 + 国内直连,境外按服务与地区分流 +author: '@HotKids' # 通用设置 From f7562517ad2ad6802c514ad4d232b3426b46edc3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 12:56:28 +0000 Subject: [PATCH 16/28] fix(sync): add the missing author field to the Stash overrides Restores the one-line name/desc and adds author, which was the field left out. The previous commit rewrote name/desc into a multi-line block modelled on another published override; that was not what was asked for. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 22 +++++++--------------- Clash/Script/MyStash.stoverride | 9 +++------ Clash/Script/Stash.stoverride | 9 +++------ 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 23dd64db7..e90b035fc 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4301,11 +4301,8 @@ def flush() -> None: "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", "", # name / desc / author 仅用于在 Stash 覆写列表中展示 - "name: 🔰 HotKids Rules", - "desc: |-", - " HotKids 规则配置 · Stash 版(由 Surge/Profile.conf 转译)", - " 规则集: 本仓库 RULE-SET(含 .mrs 二进制)· 每 24h 自动更新", - " 策略: 广告拦截 + 国内直连,境外按服务与地区分流", + f"name: {Path(out_path).stem}", + "desc: HotKids 规则配置 · 由 Surge/Profile.conf 转译", "author: '@HotKids'", ] @@ -4392,16 +4389,11 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st notes: list[str] = [] # 展示字段改成本份定制版自己的,避免与基座在覆写列表里同名 - name_i = next((i for i, l in enumerate(lines) if l.startswith("name: ")), None) - if name_i is not None: - lines[name_i] = f"name: 🔰 HotKids Rules · {Path(overlay['stash_output']).stem}" - end = next(i for i in range(name_i + 1, len(lines)) if lines[i].startswith("author: ")) - lines[name_i + 1:end] = [ - "desc: |-", - f" HotKids 规则配置 · Stash 版(由 Surge/Profile.conf 转译,叠加 {label})", - " 规则集: 本仓库 RULE-SET(含 .mrs 二进制)· 每 24h 自动更新", - " 策略: 广告拦截 + 国内直连,境外按服务与地区分流;地区组 fallback + 中转链", - ] + for i, l in enumerate(lines): + if l.startswith("name: "): + lines[i] = f"name: {Path(overlay['stash_output']).stem}" + elif l.startswith("desc: "): + lines[i] = f"desc: HotKids 规则配置 · 叠加 {label} 的私人差异" # 1) group_overrides:改写既有组的字段(filter 为 null 表示删掉该行) for name, patch in (overlay.get("group_overrides") or {}).items(): diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index f1a9567e3..cc0fd0169 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,13 +1,10 @@ # Stash for Android -# Date: 2026-08-28 20:52:11 +# Date: 2026-08-28 20:56:16 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 -name: 🔰 HotKids Rules · MyStash -desc: |- - HotKids 规则配置 · Stash 版(由 Surge/Profile.conf 转译,叠加 myscript.overlay.json) - 规则集: 本仓库 RULE-SET(含 .mrs 二进制)· 每 24h 自动更新 - 策略: 广告拦截 + 国内直连,境外按服务与地区分流;地区组 fallback + 中转链 +name: MyStash +desc: HotKids 规则配置 · 叠加 myscript.overlay.json 的私人差异 author: '@HotKids' # 通用设置 diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 67c79639b..422879c16 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,13 +1,10 @@ # Stash for Android -# Date: 2026-08-28 20:52:11 +# Date: 2026-08-28 20:56:16 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 -name: 🔰 HotKids Rules -desc: |- - HotKids 规则配置 · Stash 版(由 Surge/Profile.conf 转译) - 规则集: 本仓库 RULE-SET(含 .mrs 二进制)· 每 24h 自动更新 - 策略: 广告拦截 + 国内直连,境外按服务与地区分流 +name: Stash +desc: HotKids 规则配置 · 由 Surge/Profile.conf 转译 author: '@HotKids' # 通用设置 From 59739e6d9b310a66d04427f7f64944c56a858b4c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 12:59:24 +0000 Subject: [PATCH 17/28] =?UTF-8?q?chore(sync):=20word=20the=20Stash=20overr?= =?UTF-8?q?ide=20desc=20as=20"=E8=87=AA=E5=8A=A8=E8=BD=AC=E8=AF=91"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 2 +- Clash/Script/Stash.stoverride | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index e90b035fc..cbb5c489a 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4302,7 +4302,7 @@ def flush() -> None: "", # name / desc / author 仅用于在 Stash 覆写列表中展示 f"name: {Path(out_path).stem}", - "desc: HotKids 规则配置 · 由 Surge/Profile.conf 转译", + "desc: HotKids 规则配置 · 由 Surge/Profile.conf 自动转译", "author: '@HotKids'", ] diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 422879c16..10f2e7098 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,10 +1,10 @@ # Stash for Android -# Date: 2026-08-28 20:56:16 +# Date: 2026-08-28 20:59:23 # Author: @HotKids # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 name: Stash -desc: HotKids 规则配置 · 由 Surge/Profile.conf 转译 +desc: HotKids 规则配置 · 由 Surge/Profile.conf 自动转译 author: '@HotKids' # 通用设置 From e764499ac8ad637a2eed1645763719e096ea0711 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:01:07 +0000 Subject: [PATCH 18/28] style(sync): merge the duplicated header comments and metadata block The header comments and the YAML metadata stated the same three things twice: "# Stash for Android" against name, "# Author: @HotKids" against author, and the generated-by notice against desc. The YAML fields win because Stash displays them, so the two duplicated comment lines are gone and the platform label moved into name ("Stash for Android" / "MyStash for Android"). What stays as comments is what the fields do not carry: "# Date:", which the stamping helper matches on, and the do-not-edit-here notice, which is aimed at someone reading the repo rather than at Stash's override list. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 9 +++++---- Clash/Script/MyStash.stoverride | 6 ++---- Clash/Script/Stash.stoverride | 6 ++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index cbb5c489a..57207c71d 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4187,7 +4187,8 @@ def flush() -> None: first_key = next((i for i, l in enumerate(src) if _TOP_KEY_RE.match(l)), 0) header = [l.rstrip() for l in src[:first_key]] if header and header[0].startswith("# Clash"): - header[0] = "# Stash for Android" + del header[0] + header = [l for l in header if not l.startswith("# Author:")] # 紧贴首个键的那段注释是该键的说明(首个键必然是被略去的 mixed-port), # 随它一起去掉,避免留下孤儿注释;靠空行分隔的分区标题(# 通用设置)保留。 while header and header[-1].lstrip().startswith("#"): @@ -4296,12 +4297,12 @@ def flush() -> None: # 在文件头的 # Author 之后补一行生成说明(覆写的 name/desc 仅用于展示, # 源文件没有这些键,不属于差异点,不自行添加) - insert_at = next((i for i, l in enumerate(out) if l.startswith("# Author:")), 0) + 1 + insert_at = next((i for i, l in enumerate(out) if l.startswith("# Date:")), -1) + 1 out[insert_at:insert_at] = [ "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", "", # name / desc / author 仅用于在 Stash 覆写列表中展示 - f"name: {Path(out_path).stem}", + f"name: {Path(out_path).stem} for Android", "desc: HotKids 规则配置 · 由 Surge/Profile.conf 自动转译", "author: '@HotKids'", ] @@ -4391,7 +4392,7 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st # 展示字段改成本份定制版自己的,避免与基座在覆写列表里同名 for i, l in enumerate(lines): if l.startswith("name: "): - lines[i] = f"name: {Path(overlay['stash_output']).stem}" + lines[i] = f"name: {Path(overlay['stash_output']).stem} for Android" elif l.startswith("desc: "): lines[i] = f"desc: HotKids 规则配置 · 叠加 {label} 的私人差异" diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index cc0fd0169..3a89626e7 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,9 +1,7 @@ -# Stash for Android -# Date: 2026-08-28 20:56:16 -# Author: @HotKids +# Date: 2026-08-28 21:00:54 # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 -name: MyStash +name: MyStash for Android desc: HotKids 规则配置 · 叠加 myscript.overlay.json 的私人差异 author: '@HotKids' diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 10f2e7098..f47e7bc67 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,9 +1,7 @@ -# Stash for Android -# Date: 2026-08-28 20:59:23 -# Author: @HotKids +# Date: 2026-08-28 21:00:54 # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 -name: Stash +name: Stash for Android desc: HotKids 规则配置 · 由 Surge/Profile.conf 自动转译 author: '@HotKids' From 3da35efa50e731577474c7b499e74d8a2904bfa4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:02:20 +0000 Subject: [PATCH 19/28] style(sync): carry the timestamp as a date field instead of a comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the timestamp into the metadata block under author, alongside name/desc, matching how a published .stoverride writes it. The shared _write_stamped_if_changed only recognises a "# Date:" comment, so the Stash outputs get their own writer with the same ignore-the-timestamp comparison against a YAML date field — without it every run would rewrite both files just because the clock moved. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 34 ++++++++++++++++++++++++++++----- Clash/Script/MyStash.stoverride | 2 +- Clash/Script/Stash.stoverride | 2 +- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 57207c71d..90e04a8ba 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4143,6 +4143,29 @@ def _yq(value) -> str: return "'" + str(value).replace("'", "''") + "'" +_STASH_DATE_RE = re.compile(r"^date:.*$", re.MULTILINE) + + +def _stash_write_if_changed(filepath: Path, content: str) -> bool: + """按需写入 Stash 覆写:date 字段替换为当前北京时间。 + + 与 _write_stamped_if_changed 同样的「忽略时间戳比对」语义,只是时间戳载体 + 从 `# Date:` 注释换成了 YAML 的 date 字段——否则每次生成时间都不同, + 产物会无谓地反复变更。 + """ + content = _inject_general(content) + now = datetime.now(_CST).strftime("%Y-%m-%d %H:%M:%S") + stamped = _STASH_DATE_RE.sub(f"date: '{now}'", content, count=1) + if filepath.exists(): + placeholder = "date: __NORM__" + if (_STASH_DATE_RE.sub(placeholder, filepath.read_text(encoding="utf-8"), count=1) + == _STASH_DATE_RE.sub(placeholder, content, count=1)): + return False + filepath.parent.mkdir(parents=True, exist_ok=True) + filepath.write_text(stamped, encoding="utf-8") + return True + + def _stash_clean_nameserver(server: str) -> str: """mihomo 的 nameserver 策略后缀(#RULES / #策略名)在 Stash 中不存在——Stash 的 `#` 片段只承载选项(如 h3=true)。保留 h3= 这类合法选项,其余后缀一律剥离。""" @@ -4188,7 +4211,8 @@ def flush() -> None: header = [l.rstrip() for l in src[:first_key]] if header and header[0].startswith("# Clash"): del header[0] - header = [l for l in header if not l.startswith("# Author:")] + header = [l for l in header + if not l.startswith("# Author:") and not l.startswith("# Date:")] # 紧贴首个键的那段注释是该键的说明(首个键必然是被略去的 mixed-port), # 随它一起去掉,避免留下孤儿注释;靠空行分隔的分区标题(# 通用设置)保留。 while header and header[-1].lstrip().startswith("#"): @@ -4297,18 +4321,18 @@ def flush() -> None: # 在文件头的 # Author 之后补一行生成说明(覆写的 name/desc 仅用于展示, # 源文件没有这些键,不属于差异点,不自行添加) - insert_at = next((i for i, l in enumerate(out) if l.startswith("# Date:")), -1) + 1 - out[insert_at:insert_at] = [ + out[0:0] = [ "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", "", # name / desc / author 仅用于在 Stash 覆写列表中展示 f"name: {Path(out_path).stem} for Android", "desc: HotKids 规则配置 · 由 Surge/Profile.conf 自动转译", "author: '@HotKids'", + "date: ''", ] body = "\n".join(out).rstrip() + "\n" - changed = _write_stamped_if_changed(REPO_ROOT / out_path, body) + changed = _stash_write_if_changed(REPO_ROOT / out_path, body) for note in dict.fromkeys(changes): print(f" · {note}") print(f" {'✓ ' + out_path + ' 已更新' if changed else '✓ ' + out_path + ' 无变化'}") @@ -4521,7 +4545,7 @@ def _sync_stash_overlays(base_lines: list[str]) -> None: print(f" ── overlay: {path.name} → {target} ──") lines = _stash_apply_overlay(base_lines, overlay, path.name) body = "\n".join(lines).rstrip() + "\n" - changed = _write_stamped_if_changed(REPO_ROOT / target, body) + changed = _stash_write_if_changed(REPO_ROOT / target, body) print(f" {'✓ ' + target + ' 已更新' if changed else '✓ ' + target + ' 无变化'}") diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index 3a89626e7..272457707 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,9 +1,9 @@ -# Date: 2026-08-28 21:00:54 # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 name: MyStash for Android desc: HotKids 规则配置 · 叠加 myscript.overlay.json 的私人差异 author: '@HotKids' +date: '2026-08-28 21:01:49' # 通用设置 diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index f47e7bc67..a6b0a511f 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,9 +1,9 @@ -# Date: 2026-08-28 21:00:54 # 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 name: Stash for Android desc: HotKids 规则配置 · 由 Surge/Profile.conf 自动转译 author: '@HotKids' +date: '2026-08-28 21:01:49' # 通用设置 From 9ba7c5a27d70d2f652f6fd1d08f8d15f8bab50f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:03:10 +0000 Subject: [PATCH 20/28] style(sync): fold the generated-by notice into desc, keep one # Date header desc now carries the generated-by notice instead of repeating it as a comment line, and the header is down to a single "# Date:". The timestamp goes back to that comment, so the shared _write_stamped_if_changed handles it again and the Stash-specific writer added for a YAML date field is gone. The overlay variant's desc names the overlay it layers on, which is the only part that differs between the two files. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 40 +++++++-------------------------- Clash/Script/MyStash.stoverride | 5 ++--- Clash/Script/Stash.stoverride | 5 ++--- 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 90e04a8ba..48faf4a1d 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4143,29 +4143,6 @@ def _yq(value) -> str: return "'" + str(value).replace("'", "''") + "'" -_STASH_DATE_RE = re.compile(r"^date:.*$", re.MULTILINE) - - -def _stash_write_if_changed(filepath: Path, content: str) -> bool: - """按需写入 Stash 覆写:date 字段替换为当前北京时间。 - - 与 _write_stamped_if_changed 同样的「忽略时间戳比对」语义,只是时间戳载体 - 从 `# Date:` 注释换成了 YAML 的 date 字段——否则每次生成时间都不同, - 产物会无谓地反复变更。 - """ - content = _inject_general(content) - now = datetime.now(_CST).strftime("%Y-%m-%d %H:%M:%S") - stamped = _STASH_DATE_RE.sub(f"date: '{now}'", content, count=1) - if filepath.exists(): - placeholder = "date: __NORM__" - if (_STASH_DATE_RE.sub(placeholder, filepath.read_text(encoding="utf-8"), count=1) - == _STASH_DATE_RE.sub(placeholder, content, count=1)): - return False - filepath.parent.mkdir(parents=True, exist_ok=True) - filepath.write_text(stamped, encoding="utf-8") - return True - - def _stash_clean_nameserver(server: str) -> str: """mihomo 的 nameserver 策略后缀(#RULES / #策略名)在 Stash 中不存在——Stash 的 `#` 片段只承载选项(如 h3=true)。保留 h3= 这类合法选项,其余后缀一律剥离。""" @@ -4211,8 +4188,7 @@ def flush() -> None: header = [l.rstrip() for l in src[:first_key]] if header and header[0].startswith("# Clash"): del header[0] - header = [l for l in header - if not l.startswith("# Author:") and not l.startswith("# Date:")] + header = [l for l in header if not l.startswith("# Author:")] # 紧贴首个键的那段注释是该键的说明(首个键必然是被略去的 mixed-port), # 随它一起去掉,避免留下孤儿注释;靠空行分隔的分区标题(# 通用设置)保留。 while header and header[-1].lstrip().startswith("#"): @@ -4321,18 +4297,17 @@ def flush() -> None: # 在文件头的 # Author 之后补一行生成说明(覆写的 name/desc 仅用于展示, # 源文件没有这些键,不属于差异点,不自行添加) - out[0:0] = [ - "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", + insert_at = next((i for i, l in enumerate(out) if l.startswith("# Date:")), -1) + 1 + out[insert_at:insert_at] = [ "", # name / desc / author 仅用于在 Stash 覆写列表中展示 f"name: {Path(out_path).stem} for Android", - "desc: HotKids 规则配置 · 由 Surge/Profile.conf 自动转译", + "desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", "author: '@HotKids'", - "date: ''", ] body = "\n".join(out).rstrip() + "\n" - changed = _stash_write_if_changed(REPO_ROOT / out_path, body) + changed = _write_stamped_if_changed(REPO_ROOT / out_path, body) for note in dict.fromkeys(changes): print(f" · {note}") print(f" {'✓ ' + out_path + ' 已更新' if changed else '✓ ' + out_path + ' 无变化'}") @@ -4418,7 +4393,8 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st if l.startswith("name: "): lines[i] = f"name: {Path(overlay['stash_output']).stem} for Android" elif l.startswith("desc: "): - lines[i] = f"desc: HotKids 规则配置 · 叠加 {label} 的私人差异" + lines[i] = (f"desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译," + f"叠加 {label}),请勿手改;改内容请改 Surge/Profile.conf。") # 1) group_overrides:改写既有组的字段(filter 为 null 表示删掉该行) for name, patch in (overlay.get("group_overrides") or {}).items(): @@ -4545,7 +4521,7 @@ def _sync_stash_overlays(base_lines: list[str]) -> None: print(f" ── overlay: {path.name} → {target} ──") lines = _stash_apply_overlay(base_lines, overlay, path.name) body = "\n".join(lines).rstrip() + "\n" - changed = _stash_write_if_changed(REPO_ROOT / target, body) + changed = _write_stamped_if_changed(REPO_ROOT / target, body) print(f" {'✓ ' + target + ' 已更新' if changed else '✓ ' + target + ' 无变化'}") diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index 272457707..661773f92 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,9 +1,8 @@ -# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 +# Date: 2026-08-28 21:02:57 name: MyStash for Android -desc: HotKids 规则配置 · 叠加 myscript.overlay.json 的私人差异 +desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译,叠加 myscript.overlay.json),请勿手改;改内容请改 Surge/Profile.conf。 author: '@HotKids' -date: '2026-08-28 21:01:49' # 通用设置 diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index a6b0a511f..6f3180c41 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,9 +1,8 @@ -# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 +# Date: 2026-08-28 21:02:57 name: Stash for Android -desc: HotKids 规则配置 · 由 Surge/Profile.conf 自动转译 +desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 author: '@HotKids' -date: '2026-08-28 21:01:49' # 通用设置 From 5cc5107498e63b8a4718c4d921c7c7d54df689d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:07:07 +0000 Subject: [PATCH 21/28] style: formalize the Chinese wording across the sync tooling and its output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "请勿手改" is not a real word, and "改内容请改 X" repeats 改 twice; the notice now reads 请勿手动修改;如需调整请修改 Surge/Profile.conf, matching how the root README already phrases it. Applied to the .stoverride desc, the generated Enhance Script headers and the four .js files already checked in, so no copy is left saying it the old way. Same pass over the comments added for the Stash transcription: 略去→省略, 剥掉→去除, 丢掉→舍弃/丢失, 剪掉→移除, 删掉→移除, 孤儿注释→无主注释, 永不命中→无法命中, 由 App 自身掌管→由客户端自身管理. Also drops a stale line in the scripts README claiming the nameserver array is replaced via #!replace — those markers were removed earlier; the suffix is stripped during transcription instead. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/README.md | 28 +++++++-------- .github/scripts/sync-config.py | 64 ++++++++++++++++----------------- .github/scripts/sync-config.txt | 2 +- Clash/Script/MyClashBox.js | 4 +-- Clash/Script/MyScript.js | 4 +-- Clash/Script/MyScriptColor.js | 4 +-- Clash/Script/MyStash.stoverride | 6 ++-- Clash/Script/Script.js | 6 ++-- Clash/Script/Stash.stoverride | 6 ++-- 9 files changed, 62 insertions(+), 62 deletions(-) diff --git a/.github/scripts/README.md b/.github/scripts/README.md index a517ad9c5..2e51fa3d6 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -69,19 +69,19 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do 同一定位:整份配置逐行转录(含注释与排版),**只改写 Stash 与 mihomo 真正有差异的点**,因此 可直接作为覆写文件导入 Stash 使用。差异点仅以下四类: -- **略去 mihomo 专属的顶层键 / 整块**(连同其前置注释):监听与控制面(`mixed-port` / +- **省略 mihomo 专属的顶层键 / 整块**(连同其前置注释):监听与控制面(`mixed-port` / `allow-lan` / `bind-address` / `external-controller`)、`ipv6`、geo 数据源(`geodata-loader` / `geox-url` / `geo-auto-update` / `geo-update-interval`)、`unified-delay` / `tcp-concurrent` / `find-process-mode` / `global-ua` / `keep-alive-interval`,以及 `profile` / `ntp` / `sniffer` / - `tun` 和空占位 `proxies`。这些能力在 Stash 由 App 自身掌管或无对应项。 + `tun` 和空占位 `proxies`。这些能力在 Stash 中由客户端自身管理,或无对应项。 - **DNS 子键过滤**:只保留 Stash 文档支持的 `default-nameserver` / `nameserver` / - `nameserver-policy` / `proxy-server-nameserver` / `fake-ip-filter`,其余 14 个 mihomo 专属键 - (`enhanced-mode` / `fake-ip-range` / `cache-algorithm` / `direct-nameserver` 等)略去。 -- **DNS 写法转换**:mihomo 用每条 nameserver 的 `#RULES` 后缀表达「跟随规则」,Stash 是全局 - 开关 → 补 `follow-rule: true`,并用 `#!replace` 整体替换 nameserver 数组以去掉该后缀(Stash 的 - `#` 片段只承载 `h3=true` 这类选项);`nameserver-policy` 里逗号拼接的多域名单键是 mihomo 专属, - 按 Stash 语法拆成独立键(官方只支持精确域名 / 通配域名 / `geosite:`)。 -- **Provider 字段**:去掉 mihomo 专属的 `type`;proxy-providers 的 `header` 改为 Stash 文档 + `nameserver-policy` / `proxy-server-nameserver` / `fake-ip-filter`,其余 14 个 mihomo 专属键则予以省略 + (`enhanced-mode` / `fake-ip-range` / `cache-algorithm` / `direct-nameserver` 等)。 +- **DNS 写法转换**:mihomo 用每条 nameserver 的 `#RULES` 后缀表达「跟随规则」,Stash 为全局 + 开关 → 补 `follow-rule: true`,并在转译时去除该后缀(Stash 的 `#` 片段只承载 `h3=true` + 这类选项);`nameserver-policy` 里逗号拼接的多域名单键是 mihomo 专属, + 按 Stash 语法拆分为独立键(官方仅支持精确域名 / 通配域名 / `geosite:`)。 +- **Provider 字段**:移除 mihomo 专属的 `type`;proxy-providers 的 `header` 改为 Stash 文档 拼写的 `headers`。 其余内容——`hosts` / `mode` / `log-level`、23 个策略组(含 `use: [Server]` 与地区 `filter`)、 @@ -96,13 +96,13 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do `Clash/Script/MyStash.stoverride` 是 Stash 的私人定制版,复用 `Enhanced/` 下的同一份 overlay:只要 overlay 里除 `output` 外再声明一个 `stash_output`,就会在 Stash 基座上叠加同样 的私人差异(目前只有 `myscript.overlay.json` 声明了)。overlay 的差异声明本身与输出格式无关, -但 `_apply_overlay` 面向解析后的结构、供 Script.js 使用,而 Stash 侧是文本级转译(要保住 -Sample.yaml 的注释与排版),因此这些指令在 `_stash_apply_overlay` 里按文本重新实现,**遇到尚未 -实现的指令直接报错**,避免私人差异被静默丢掉。 +但 `_apply_overlay` 面向解析后的结构、供 Script.js 使用,而 Stash 侧为文本级转译(需保留 +Sample.yaml 的注释与排版),因此这些指令在 `_stash_apply_overlay` 中按文本重新实现,**遇到尚未 +实现的指令直接报错**,避免私人差异被静默丢弃。 其中 `disabled_by_default` 没有静态等价物——它是 Script.js 的运行时开关(`ruleOptionsEnable`), -YAML 覆写没有「默认关但可开」这种状态。因此按声明**整组剪掉**:删组、删以它为落点的规则、删 -其余分组候选里对它的引用,并清掉因此不再被任何 `RULE-SET` 引用的规则集。`extra_pool_groups` +YAML 覆写没有「默认关但可开」这种状态。因此按声明**整组移除**:移除该组、以其为落点的规则,以及 +其余分组候选中对它的引用,并清理因此不再被任何 `RULE-SET` 引用的规则集。`extra_pool_groups` 的新增池组在 Script.js 里靠运行时过滤 `config.proxies` 填充,静态 YAML 必须显式写节点来源, 统一按基座地区组的写法输出 `use: [Server]` + `filter`。 diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 48faf4a1d..1aea9192d 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -3078,7 +3078,7 @@ def _gen_mihomo_yaml(sample_yaml_text: str) -> str: "# Date: ", "# Author: @HotKids", "#", - "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", + "# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。", "", ] @@ -3259,17 +3259,17 @@ def _gen_clash_script_js( if overlay: source_lines = [ - " * 自动生成,请勿手改:由 sync-config.py 从 Surge/Profile.conf(经", + " * 自动生成,请勿手动修改:由 sync-config.py 从 Surge/Profile.conf(经", f" * Clash/Mihomo.yaml)叠加 sync-config/Enhanced/{overlay_label}(私人差异声明)", - " * 而来,直接改本文件会在下次同步时被覆盖。公共部分请改 Surge/Profile.conf;", + " * 而来,直接修改本文件将在下次同步时被覆盖。公共部分请修改 Surge/Profile.conf;", " * 私人差异(改名 / 换图标 / 额外分组 / 分组类型 / 候选节点 / 默认开关等)", f" * 请改 {overlay_label}。", ] else: source_lines = [ - " * 自动生成,请勿手改:由 sync-config.py 从 Surge/Profile.conf(经", - " * Clash/Mihomo.yaml)转译而来,直接改本文件会在下次同步时被覆盖;", - " * 要改内容请改 Surge/Profile.conf。", + " * 自动生成,请勿手动修改:由 sync-config.py 从 Surge/Profile.conf(经", + " * Clash/Mihomo.yaml)转译而来,直接修改本文件将在下次同步时被覆盖;", + " * 如需调整请修改 Surge/Profile.conf。", ] lines = [ @@ -4117,10 +4117,10 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - # # Clash/Stash.stoverride 是 Clash/Sample.yaml 的二次转换产物(与 Clash/Mihomo.yaml # 同一定位):整份配置原样转录,只在 Stash 与 mihomo 真正有差异的点上改写,因此 -# 可以直接作为覆写文件导入 Stash 使用。差异点仅以下四类,其余逐行照搬(含注释与排版)。 +# 可以直接作为覆写文件导入 Stash 使用。差异点仅以下四类,其余逐行转录(含注释与排版)。 -# 1) mihomo 专属的顶层键 / 整块——Stash 文档中不存在,且多为 Stash 由 App 自身掌管 -# 的能力(监听端口、TUN、嗅探、geo 数据源等),连同其前置注释一并略去。 +# 1) mihomo 专属的顶层键 / 整块——Stash 文档中不存在,且多为 Stash 由客户端自身管理 +# 的能力(监听端口、TUN、嗅探、geo 数据源等),连同其前置注释一并省略。 _STASH_DROP_TOP = { "mixed-port", "allow-lan", "bind-address", "ipv6", "external-controller", "unified-delay", "tcp-concurrent", "find-process-mode", "geodata-loader", @@ -4128,7 +4128,7 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - "geox-url", "profile", "ntp", "sniffer", "tun", "proxies", } -# 2) dns 块内 Stash 支持的子键(其余为 mihomo 专属,略去) +# 2) dns 块内 Stash 支持的子键(其余为 mihomo 专属,予以省略) _STASH_DNS_KEEP = { "default-nameserver", "nameserver", "nameserver-policy", "proxy-server-nameserver", "fake-ip-filter", @@ -4139,13 +4139,13 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - def _yq(value) -> str: - """YAML 单引号标量(组名 / filter 正则含 emoji、空格、反斜杠,统一加引号最稳)。""" + """YAML 单引号标量(组名 / filter 正则含 emoji、空格、反斜杠,统一加引号最为稳妥)。""" return "'" + str(value).replace("'", "''") + "'" def _stash_clean_nameserver(server: str) -> str: """mihomo 的 nameserver 策略后缀(#RULES / #策略名)在 Stash 中不存在——Stash 的 - `#` 片段只承载选项(如 h3=true)。保留 h3= 这类合法选项,其余后缀一律剥离。""" + `#` 片段只承载选项(如 h3=true)。保留 h3= 这类合法选项,其余后缀一律去除。""" if "#" not in server: return server base, frag = server.split("#", 1) @@ -4175,7 +4175,7 @@ def _sync_stash(config: dict) -> None: changes: list[str] = [] def flush() -> None: - # 略去某个键时它两侧的空行会跨过删除点叠在一起;这里丢掉与已输出空行相邻的 + # 省略某个键时,其两侧的空行会在删除处相邻叠加;此处舍弃与已输出空行相邻的 # 前导空行,避免出现源文件没有的空行堆积(源本身的空行结构保持不变)。 while buf and not buf[0].strip() and out and not out[-1].strip(): buf.pop(0) @@ -4183,14 +4183,14 @@ def flush() -> None: buf.clear() # 文件头(首个顶层键之前的注释/空行,含 # Clash / # Date / # Author / # 通用设置) - # 始终保留:它不属于任何键,不能跟着被略去的首个键一起丢掉。 + # 始终保留:其不属于任何键,不应随被省略的首个键一同丢失。 first_key = next((i for i, l in enumerate(src) if _TOP_KEY_RE.match(l)), 0) header = [l.rstrip() for l in src[:first_key]] if header and header[0].startswith("# Clash"): del header[0] header = [l for l in header if not l.startswith("# Author:")] - # 紧贴首个键的那段注释是该键的说明(首个键必然是被略去的 mixed-port), - # 随它一起去掉,避免留下孤儿注释;靠空行分隔的分区标题(# 通用设置)保留。 + # 紧贴首个键的那段注释是该键的说明(首个键必为被省略的 mixed-port), + # 一并省略,避免遗留无主注释;靠空行分隔的分区标题(# 通用设置)保留。 while header and header[-1].lstrip().startswith("#"): header.pop() out.extend(header) @@ -4208,7 +4208,7 @@ def flush() -> None: out.append(f' "{dom}":') out.extend(policy_val) policy_split, policy_val = None, [] - # 落到下面继续处理当前行 + # 继续按普通行处理当前行 m_top = _TOP_KEY_RE.match(line) if m_top: @@ -4226,7 +4226,7 @@ def flush() -> None: out += [ " # DNS 查询跟随规则出站(mihomo 用 nameserver 的 #RULES 后缀表达,", " # Stash 为全局开关)。官方提示多数场景无需开启:DNS 经代理转发可能", - " # 破坏云服务商 CDN 优化并轻微增加延迟。如需 DNS 直连改为 false。", + " # 破坏云服务商 CDN 优化并轻微增加延迟;如需 DNS 直连,将其改为 false。", " # 下方 proxy-server-nameserver 已为代理服务器域名提供独立解析,", " # 满足官方要求的前置条件之一(避免递归查询)。", " follow-rule: true", @@ -4259,7 +4259,7 @@ def flush() -> None: buf.clear() continue - # nameserver 条目:剥掉 #RULES 后缀 + # nameserver 条目:去除 #RULES 后缀 if top == "dns" and dns_keep and stripped.startswith("- ") and "#RULES" in line: flush() val = stripped[2:].strip().strip("'\"") @@ -4268,7 +4268,7 @@ def flush() -> None: continue # nameserver-policy:逗号拼接多域名的单键是 mihomo 专属;Stash 只认 - # 「精确域名 / 通配域名 / geosite:」,拼接键会被当成字面域名永不命中。 + # 「精确域名 / 通配域名 / geosite:」,拼接键将被视作字面域名,无法命中。 if top == "dns" and indent == 4 and m_sub and "," in m_sub.group(3): flush() policy_split = [d.strip() for d in m_sub.group(3).split(",") if d.strip()] @@ -4302,7 +4302,7 @@ def flush() -> None: "", # name / desc / author 仅用于在 Stash 覆写列表中展示 f"name: {Path(out_path).stem} for Android", - "desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。", + "desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。", "author: '@HotKids'", ] @@ -4318,10 +4318,10 @@ def flush() -> None: # ── Enhanced/*.overlay.json → Stash 私人定制版 ──────────────────────────── # # overlay 声明的是「相对基座的私人差异」,本身与输出格式无关(_apply_overlay 面向 -# 解析后的 groups/rules 结构,供 Script.js 使用)。Stash 侧是文本级转译(要保住 +# 解析后的 groups/rules 结构,供 Script.js 使用)。Stash 侧为文本级转译(需保留 # Sample.yaml 的注释与排版),因此这里按同一份 overlay 在文本层实现对应改写。 # 只有声明了 stash_output 的 overlay 才会产出 .stoverride;未实现的指令直接报错, -# 避免私人差异被静默丢掉。 +# 避免私人差异被静默丢弃。 _STASH_OVERLAY_OK = { "_comment", "output", "stash_output", "extends", "disabled_by_default", "rules_insert", "group_overrides", @@ -4361,7 +4361,7 @@ def _stash_render_group(g: dict) -> list[str]: out.append(f" icon: {g['icon']}") if g.get("hidden"): out.append(" hidden: true") - # 池组的节点来源:与基座地区组写法一致,从本仓库 provider 里按 filter 筛 + # 池组的节点来源:与基座地区组写法一致,按 filter 从本仓库 provider 中筛选 out += [" use:", " - Server"] for key in ("interval", "tolerance", "lazy"): if key in g: @@ -4388,15 +4388,15 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st lines = list(lines) notes: list[str] = [] - # 展示字段改成本份定制版自己的,避免与基座在覆写列表里同名 + # 展示字段替换为本定制版专属内容,避免与基座在覆写列表中同名 for i, l in enumerate(lines): if l.startswith("name: "): lines[i] = f"name: {Path(overlay['stash_output']).stem} for Android" elif l.startswith("desc: "): lines[i] = (f"desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译," - f"叠加 {label}),请勿手改;改内容请改 Surge/Profile.conf。") + f"叠加 {label}),请勿手动修改;如需调整请修改 Surge/Profile.conf。") - # 1) group_overrides:改写既有组的字段(filter 为 null 表示删掉该行) + # 1) group_overrides:改写既有组的字段(filter 为 null 表示移除该行) for name, patch in (overlay.get("group_overrides") or {}).items(): span = _stash_group_spans(lines).get(name) if span is None: @@ -4441,7 +4441,7 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st lines[at:at] = [f" - {p}" for p in spec["insert"]] notes.append(f"{name}: 候选插入 {len(spec['insert'])} 项") - # 3) extra_pool_groups:整组新增,插到锚点组之后 + # 3) extra_pool_groups:整组新增,插入至锚点组之后 for g in (overlay.get("extra_pool_groups") or []): spans = _stash_group_spans(lines) anchor = g.get("insert_after") @@ -4462,15 +4462,15 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st lines[at:at] = [f" - {r}" for r in spec["rules"]] notes.append(f"规则插入 {len(spec['rules'])} 条") - # 5) disabled_by_default:静态配置没有运行时开关,按声明整组剪掉—— - # 删组、删以它为落点的规则、删其余组候选里对它的引用, - # 最后清掉因此不再被任何 RULE-SET 引用的规则集。 + # 5) disabled_by_default:静态配置没有运行时开关,按声明整组移除—— + # 移除该组、以其为落点的规则,以及其余组候选中对它的引用, + # 并清理因此不再被任何 RULE-SET 引用的规则集。 for name in (overlay.get("disabled_by_default") or []): spans = _stash_group_spans(lines) if name not in spans: raise ValueError(f"{label}: disabled_by_default 引用了不存在的分组 {name!r}") s, e = spans[name] - # 组前的注释行一并删掉 + # 组前的注释行一并移除 while s > 0 and lines[s - 1].lstrip().startswith("#"): s -= 1 del lines[s:e] diff --git a/.github/scripts/sync-config.txt b/.github/scripts/sync-config.txt index 4160a2f17..b1024bfce 100644 --- a/.github/scripts/sync-config.txt +++ b/.github/scripts/sync-config.txt @@ -62,7 +62,7 @@ rule-providers: # Stash # 覆写文件:由上面的 Clash 产物二次转换(同 Mihomo.yaml 定位),整份逐行转录, -# 只改写 Stash 与 mihomo 的差异点。无 Builtin,内容全部由脚本推导。 +# 仅改写 Stash 与 mihomo 的差异点。无 Builtin 段,内容全部由脚本推导。 >> Clash/Script/Stash.stoverride # Quantumult X diff --git a/Clash/Script/MyClashBox.js b/Clash/Script/MyClashBox.js index 62c0af894..58a077b7f 100644 --- a/Clash/Script/MyClashBox.js +++ b/Clash/Script/MyClashBox.js @@ -5,9 +5,9 @@ * (如 https://sub.hotkids.me)动态套用与本仓库 Surge/Profile.conf 等效的 * 策略组、分流规则与基础设置,不必依赖机场自带配置。 * - * 自动生成,请勿手改:由 sync-config.py 从 Surge/Profile.conf(经 + * 自动生成,请勿手动修改:由 sync-config.py 从 Surge/Profile.conf(经 * Clash/Mihomo.yaml)叠加 sync-config/Enhanced/clashbox.overlay.json(私人差异声明) - * 而来,直接改本文件会在下次同步时被覆盖。公共部分请改 Surge/Profile.conf; + * 而来,直接修改本文件将在下次同步时被覆盖。公共部分请修改 Surge/Profile.conf; * 私人差异(改名 / 换图标 / 额外分组 / 分组类型 / 候选节点 / 默认开关等) * 请改 clashbox.overlay.json。 * diff --git a/Clash/Script/MyScript.js b/Clash/Script/MyScript.js index df60502d1..cb3289285 100644 --- a/Clash/Script/MyScript.js +++ b/Clash/Script/MyScript.js @@ -5,9 +5,9 @@ * (如 https://sub.hotkids.me)动态套用与本仓库 Surge/Profile.conf 等效的 * 策略组、分流规则与基础设置,不必依赖机场自带配置。 * - * 自动生成,请勿手改:由 sync-config.py 从 Surge/Profile.conf(经 + * 自动生成,请勿手动修改:由 sync-config.py 从 Surge/Profile.conf(经 * Clash/Mihomo.yaml)叠加 sync-config/Enhanced/myscript.overlay.json(私人差异声明) - * 而来,直接改本文件会在下次同步时被覆盖。公共部分请改 Surge/Profile.conf; + * 而来,直接修改本文件将在下次同步时被覆盖。公共部分请修改 Surge/Profile.conf; * 私人差异(改名 / 换图标 / 额外分组 / 分组类型 / 候选节点 / 默认开关等) * 请改 myscript.overlay.json。 * diff --git a/Clash/Script/MyScriptColor.js b/Clash/Script/MyScriptColor.js index 9c39a8e50..1cdc1251d 100644 --- a/Clash/Script/MyScriptColor.js +++ b/Clash/Script/MyScriptColor.js @@ -5,9 +5,9 @@ * (如 https://sub.hotkids.me)动态套用与本仓库 Surge/Profile.conf 等效的 * 策略组、分流规则与基础设置,不必依赖机场自带配置。 * - * 自动生成,请勿手改:由 sync-config.py 从 Surge/Profile.conf(经 + * 自动生成,请勿手动修改:由 sync-config.py 从 Surge/Profile.conf(经 * Clash/Mihomo.yaml)叠加 sync-config/Enhanced/myscriptcolor.overlay.json(私人差异声明) - * 而来,直接改本文件会在下次同步时被覆盖。公共部分请改 Surge/Profile.conf; + * 而来,直接修改本文件将在下次同步时被覆盖。公共部分请修改 Surge/Profile.conf; * 私人差异(改名 / 换图标 / 额外分组 / 分组类型 / 候选节点 / 默认开关等) * 请改 myscriptcolor.overlay.json。 * diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index 661773f92..fa18350ae 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,7 +1,7 @@ -# Date: 2026-08-28 21:02:57 +# Date: 2026-08-28 21:06:08 name: MyStash for Android -desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译,叠加 myscript.overlay.json),请勿手改;改内容请改 Surge/Profile.conf。 +desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译,叠加 myscript.overlay.json),请勿手动修改;如需调整请修改 Surge/Profile.conf。 author: '@HotKids' # 通用设置 @@ -23,7 +23,7 @@ hosts: dns: # DNS 查询跟随规则出站(mihomo 用 nameserver 的 #RULES 后缀表达, # Stash 为全局开关)。官方提示多数场景无需开启:DNS 经代理转发可能 - # 破坏云服务商 CDN 优化并轻微增加延迟。如需 DNS 直连改为 false。 + # 破坏云服务商 CDN 优化并轻微增加延迟;如需 DNS 直连,将其改为 false。 # 下方 proxy-server-nameserver 已为代理服务器域名提供独立解析, # 满足官方要求的前置条件之一(避免递归查询)。 follow-rule: true diff --git a/Clash/Script/Script.js b/Clash/Script/Script.js index fc04830ff..9d53b91aa 100644 --- a/Clash/Script/Script.js +++ b/Clash/Script/Script.js @@ -5,9 +5,9 @@ * (如 https://sub.hotkids.me)动态套用与本仓库 Surge/Profile.conf 等效的 * 策略组、分流规则与基础设置,不必依赖机场自带配置。 * - * 自动生成,请勿手改:由 sync-config.py 从 Surge/Profile.conf(经 - * Clash/Mihomo.yaml)转译而来,直接改本文件会在下次同步时被覆盖; - * 要改内容请改 Surge/Profile.conf。 + * 自动生成,请勿手动修改:由 sync-config.py 从 Surge/Profile.conf(经 + * Clash/Mihomo.yaml)转译而来,直接修改本文件将在下次同步时被覆盖; + * 如需调整请修改 Surge/Profile.conf。 * * 本地唯一可临时修改的是下方 ruleOptionsEnable 的取值,用于按需开关某个分组。 * diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 6f3180c41..161dac8d0 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,7 +1,7 @@ -# Date: 2026-08-28 21:02:57 +# Date: 2026-08-28 21:06:08 name: Stash for Android -desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 +desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。 author: '@HotKids' # 通用设置 @@ -23,7 +23,7 @@ hosts: dns: # DNS 查询跟随规则出站(mihomo 用 nameserver 的 #RULES 后缀表达, # Stash 为全局开关)。官方提示多数场景无需开启:DNS 经代理转发可能 - # 破坏云服务商 CDN 优化并轻微增加延迟。如需 DNS 直连改为 false。 + # 破坏云服务商 CDN 优化并轻微增加延迟;如需 DNS 直连,将其改为 false。 # 下方 proxy-server-nameserver 已为代理服务器域名提供独立解析, # 满足官方要求的前置条件之一(避免递归查询)。 follow-rule: true From c2e81932d9b84176894bf935a3f7583579206a26 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:08:06 +0000 Subject: [PATCH 22/28] feat(sync): mark every block key #!replace, except MyStash's proxy-groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hosts, dns, proxy-providers, proxy-groups, rule-providers and rules now carry the marker so each block replaces the base config's rather than merging or prepending. Scalars (mode, log-level) need no marker — the docs already define same-named simple types as a direct overwrite. MyStash keeps proxy-groups unmarked, so its groups follow the default insert -before behaviour. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 11 ++++++++++- Clash/Script/MyStash.stoverride | 12 ++++++------ Clash/Script/Stash.stoverride | 14 +++++++------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 1aea9192d..ec8d8904f 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4129,6 +4129,9 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - } # 2) dns 块内 Stash 支持的子键(其余为 mihomo 专属,予以省略) +_STASH_REPLACE_TOP = {"hosts", "dns", "proxy-providers", "proxy-groups", + "rule-providers", "rules"} + _STASH_DNS_KEEP = { "default-nameserver", "nameserver", "nameserver-policy", "proxy-server-nameserver", "fake-ip-filter", @@ -4219,7 +4222,7 @@ def flush() -> None: changes.append(f"略去 {top}") continue flush() - out.append(line) + out.append(f"{line} #!replace" if top in _STASH_REPLACE_TOP else line) if top == "dns": # mihomo 用每条 nameserver 的 #RULES 后缀表达「DNS 跟随规则」, # Stash 的等价物是全局开关 follow-rule。 @@ -4388,6 +4391,12 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st lines = list(lines) notes: list[str] = [] + # 定制版的 proxy-groups 保持默认的前置插入,不整体替换 + for i, l in enumerate(lines): + if l.startswith("proxy-groups:"): + lines[i] = "proxy-groups:" + break + # 展示字段替换为本定制版专属内容,避免与基座在覆写列表中同名 for i, l in enumerate(lines): if l.startswith("name: "): diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index fa18350ae..381245ee5 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,4 +1,4 @@ -# Date: 2026-08-28 21:06:08 +# Date: 2026-08-28 21:07:50 name: MyStash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译,叠加 myscript.overlay.json),请勿手动修改;如需调整请修改 Surge/Profile.conf。 @@ -15,12 +15,12 @@ log-level: info # ── Hosts ── # 静态域名映射,优先级高于 DNS 解析 -hosts: +hosts: #!replace 'localhost': 127.0.0.1 # ── DNS ── -dns: +dns: #!replace # DNS 查询跟随规则出站(mihomo 用 nameserver 的 #RULES 后缀表达, # Stash 为全局开关)。官方提示多数场景无需开启:DNS 经代理转发可能 # 破坏云服务商 CDN 优化并轻微增加延迟;如需 DNS 直连,将其改为 false。 @@ -132,7 +132,7 @@ dns: # 服务器订阅配置 -proxy-providers: +proxy-providers: #!replace Server: path: ./Provider/Proxy/Server.yaml url: https://sub.hotkids.me @@ -415,7 +415,7 @@ proxy-groups: - Server filter: '(?i)^(?=.*\b(?:DE|DEU)\d*\b)' -rule-providers: +rule-providers: #!replace # name: # Provider 名称 # type: http # http 或 file # behavior: classical # 或 ipcidr、domain @@ -626,7 +626,7 @@ rule-providers: interval: 86400 # 规则 -rules: +rules: #!replace # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) - AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 161dac8d0..6a5f23d91 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,4 +1,4 @@ -# Date: 2026-08-28 21:06:08 +# Date: 2026-08-28 21:07:50 name: Stash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。 @@ -15,12 +15,12 @@ log-level: info # ── Hosts ── # 静态域名映射,优先级高于 DNS 解析 -hosts: +hosts: #!replace 'localhost': 127.0.0.1 # ── DNS ── -dns: +dns: #!replace # DNS 查询跟随规则出站(mihomo 用 nameserver 的 #RULES 后缀表达, # Stash 为全局开关)。官方提示多数场景无需开启:DNS 经代理转发可能 # 破坏云服务商 CDN 优化并轻微增加延迟;如需 DNS 直连,将其改为 false。 @@ -132,7 +132,7 @@ dns: # 服务器订阅配置 -proxy-providers: +proxy-providers: #!replace Server: path: ./Provider/Proxy/Server.yaml url: https://sub.hotkids.me @@ -149,7 +149,7 @@ proxy-providers: timeout: 5000 expected-status: 204 -proxy-groups: +proxy-groups: #!replace # Global # name: "GLOBAL" # type: select @@ -357,7 +357,7 @@ proxy-groups: # 关于 Rule Provider 请查阅:https://wiki.metacubex.one/en/config/rule-providers/ -rule-providers: +rule-providers: #!replace # name: # Provider 名称 # type: http # http 或 file # behavior: classical # 或 ipcidr、domain @@ -575,7 +575,7 @@ rule-providers: interval: 86400 # 规则 -rules: +rules: #!replace # 境外 QUIC(UDP 443)拦截回退 TCP、国内放行(Surge PROTOCOL,QUIC 转 Clash 无直接等价,用 mihomo 逻辑规则表达) - AND,((NETWORK,UDP),(DST-PORT,443),(NOT,((OR,((GEOSITE,cn),(GEOIP,CN)))))),⛔️ REJECT From 9888975cf69ee1c7c52ff056b1212187255882dd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:10:26 +0000 Subject: [PATCH 23/28] feat(sync): annotate the proxy-providers block in the Stash overrides The provider block was the only one arriving without per-field comments, since the source config has none there. The Stash outputs are read by whoever imports them, so the transform now adds a line for each field: cache path, subscription URL and refresh interval, why the fetch is pinned to DIRECT (the groups do not exist yet, so going through the proxy would be circular), what the User-Agent header is for, and each health-check parameter. Comments are keyed by (indent, key) because url and interval appear both on the provider and inside health-check. The dropped mihomo-only "type" gets none, and the comment for "header" avoids naming the key so it still reads correctly after the rename to "headers". Added in the transform rather than in clash.ini, so Sample.yaml and the other platform outputs are untouched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 22 ++++++++++++++++++++-- Clash/Script/MyStash.stoverride | 13 ++++++++++++- Clash/Script/Stash.stoverride | 13 ++++++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index ec8d8904f..68728017f 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4129,6 +4129,20 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - } # 2) dns 块内 Stash 支持的子键(其余为 mihomo 专属,予以省略) +_STASH_PROVIDER_NOTES = { + (4, "path"): "本地缓存路径,启动时优先读取,拉取失败也能沿用上次结果", + (4, "url"): "订阅地址", + (4, "interval"): "订阅更新间隔(秒)", + (4, "proxy"): "拉取订阅固定直连:此时策略组尚未就绪,经代理会形成循环依赖", + (4, "header"): "拉取订阅时携带的请求头:部分机场按 UA 返回不同格式,声明 Clash / mihomo 以取得正确内容", + (4, "health-check"): "节点可用性检查", + (6, "enable"): "是否启用", + (6, "url"): "测试地址:返回 204 空响应,体积最小", + (6, "interval"): "检查间隔(秒)", + (6, "timeout"): "单次超时(毫秒)", + (6, "expected-status"): "期望的 HTTP 状态码,不符则判定为不可用", +} + _STASH_REPLACE_TOP = {"hosts", "dns", "proxy-providers", "proxy-groups", "rule-providers", "rules"} @@ -4281,10 +4295,14 @@ def flush() -> None: # ── provider:type 是 mihomo 专属;header 在 Stash 中为 headers ── if top in ("proxy-providers", "rule-providers"): - if m_sub and m_sub.group(3).strip() == "type": + key = m_sub.group(3).strip() if m_sub else "" + if key == "type": buf.clear() continue - if m_sub and m_sub.group(3).strip() == "header": + note = _STASH_PROVIDER_NOTES.get((indent, key)) if top == "proxy-providers" else None + if note and not any(l.lstrip().startswith("#") for l in buf): + buf.append(f"{' ' * indent}# {note}") + if key == "header": flush() out.append(line.replace("header:", "headers:", 1)) changes.append("proxy-providers: header → headers") diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index 381245ee5..bb4537723 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,4 +1,4 @@ -# Date: 2026-08-28 21:07:50 +# Date: 2026-08-28 21:10:09 name: MyStash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译,叠加 myscript.overlay.json),请勿手动修改;如需调整请修改 Surge/Profile.conf。 @@ -134,19 +134,30 @@ dns: #!replace # 服务器订阅配置 proxy-providers: #!replace Server: + # 本地缓存路径,启动时优先读取,拉取失败也能沿用上次结果 path: ./Provider/Proxy/Server.yaml + # 订阅地址 url: https://sub.hotkids.me + # 订阅更新间隔(秒) interval: 3600 + # 拉取订阅固定直连:此时策略组尚未就绪,经代理会形成循环依赖 proxy: DIRECT + # 拉取订阅时携带的请求头:部分机场按 UA 返回不同格式,声明 Clash / mihomo 以取得正确内容 headers: User-Agent: - "Clash/v1.18.0" - "mihomo/1.18.3" + # 节点可用性检查 health-check: + # 是否启用 enable: true + # 测试地址:返回 204 空响应,体积最小 url: 'http://cp.cloudflare.com/generate_204' + # 检查间隔(秒) interval: 600 + # 单次超时(毫秒) timeout: 5000 + # 期望的 HTTP 状态码,不符则判定为不可用 expected-status: 204 proxy-groups: diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 6a5f23d91..83a866284 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,4 +1,4 @@ -# Date: 2026-08-28 21:07:50 +# Date: 2026-08-28 21:10:09 name: Stash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。 @@ -134,19 +134,30 @@ dns: #!replace # 服务器订阅配置 proxy-providers: #!replace Server: + # 本地缓存路径,启动时优先读取,拉取失败也能沿用上次结果 path: ./Provider/Proxy/Server.yaml + # 订阅地址 url: https://sub.hotkids.me + # 订阅更新间隔(秒) interval: 3600 + # 拉取订阅固定直连:此时策略组尚未就绪,经代理会形成循环依赖 proxy: DIRECT + # 拉取订阅时携带的请求头:部分机场按 UA 返回不同格式,声明 Clash / mihomo 以取得正确内容 headers: User-Agent: - "Clash/v1.18.0" - "mihomo/1.18.3" + # 节点可用性检查 health-check: + # 是否启用 enable: true + # 测试地址:返回 204 空响应,体积最小 url: 'http://cp.cloudflare.com/generate_204' + # 检查间隔(秒) interval: 600 + # 单次超时(毫秒) timeout: 5000 + # 期望的 HTTP 状态码,不符则判定为不可用 expected-status: 204 proxy-groups: #!replace From 02285b82ebee77de18e4a382a4469227ad003008 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:16:57 +0000 Subject: [PATCH 24/28] fix(sync): comment out the proxy-providers block in the Stash overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subscription provider is this repo's own and does not apply to someone else's config, so the block ships commented out — content preserved and already converted to Stash's spelling, so uncommenting is all it takes to enable. The #!replace marker is dropped while commented since the block is inactive. Also reverts the per-field comments added to that block a moment ago; they were a misreading of the request. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 34 ++++++++++++------------- Clash/Script/MyStash.stoverride | 45 +++++++++++++-------------------- Clash/Script/Stash.stoverride | 45 +++++++++++++-------------------- 3 files changed, 51 insertions(+), 73 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 68728017f..544a762c4 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4129,20 +4129,6 @@ def _sync_singbox(config: dict, group_lines: list[str], rule_lines: list[str]) - } # 2) dns 块内 Stash 支持的子键(其余为 mihomo 专属,予以省略) -_STASH_PROVIDER_NOTES = { - (4, "path"): "本地缓存路径,启动时优先读取,拉取失败也能沿用上次结果", - (4, "url"): "订阅地址", - (4, "interval"): "订阅更新间隔(秒)", - (4, "proxy"): "拉取订阅固定直连:此时策略组尚未就绪,经代理会形成循环依赖", - (4, "header"): "拉取订阅时携带的请求头:部分机场按 UA 返回不同格式,声明 Clash / mihomo 以取得正确内容", - (4, "health-check"): "节点可用性检查", - (6, "enable"): "是否启用", - (6, "url"): "测试地址:返回 204 空响应,体积最小", - (6, "interval"): "检查间隔(秒)", - (6, "timeout"): "单次超时(毫秒)", - (6, "expected-status"): "期望的 HTTP 状态码,不符则判定为不可用", -} - _STASH_REPLACE_TOP = {"hosts", "dns", "proxy-providers", "proxy-groups", "rule-providers", "rules"} @@ -4169,6 +4155,21 @@ def _stash_clean_nameserver(server: str) -> str: return server if frag.startswith("h3=") else base +def _stash_comment_out(lines: list[str], top_key: str) -> list[str]: + """将某个顶层块整体注释掉(内容保留,需要时取消注释即可启用)。""" + start = next((i for i, l in enumerate(lines) if re.match(rf"^{re.escape(top_key)}:", l)), -1) + if start < 0: + return lines + end = next((i for i in range(start + 1, len(lines)) + if lines[i] and not lines[i].startswith((" ", "#"))), len(lines)) + out = list(lines) + for i in range(start, end): + if out[i].strip(): + # 该块整体停用,无需保留 #!replace 标记 + out[i] = "# " + out[i].replace(" #!replace", "") + return out + + def _sync_stash(config: dict) -> None: """Clash/Sample.yaml → Clash/Stash.stoverride(只改 Stash 与 mihomo 的差异点)。""" out_path = config.get("Stash", {}).get("output") @@ -4299,9 +4300,6 @@ def flush() -> None: if key == "type": buf.clear() continue - note = _STASH_PROVIDER_NOTES.get((indent, key)) if top == "proxy-providers" else None - if note and not any(l.lstrip().startswith("#") for l in buf): - buf.append(f"{' ' * indent}# {note}") if key == "header": flush() out.append(line.replace("header:", "headers:", 1)) @@ -4327,6 +4325,8 @@ def flush() -> None: "author: '@HotKids'", ] + out = _stash_comment_out(out, "proxy-providers") + body = "\n".join(out).rstrip() + "\n" changed = _write_stamped_if_changed(REPO_ROOT / out_path, body) for note in dict.fromkeys(changes): diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index bb4537723..bb004ec2a 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,4 +1,4 @@ -# Date: 2026-08-28 21:10:09 +# Date: 2026-08-28 21:16:36 name: MyStash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译,叠加 myscript.overlay.json),请勿手动修改;如需调整请修改 Surge/Profile.conf。 @@ -132,33 +132,22 @@ dns: #!replace # 服务器订阅配置 -proxy-providers: #!replace - Server: - # 本地缓存路径,启动时优先读取,拉取失败也能沿用上次结果 - path: ./Provider/Proxy/Server.yaml - # 订阅地址 - url: https://sub.hotkids.me - # 订阅更新间隔(秒) - interval: 3600 - # 拉取订阅固定直连:此时策略组尚未就绪,经代理会形成循环依赖 - proxy: DIRECT - # 拉取订阅时携带的请求头:部分机场按 UA 返回不同格式,声明 Clash / mihomo 以取得正确内容 - headers: - User-Agent: - - "Clash/v1.18.0" - - "mihomo/1.18.3" - # 节点可用性检查 - health-check: - # 是否启用 - enable: true - # 测试地址:返回 204 空响应,体积最小 - url: 'http://cp.cloudflare.com/generate_204' - # 检查间隔(秒) - interval: 600 - # 单次超时(毫秒) - timeout: 5000 - # 期望的 HTTP 状态码,不符则判定为不可用 - expected-status: 204 +# proxy-providers: +# Server: +# path: ./Provider/Proxy/Server.yaml +# url: https://sub.hotkids.me +# interval: 3600 +# proxy: DIRECT +# headers: +# User-Agent: +# - "Clash/v1.18.0" +# - "mihomo/1.18.3" +# health-check: +# enable: true +# url: 'http://cp.cloudflare.com/generate_204' +# interval: 600 +# timeout: 5000 +# expected-status: 204 proxy-groups: # Global diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 83a866284..80c985531 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,4 +1,4 @@ -# Date: 2026-08-28 21:10:09 +# Date: 2026-08-28 21:16:36 name: Stash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。 @@ -132,33 +132,22 @@ dns: #!replace # 服务器订阅配置 -proxy-providers: #!replace - Server: - # 本地缓存路径,启动时优先读取,拉取失败也能沿用上次结果 - path: ./Provider/Proxy/Server.yaml - # 订阅地址 - url: https://sub.hotkids.me - # 订阅更新间隔(秒) - interval: 3600 - # 拉取订阅固定直连:此时策略组尚未就绪,经代理会形成循环依赖 - proxy: DIRECT - # 拉取订阅时携带的请求头:部分机场按 UA 返回不同格式,声明 Clash / mihomo 以取得正确内容 - headers: - User-Agent: - - "Clash/v1.18.0" - - "mihomo/1.18.3" - # 节点可用性检查 - health-check: - # 是否启用 - enable: true - # 测试地址:返回 204 空响应,体积最小 - url: 'http://cp.cloudflare.com/generate_204' - # 检查间隔(秒) - interval: 600 - # 单次超时(毫秒) - timeout: 5000 - # 期望的 HTTP 状态码,不符则判定为不可用 - expected-status: 204 +# proxy-providers: +# Server: +# path: ./Provider/Proxy/Server.yaml +# url: https://sub.hotkids.me +# interval: 3600 +# proxy: DIRECT +# headers: +# User-Agent: +# - "Clash/v1.18.0" +# - "mihomo/1.18.3" +# health-check: +# enable: true +# url: 'http://cp.cloudflare.com/generate_204' +# interval: 600 +# timeout: 5000 +# expected-status: 204 proxy-groups: #!replace # Global From 07dd9d686f61deaa622b0b764674c0b4f697f349 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:18:24 +0000 Subject: [PATCH 25/28] fix(sync): use include-all instead of the disabled provider in Stash groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With proxy-providers commented out, the seven groups carrying use: [Server] referenced a provider that no longer exists. Stash treats a group containing no proxies as DIRECT, so those groups — the five region groups plus Speedtest and Server — would have silently gone direct, and since 🔰 Proxy is built from them the whole proxy path would have collapsed without any error. They now declare include-all: true, which is how Stash pulls in every proxy from the config, and the region filters keep working unchanged. The pool groups the overlay adds are emitted the same way. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 20 ++++++++++++--- Clash/Script/MyStash.stoverride | 44 +++++++++++---------------------- Clash/Script/Stash.stoverride | 23 ++++++----------- 3 files changed, 40 insertions(+), 47 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 544a762c4..db10f55f1 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4190,6 +4190,7 @@ def _sync_stash(config: dict) -> None: dns_keep = True # dns 块内当前子键是否保留 policy_split: list[str] | None = None # 逗号拼接键待展开的域名 policy_val: list[str] = [] # 该键的值行 + skip_use_items = False # use: 的列表项(已换成 include-all) changes: list[str] = [] def flush() -> None: @@ -4263,6 +4264,19 @@ def flush() -> None: indent = len(line) - len(line.lstrip()) m_sub = _SUB_KEY_RE.match(line) + # use 引用的是本仓库自己的 provider,而它在 Stash 产物里已停用; + # 改用 include-all 从基础配置的 proxies 取节点(filter 仍照常生效)。 + if skip_use_items: + if indent >= 6 and not m_sub: + continue + skip_use_items = False + if top == "proxy-groups" and indent == 4 and m_sub and m_sub.group(3).strip() == "use": + flush() + out.append(" include-all: true") + skip_use_items = True + changes.append("proxy-groups: use → include-all") + continue + # ── dns:按 Stash 支持的子键过滤 ── if top == "dns" and indent == 2 and m_sub: key = m_sub.group(3).strip() @@ -4382,8 +4396,8 @@ def _stash_render_group(g: dict) -> list[str]: out.append(f" icon: {g['icon']}") if g.get("hidden"): out.append(" hidden: true") - # 池组的节点来源:与基座地区组写法一致,按 filter 从本仓库 provider 中筛选 - out += [" use:", " - Server"] + # 池组的节点来源:与基座地区组写法一致,从基础配置的 proxies 中按 filter 筛选 + out.append(" include-all: true") for key in ("interval", "tolerance", "lazy"): if key in g: out.append(f" {key}: {g[key]}") @@ -4394,7 +4408,7 @@ def _stash_render_group(g: dict) -> list[str]: # 组内字段的规范顺序(与 Sample.yaml 一致),group_overrides 新增字段时按此定位 -_STASH_FIELD_ORDER = ["name", "type", "icon", "hidden", "use", "proxies", +_STASH_FIELD_ORDER = ["name", "type", "icon", "hidden", "include-all", "use", "proxies", "interval", "tolerance", "lazy", "filter"] diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index bb004ec2a..13ee199b7 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,4 +1,4 @@ -# Date: 2026-08-28 21:16:36 +# Date: 2026-08-28 21:18:09 name: MyStash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译,叠加 myscript.overlay.json),请勿手动修改;如需调整请修改 Surge/Profile.conf。 @@ -271,8 +271,7 @@ proxy-groups: - name: "⏱️ Speedtest" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Speed.png - use: - - Server + include-all: true # Adblock - name: "🚧 AdGuard" @@ -312,8 +311,7 @@ proxy-groups: type: url-test icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png hidden: true - use: - - Server + include-all: true tolerance: 50 filter: '(?i)^(?=.*(?:GoMaMi|Neburst|Pro))' @@ -321,79 +319,69 @@ proxy-groups: type: fallback icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:HK|HKG)\d*\b)(?=.*(?:GoMaMi|Pro))' - name: "🇨🇳 TW Relay" type: fallback icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:TW|TWN)\d*\b)(?=.*Neburst)' - name: "🇯🇵 JP Relay" type: fallback icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:JP|JPN)\d*\b)(?=.*Pro)' - name: "🇺🇸 US Relay" type: fallback icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Loop.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:US|USA)\d*\b)(?=.*(?:GoMaMi|Pro))' - name: "🇺🇳 Server" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Club.png - use: - - Server + include-all: true # Area - name: "🇭🇰 Hong Kong" type: fallback icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/HK.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:HK|HKG)\d*\b)(?!.*GoMaMi)(?!.*Pro)' - name: "🇨🇳 Taiwan" type: fallback icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/TW.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:TW|TWN)\d*\b)(?!.*Neburst)(?!.*Pro)' - name: "🇸🇬 Singapore" type: fallback icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/SG.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:SG|SGP)\d*\b)(?!.*Neburst)(?!.*Pro)' - name: "🇯🇵 Japan" type: fallback icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/JP.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:JP|JPN)\d*\b)(?!.*Pro)' - name: "🇺🇸 America" type: fallback icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/US.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:US|USA)\d*\b)(?!.*GoMaMi)(?!.*Pro)' @@ -403,16 +391,14 @@ proxy-groups: type: fallback icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/UK.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:UK|GBR)\d*\b)' - name: "🇩🇪 Germany" type: fallback icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Flags/DE.png hidden: true - use: - - Server + include-all: true filter: '(?i)^(?=.*\b(?:DE|DEU)\d*\b)' rule-providers: #!replace diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 80c985531..971e875c0 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,4 +1,4 @@ -# Date: 2026-08-28 21:16:36 +# Date: 2026-08-28 21:17:50 name: Stash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。 @@ -275,8 +275,7 @@ proxy-groups: #!replace - name: "⏱️ Speedtest" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Speed.png - use: - - Server + include-all: true # Adblock - name: "🚧 AdGuard" @@ -315,43 +314,37 @@ proxy-groups: #!replace - name: "🇺🇳 Server" type: select icon: https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Color/Club.png - use: - - Server + include-all: true # Area - name: "🇭🇰 Hong Kong" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Hong_Kong.png - use: - - Server + include-all: true filter: '(?i)(?:🇭🇰|香港|Hong Kong|\b(?:HK|HKG)\d*\b)' - name: "🇨🇳 Taiwan" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Taiwan.png - use: - - Server + include-all: true filter: '(?i)(?:🇨🇳|🇹🇼|台湾|Taiwan|\b(?:TW|TWN)\d*\b)' - name: "🇸🇬 Singapore" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Singapore.png - use: - - Server + include-all: true filter: '(?i)(?:🇸🇬|新加坡|Singapore|\b(?:SG|SGP)\d*\b)' - name: "🇯🇵 Japan" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Japan.png - use: - - Server + include-all: true filter: '(?i)(?:🇯🇵|日本|Japan|\b(?:JP|JPN)\d*\b)' - name: "🇺🇸 America" type: select icon: https://fastly.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/United_States.png - use: - - Server + include-all: true filter: '(?i)(?:🇺🇸|美国|United States|\b(?:US|USA)\d*\b)' From 28674ca7a895d2e13314127ebaa2239920f8046a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:19:00 +0000 Subject: [PATCH 26/28] fix(sync): mark MyStash's proxy-groups #!replace as well MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both overrides now replace every block they declare, so applying either to a nodes-only config leaves exactly the base's proxies and takes everything else — hosts, dns, proxy-groups, rule-providers, rules — from the override. proxies is never emitted, so the base's node list is untouched, and proxy-providers ships commented out and stays out of the merge. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 6 ------ Clash/Script/MyStash.stoverride | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index db10f55f1..0da06997f 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4423,12 +4423,6 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st lines = list(lines) notes: list[str] = [] - # 定制版的 proxy-groups 保持默认的前置插入,不整体替换 - for i, l in enumerate(lines): - if l.startswith("proxy-groups:"): - lines[i] = "proxy-groups:" - break - # 展示字段替换为本定制版专属内容,避免与基座在覆写列表中同名 for i, l in enumerate(lines): if l.startswith("name: "): diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index 13ee199b7..e5a701faf 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,4 +1,4 @@ -# Date: 2026-08-28 21:18:09 +# Date: 2026-08-28 21:18:40 name: MyStash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译,叠加 myscript.overlay.json),请勿手动修改;如需调整请修改 Surge/Profile.conf。 @@ -149,7 +149,7 @@ dns: #!replace # timeout: 5000 # expected-status: 204 -proxy-groups: +proxy-groups: #!replace # Global # name: "GLOBAL" # type: select From 6281410fb83fac699296803fcce12bae88e11b66 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:23:31 +0000 Subject: [PATCH 27/28] feat(sync): add the icon field to the Stash overrides Points at Quantumult/X/Images/Want.png in this repo, served over jsDelivr; verified the file is present at that path. The override docs list icon among the display fields, so it sits with name/desc/author. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/sync-config.py | 1 + Clash/Script/MyStash.stoverride | 3 ++- Clash/Script/Stash.stoverride | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index 0da06997f..c6bd7cc1b 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -4337,6 +4337,7 @@ def flush() -> None: f"name: {Path(out_path).stem} for Android", "desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。", "author: '@HotKids'", + 'icon: "https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Want.png"', ] out = _stash_comment_out(out, "proxy-providers") diff --git a/Clash/Script/MyStash.stoverride b/Clash/Script/MyStash.stoverride index e5a701faf..f6cceaed6 100644 --- a/Clash/Script/MyStash.stoverride +++ b/Clash/Script/MyStash.stoverride @@ -1,8 +1,9 @@ -# Date: 2026-08-28 21:18:40 +# Date: 2026-08-28 21:23:19 name: MyStash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译,叠加 myscript.overlay.json),请勿手动修改;如需调整请修改 Surge/Profile.conf。 author: '@HotKids' +icon: "https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Want.png" # 通用设置 diff --git a/Clash/Script/Stash.stoverride b/Clash/Script/Stash.stoverride index 971e875c0..854719533 100644 --- a/Clash/Script/Stash.stoverride +++ b/Clash/Script/Stash.stoverride @@ -1,8 +1,9 @@ -# Date: 2026-08-28 21:17:50 +# Date: 2026-08-28 21:23:19 name: Stash for Android desc: 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。 author: '@HotKids' +icon: "https://fastly.jsdelivr.net/gh/HotKids/Rules@master/Quantumult/X/Images/Want.png" # 通用设置 From f0c1c756ead38aeceb4bf4cb364dc17c0277995a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 13:38:51 +0000 Subject: [PATCH 28/28] docs: formalize Chinese wording across the repo, refresh stale Stash notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wording pass over the hand-maintained sources; generated artefacts follow from their generators, and Mihomo.yaml's header is synced by hand since the full Clash sync cannot run offline. - coined/ungrammatical: 吃掉→去除, 打戳→写入时间戳 - colloquial verbs: 删掉→移除 (×3), 丢掉→丢弃, 剪掉→移除, 拿到→取得/一并纳入, 漏掉→遗漏, 顺手→并 - filler: 「也没关系,反正…仍然对」→「不影响判断:…仍然成立」, 方便→便于 - unified the overwrite warning as 直接修改…将在下次同步时被覆盖 (7 places across the root, scripts, sing-box READMEs and the generator) Domain terms stay as they are — 兜底, 看门狗, 机场 read naturally here and match the rest of the repo's voice. Also corrects three statements in the scripts README that the last few Stash changes left stale: the provider block is commented out rather than field-fixed, groups use include-all rather than use: [Server], and the #!replace markers were not documented at all. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012LPYq4FJhPgzP1eZnexyyT --- .github/scripts/README.md | 20 ++++++++++++-------- .github/scripts/sync-config.py | 22 +++++++++++----------- Clash/Mihomo.yaml | 2 +- README.md | 2 +- Surge/Module/Scripts/friday-checkin.js | 2 +- Surge/Module/Scripts/ip-security.js | 2 +- Surge/Module/Scripts/komari-traffic.js | 2 +- sing-box/README.md | 4 ++-- 8 files changed, 30 insertions(+), 26 deletions(-) diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 2e51fa3d6..ced8cc400 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -81,10 +81,14 @@ domain 语义转换:QX 展开为 `DOMAIN` / `DOMAIN-SUFFIX` 行、Clash 出 do 开关 → 补 `follow-rule: true`,并在转译时去除该后缀(Stash 的 `#` 片段只承载 `h3=true` 这类选项);`nameserver-policy` 里逗号拼接的多域名单键是 mihomo 专属, 按 Stash 语法拆分为独立键(官方仅支持精确域名 / 通配域名 / `geosite:`)。 -- **Provider 字段**:移除 mihomo 专属的 `type`;proxy-providers 的 `header` 改为 Stash 文档 - 拼写的 `headers`。 - -其余内容——`hosts` / `mode` / `log-level`、23 个策略组(含 `use: [Server]` 与地区 `filter`)、 +- **Provider 处理**:`proxy-providers` 整块注释停用——它是本仓库自己的订阅,不适用于他人的 + 配置;内容已按 Stash 口径转换(移除 mihomo 专属的 `type`、`header` 改为文档拼写的 `headers`), + 取消注释即可启用。相应地,策略组的 `use: [Server]` 改为 `include-all: true`,直接从基础配置 + 的 `proxies` 中按 `filter` 取节点——否则组内无代理,会被 Stash 当作 `DIRECT` 处理。 +- **整体替换标记**:`hosts` / `dns` / `proxy-groups` / `rule-providers` / `rules` 均加 `#!replace`, + 使覆写以本文件为准;`proxies` 不输出,因此基础配置的节点原样保留。 + +其余内容——`hosts` / `mode` / `log-level`、23 个策略组(含地区 `filter`)、 30 个规则集、36 条规则——全部原样保留:Stash 的规则类型是 Clash Premium 超集,我们用到的 `RULE-SET` / `GEOIP` / `GEOSITE` / `MATCH` / `no-resolve`、`AND` / `OR` / `NOT` 逻辑规则(含嵌套) 及内置策略 `REJECT` / `REJECT-DROP`,官方文档均明确支持。 @@ -104,12 +108,12 @@ Sample.yaml 的注释与排版),因此这些指令在 `_stash_apply_overlay` YAML 覆写没有「默认关但可开」这种状态。因此按声明**整组移除**:移除该组、以其为落点的规则,以及 其余分组候选中对它的引用,并清理因此不再被任何 `RULE-SET` 引用的规则集。`extra_pool_groups` 的新增池组在 Script.js 里靠运行时过滤 `config.proxies` 填充,静态 YAML 必须显式写节点来源, -统一按基座地区组的写法输出 `use: [Server]` + `filter`。 +统一按基座地区组的写法输出 `include-all: true` + `filter`。 `Clash/Script/Script.js` 是 `Clash/Mihomo.yaml` 生成完毕后再解析出来的等效 mihomo 覆写 脚本(Enhance Script),供 Clash Verge Rev / FlClash / Bettbox 等客户端直接对任意订阅动态生成同一套策略组 / 规则 / 基础设置,无需依赖本仓库自身的 proxy-providers。它只读 Mihomo.yaml 的解析结果、 -不重新实现转换逻辑,因此随 `Profile.conf` 改动自动同步,直接改动会被下次同步覆盖。地区组 / `🇺🇳 Server` +不重新实现转换逻辑,因此随 `Profile.conf` 改动自动同步,直接修改将在下次同步时被覆盖。地区组 / `🇺🇳 Server` 组不用 mihomo 的 `include-all` / `include-all-proxies`(它对候选节点做隐式字母序排序, 无开关可关,见 `_gen_clash_script_js` 注释):订阅里的内联节点由运行时按 `poolGroupFilters` 手动过滤 `config.proxies` 填入并保持订阅原始顺序;订阅只给 @@ -126,8 +130,8 @@ YAML 覆写没有「默认关但可开」这种状态。因此按声明**整组 规则落点重定向、`remove_groups` 整组删除、`group_overrides` 类型/filter 覆盖、`group_proxies_insert` 候选节点插入、 `extra_pool_groups` 额外分组、`move_after` 调整展示顺序、`disabled_by_default` 让部分 分组默认关闭),因此公共部分(rules/rule-providers/基础设置、以及未被 overlay 覆盖的 -分组)随 `Profile.conf` 自动同步,私人差异集中改对应的 `*.overlay.json` 即可——直接改 -生成产物本体会被下次同步覆盖。overlay 还可以用 `extends: "<其他 overlay 文件名>"` 声明基于另一份 +分组)随 `Profile.conf` 自动同步,私人差异集中修改对应的 `*.overlay.json` 即可——直接修改 +生成产物本体将在下次同步时被覆盖。overlay 还可以用 `extends: "<其他 overlay 文件名>"` 声明基于另一份 已生成的 overlay 结果继续叠加(链式:`clashbox.overlay.json` extends `myscriptcolor.overlay.json` extends `myscript.overlay.json`,图标继承自 MyScriptColor), 只需要写与被继承者的差异,公共部分(地区 fallback、Relay 中转链等)不必重复声明。 diff --git a/.github/scripts/sync-config.py b/.github/scripts/sync-config.py index c6bd7cc1b..c7e6f6e18 100644 --- a/.github/scripts/sync-config.py +++ b/.github/scripts/sync-config.py @@ -159,8 +159,8 @@ def _gen_clash_action_wrapper_groups( # --------------------------------------------------------------------------- _CST = timezone(timedelta(hours=8)) -# 冒号后的空格可选:loon/qx 头部经 _process_builtin_* 的 rstrip 会吃掉 -# 空占位 `# Date: ` 的尾空格变成 `# Date:`,仍需能匹配并打戳。 +# 冒号后的空格可选:loon/qx 头部经 _process_builtin_* 的 rstrip 会去除 +# 空占位 `# Date: ` 的尾空格变成 `# Date:`,仍需能匹配并写入时间戳。 _DATE_LINE_RE = re.compile(r"^# Date:.*$", re.MULTILINE) @@ -2800,8 +2800,8 @@ def _ordered_group(g: dict) -> dict: def _rule_policy_index(parts: list[str]) -> int: """Surge/Clash 规则行里策略字段的下标。`MATCH,POLICY` 策略在 index 1; - `AND/OR/NOT,(...),POLICY` 策略永远是最后一个逗号分段(拆括号里的逗号 - 也没关系,反正策略本身不含逗号,取 -1 仍然对);其余类型固定是 + `AND/OR/NOT,(...),POLICY` 策略始终为最后一个逗号分段(括号内的逗号 + 不影响判断:策略本身不含逗号,取 -1 仍然成立);其余类型固定是 `TYPE,VALUE,POLICY[,no-resolve]` 形式,策略在 index 2。 """ if parts[0] == "MATCH": @@ -2825,10 +2825,10 @@ def _apply_overlay( - rule_policy_redirect:把 rules 里以某分组为策略目标的行改指另一分组 ({旧落点: 新落点},用改名前的基座名字)。先于 remove_groups 执行, - 因此「删掉某组但保留其规则」可以两者搭配(如 📛 REJECT-DROP 组删掉、 + 因此「移除某组但保留其规则」可以两者搭配(如 📛 REJECT-DROP 组移除、 其规则落点改指 ⛔️ REJECT)。 - - remove_groups:整组删掉(如 📛 REJECT-DROP),同时从其余分组的 proxies 候选 - 里剔除对它的引用、删掉 rules 里以它为策略目标的行。 + - remove_groups:整组移除(如 📛 REJECT-DROP),同时从其余分组的 proxies 候选 + 里剔除对它的引用、移除 rules 中以其为策略目标的行。 - rename_map:批量改名({旧名: 新名}),同步更新其余分组 proxies 候选里的旧名 引用、pool_filters 的 key、以及 rules 里以该分组为策略目标的行,避免残留 指向旧名字的悬空引用。多个 overlay 之间要做同一批改名时用这个,而不是在 @@ -3220,7 +3220,7 @@ def _gen_clash_script_js( # 结构性池组(Server + 地区,均来自 Sample.yaml 的 use:[Server],或链式继承自 # base_state)——这些没有直接对应的 RULE-SET 目标,不纳入可选开关。overlay 的 # extra_pool_groups 新增的同样是结构性的(Relay 链 / 新地区)。但 group_overrides - # 给既有分组(如 📧 Mail)追加 filter 只是让它"顺带拿到全部节点",不改变它本来是 + # 给既有分组(如 📧 Mail)追加 filter 只是使其一并纳入全部节点,不改变它本来是 # 个可开关的功能分组这件事,因此不计入本集合。 if overlay: _apply_overlay(groups, pool_filters, rules, structural_pool_names, overlay, overlay_label) @@ -3833,7 +3833,7 @@ def _sync_surfboard( SB_SKIP_GROUP_KW = ("Gateway", "Apple TV") # 各地区示例节点(占位用途:sing-box 无订阅机制,先内置一份可直接连通的示例 -# Shadowsocks 节点,方便直接改 server/password 试用;真实使用请用订阅工具替换) +# Shadowsocks 节点,便于直接修改 server/password 试用;真实使用请用订阅工具替换) # 按地区组 policy-regex-filter 命中的关键词匹配,与本仓库 Profile.conf 的固定 5 个地区一一对应 SB_EXAMPLE_NODES = { "HK": ("🇭🇰 HK", "hk.hotkids.me"), @@ -4419,7 +4419,7 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st if unknown: raise ValueError( f"{label}: Stash 转译尚未实现这些 overlay 指令 {sorted(unknown)};" - f"请在 _stash_apply_overlay 中补齐,避免私人差异被静默丢掉" + f"请在 _stash_apply_overlay 中补齐,避免私人差异被静默丢弃" ) lines = list(lines) notes: list[str] = [] @@ -4513,7 +4513,7 @@ def _stash_apply_overlay(lines: list[str], overlay: dict, label: str) -> list[st lines = [l for l in lines if not (l.startswith(" - ") and l.rstrip().endswith(name)) and not (l.strip().startswith("- ") and l.strip().strip("-").strip().strip("'\"") == name)] - notes.append(f"剪掉分组 {name}(含其规则与候选引用)") + notes.append(f"移除分组 {name}(含其规则与候选引用)") # 清理不再被引用的规则集 used = {m.group(1) for l in lines if (m := re.match(r"^ - RULE-SET,([^,]+),", l))} diff --git a/Clash/Mihomo.yaml b/Clash/Mihomo.yaml index bbcba99fc..9c8c6220d 100644 --- a/Clash/Mihomo.yaml +++ b/Clash/Mihomo.yaml @@ -2,7 +2,7 @@ # Date: 2026-08-06 19:38:00 # Author: @HotKids # -# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手改;改内容请改 Surge/Profile.conf。 +# 自动生成(sync-config.py 从 Clash/Sample.yaml 转译),请勿手动修改;如需调整请修改 Surge/Profile.conf。 # ── 通用设置 ── diff --git a/README.md b/README.md index a30364e8f..2d0e04087 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 自用规则、模块和代理工具集合。 `Surge/Profile.conf` 与 `Surge/RULE-SET/` 是唯一手动维护的配置 / 规则来源, -其他平台产物全部由 `.github/scripts/` 下的同步脚本自动生成——**改内容请改源头,直接改生成产物会被下次同步覆盖**。 +其他平台产物全部由 `.github/scripts/` 下的同步脚本自动生成——**如需调整请修改源头;直接修改生成产物将在下次同步时被覆盖**。 | 目录 | 说明 | |---|---| diff --git a/Surge/Module/Scripts/friday-checkin.js b/Surge/Module/Scripts/friday-checkin.js index a806824e7..636f76e94 100644 --- a/Surge/Module/Scripts/friday-checkin.js +++ b/Surge/Module/Scripts/friday-checkin.js @@ -4,7 +4,7 @@ * 背景:签到 Bearer(accessToken) 仅 3 小时;fet-token 只在 token/getv2(刷新时)出现, * 光开 App 常撞不到。故改混合策略: * - 开 App 时(member/get 必触发)抓当前 accessToken → 当场签到(保证开 App 即签) - * - 顺手在 token/getv2 出现时存 fet-token + 设备字段 → 给 cron 做自动续期 + * - 并在 token/getv2 出现时存 fet-token + 设备字段 → 给 cron 做自动续期 * cron:有 fet-token 就刷新 accessToken 自动签;否则回退用最近存的 accessToken(3h 内有效)。 * * 维护:每天开下 App 必签到;若抓到过 fet-token,则数天内不开 App 也能自动签。 diff --git a/Surge/Module/Scripts/ip-security.js b/Surge/Module/Scripts/ip-security.js index 880e72daf..7554dbdfb 100644 --- a/Surge/Module/Scripts/ip-security.js +++ b/Surge/Module/Scripts/ip-security.js @@ -831,7 +831,7 @@ function sendNetworkChangeNotification({ localZh, policy, localIP, outIP, entran } // 先并行发起 geo/risk/流量 API 请求,确保 ip.sb/ipinfo/ip-api 请求完成后再查策略 - // DNS 泄露检测需要走代理策略,必须等拿到 policy 后再执行 + // DNS 泄露检测需要走代理策略,必须在取得 policy 后执行 const [riskInfo, ipTypeResult, localSbRaw, localBaiduRaw, outGeoRaw, outOrgRaw, trafficResult] = await Promise.all([ getRiskScore(outIP), // 0: 风险评分 getIPType(outIP), // 1: IP 类型 diff --git a/Surge/Module/Scripts/komari-traffic.js b/Surge/Module/Scripts/komari-traffic.js index 296bc4dae..ded7cf91a 100644 --- a/Surge/Module/Scripts/komari-traffic.js +++ b/Surge/Module/Scripts/komari-traffic.js @@ -354,7 +354,7 @@ if (!base) { changes.push(online ? `🟢 ${n.name} 恢复上线` : `🔴 ${n.name} 离线`); } - // 重启检测:uptime 明显回落(补离线推送因刷新间隔漏掉的快速重启) + // 重启检测:uptime 明显回落(补离线推送因刷新间隔遗漏的快速重启) const up = rec && rec.uptime > 0 ? rec.uptime : 0; if (up) next.uptime[n.uuid] = up; const prevUp = prev.uptime ? prev.uptime[n.uuid] : 0; diff --git a/sing-box/README.md b/sing-box/README.md index da6309e1e..e6d14ae80 100644 --- a/sing-box/README.md +++ b/sing-box/README.md @@ -2,7 +2,7 @@ ## 完整配置 `config.json` -对齐 sing-box **1.12+ 新格式**(typed DNS server、route rule action、`default_domain_resolver`、remote binary rule-set)的完整客户端配置。由 `sync-config.py` 从 `Surge/Profile.conf` 自动生成(静态基座见 `.github/scripts/sync-config/sing-box.ini`),策略组 / 路由与其他平台一致,直接改动会被下次同步覆盖。 +对齐 sing-box **1.12+ 新格式**(typed DNS server、route rule action、`default_domain_resolver`、remote binary rule-set)的完整客户端配置。由 `sync-config.py` 从 `Surge/Profile.conf` 自动生成(静态基座见 `.github/scripts/sync-config/sing-box.ini`),策略组 / 路由与其他平台一致,直接修改将在下次同步时被覆盖。 > ⚠️ 由脚本自动生成,仅做过 `sing-box check` 结构校验(CI `lint.yml`),**未实际跑过 sing-box 内核联网测试**,正式使用前请自行验证可用性。 @@ -19,7 +19,7 @@ | `source/*.json` | 源码 | `source` | 人类可读、可 diff 审查 | | `rule-set/*.srs` | 二进制 | `binary` | 由 CI 用官方 `sing-box rule-set compile` 编译,体积更小、加载更快 | -两者内容等价,均声明 `version: 2`(需 sing-box ≥ 1.10)。`.srs` 由 `source/` 下同名 `.json` 自动编译,直接改动会被 CI 重新编译覆盖;规则内容改动提交到 `Surge/RULE-SET/`(经 `sync-rules.py` 同步)。 +两者内容等价,均声明 `version: 2`(需 sing-box ≥ 1.10)。`.srs` 由 `source/` 下同名 `.json` 自动编译,直接修改将被 CI 重新编译覆盖;规则内容改动提交到 `Surge/RULE-SET/`(经 `sync-rules.py` 同步)。 ## 引用示例