From e5a3c7077358519a8743aac106c479c8cd9f3fea Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Tue, 25 Aug 2026 13:52:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20ShadowRocket=20=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E7=BB=84=E6=94=B9=E7=94=A8=20policy-regex-filter=20=E6=94=B6?= =?UTF-8?q?=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ShadowRocket 没有 Surge 的 use= 订阅引用语法,首版 conf 里 「订阅名 + use=true」在真机上解析失败,All Nodes 组只显示一个 无效的 Proxy Group 残留成员。改为原生的 policy-regex-filter 从 App 内全部节点筛选(收纳全部节点 = .*),订阅命名约束随之 取消,删除 SHADOWROCKET_SUB_NAME 常量并同步前端提示与文档。 Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- README.md | 2 +- server/src/routes/sub.ts | 5 +--- .../services/generators/shadowrocket-conf.ts | 24 +++++++------------ shared/src/protocols.ts | 6 ----- web/src/pages/SubscriptionsPage.vue | 8 ++----- 6 files changed, 14 insertions(+), 33 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8f79239..9213861 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,7 @@ IR 层承担三件防护,改动时不要绕过: ### ShadowRocket 分两步导入 -ShadowRocket 的原生订阅格式只承载节点,因此拆成两个端点:`/shadowrocket`(节点 base64 URI 列表)与 `/shadowrocket-conf`(策略组与规则的 .conf)。**conf 里的策略组用 `use=true` 引用订阅名**,所以用户在 App 内必须把节点订阅命名为 `SHADOWROCKET_SUB_NAME`(= `Proxy Station`)。ShadowRocket 不支持 `DOMAIN-SET`/`AND`/`URL-REGEX`,生成时跳过并回报 `skippedRules`。**该 conf 尚未经过真机验证。** +ShadowRocket 的原生订阅格式只承载节点,因此拆成两个端点:`/shadowrocket`(节点 base64 URI 列表)与 `/shadowrocket-conf`(策略组与规则的 .conf)。**ShadowRocket 没有 Surge 的 `use=` 订阅引用语法**(真机验证过:`use=true` 会被当成解析不了的成员残留),策略组收节点只能靠 `policy-regex-filter` 从 App 内全部节点里筛(「收纳全部节点」输出 `policy-regex-filter=.*`),因此订阅名随意,但用户在 App 里挂的其他订阅的节点也会被筛进来。ShadowRocket 不支持 `DOMAIN-SET`/`AND`/`URL-REGEX`,生成时跳过并回报 `skippedRules`。 ### 鉴权与订阅 diff --git a/README.md b/README.md index 07bdc6d..9f93fdc 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ | **ClashMeta** (Android) | ✅ | ✅ | ✅ | 一个 URL,`RULE-SET` 自动映射为 rule-providers | | **ShadowRocket** (iOS) | ✅ | ✅ | ✅ | **两个 URL**:节点订阅 + 规则配置,见下 | -ShadowRocket 的原生订阅格式只承载节点,因此拆成两步:先在「订阅」里添加节点链接并**命名为 `Proxy Station`**,再在「配置」里添加规则链接。配置文件按这个名字引用节点,名字必须一致。订阅页的 ⓘ 图标里有同样的说明。 +ShadowRocket 的原生订阅格式只承载节点,因此拆成两步:先在「订阅」里添加节点链接(名称随意),再在「配置」里添加规则链接。策略组通过 `policy-regex-filter` 从 App 内全部节点里筛选。订阅页的 ⓘ 图标里有同样的说明。 > ClashMeta 侧无 Clash 版本的规则集,由本服务实时拉取 Surge 版并转换后提供,无需外部转换服务。 diff --git a/server/src/routes/sub.ts b/server/src/routes/sub.ts index f87f939..ecc3ce9 100644 --- a/server/src/routes/sub.ts +++ b/server/src/routes/sub.ts @@ -9,7 +9,6 @@ import { convertSurgeList } from '../services/rulesets/convert.js'; import { generateSurge } from '../services/generators/surge.js'; import { generateShadowrocket } from '../services/generators/shadowrocket.js'; import { generateShadowrocketConf } from '../services/generators/shadowrocket-conf.js'; -import { SHADOWROCKET_SUB_NAME } from '@proxy-station/shared'; import { generateClash } from '../services/generators/clash.js'; function constantTimeEqual(a: string, b: string): boolean { @@ -65,9 +64,7 @@ export const subRoute = new Hono() const row = findToken(c.req.param('token')); if (!row) return c.text('not found', 404); touchToken(row.id, row.accessCount); - const { content } = generateShadowrocketConf(irFor(row), { - subscriptionName: SHADOWROCKET_SUB_NAME, - }); + const { content } = generateShadowrocketConf(irFor(row)); c.header('content-type', 'text/plain; charset=utf-8'); c.header('content-disposition', 'attachment; filename="Proxy Station Rules.conf"; filename*=UTF-8\'\'Proxy%20Station%20Rules.conf'); return c.body(content); diff --git a/server/src/services/generators/shadowrocket-conf.ts b/server/src/services/generators/shadowrocket-conf.ts index c423f2f..b7492ae 100644 --- a/server/src/services/generators/shadowrocket-conf.ts +++ b/server/src/services/generators/shadowrocket-conf.ts @@ -50,18 +50,15 @@ export interface ShadowrocketConfOutput { /** * 生成 ShadowRocket 配置文件:策略组 + 规则,不含节点。 - * 节点由「节点订阅」单独导入,策略组通过 `use=true` 引用该订阅的名称, - * 因此用户在 App 里给订阅起的名字必须与 subscriptionName 一致。 + * 节点由「节点订阅」单独导入。ShadowRocket 没有 Surge 的 `use=` 订阅引用语法, + * 策略组收节点只能靠 `policy-regex-filter` 从 App 内全部节点里筛选, + * 因此订阅在 App 里叫什么名字都可以。 */ -export function generateShadowrocketConf( - ir: ProfileIR, - opts: { subscriptionName: string } -): ShadowrocketConfOutput { - const sub = quoteName(opts.subscriptionName); +export function generateShadowrocketConf(ir: ProfileIR): ShadowrocketConfOutput { const lines: string[] = []; lines.push('# 由 Proxy Station 生成的 ShadowRocket 配置(策略组 + 规则)'); - lines.push(`# 节点请另行导入「节点订阅」,并在 App 中将其命名为:${opts.subscriptionName}`); + lines.push('# 节点请另行导入「节点订阅」'); lines.push(''); lines.push('[General]'); lines.push(shadowrocketGeneral(ir.sections['General'] ?? '')); @@ -69,13 +66,7 @@ export function generateShadowrocketConf( lines.push('', '[Proxy Group]'); for (const g of ir.groups) { const parts: string[] = [g.type]; - if (g.filterRegex) { - // 地区组:引用订阅 + 正则筛选(ShadowRocket 原生支持 policy-regex-filter) - parts.push(sub, 'use=true', `policy-regex-filter=${g.filterRegex}`); - } else if (g.includeAllNodes) { - // 收纳全部节点:引用订阅 - parts.push(sub, 'use=true'); - } else { + if (!g.filterRegex && !g.includeAllNodes) { const members = g.members.map((m) => quoteName(m.name)); parts.push(...(members.length ? members : ['DIRECT'])); } @@ -84,6 +75,9 @@ export function generateShadowrocketConf( if (g.interval) parts.push(`interval=${g.interval}`); if (g.tolerance) parts.push(`tolerance=${g.tolerance}`); } + // policy-regex-filter 从 App 内全部节点里筛选;「收纳全部节点」用全匹配正则 + if (g.filterRegex) parts.push(`policy-regex-filter=${g.filterRegex}`); + else if (g.includeAllNodes) parts.push('policy-regex-filter=.*'); lines.push(`${g.name} = ${parts.join(', ')}`); } diff --git a/shared/src/protocols.ts b/shared/src/protocols.ts index 0dbaf6f..2efffad 100644 --- a/shared/src/protocols.ts +++ b/shared/src/protocols.ts @@ -10,12 +10,6 @@ export const CLIENT_LABELS: Record = { clash: 'ClashMeta (Android)', }; -/** - * ShadowRocket 分两步导入:节点走订阅、策略组与规则走配置文件。 - * 配置文件里的策略组用 `use=true` 引用订阅,因此 App 内的订阅名必须与此一致。 - */ -export const SHADOWROCKET_SUB_NAME = 'Proxy Station'; - export type Egress = 'direct' | 'warp' | 'unknown'; /** Surge 与 Clash 使用 IETF 命名,Xray 分享链接使用简名 */ diff --git a/web/src/pages/SubscriptionsPage.vue b/web/src/pages/SubscriptionsPage.vue index 711d22e..19c6a52 100644 --- a/web/src/pages/SubscriptionsPage.vue +++ b/web/src/pages/SubscriptionsPage.vue @@ -1,7 +1,7 @@