diff --git a/AGENTS.md b/AGENTS.md index 1746cdd..eb16c85 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,18 @@ 本仓库统一维护边缘 VPS 节点的部署管理与 Proxy Station 代理节点、分流规则管理平台。Web 界面管理多协议节点(VMess/Trojan/Shadowsocks/Hysteria2)与策略组、规则,向 macOS Surge、iOS ShadowRocket、Android ClashMeta 下发托管订阅。规则模板来自 [ClashConnectRules/Surge](https://github.com/ClashConnectRules/Surge),边缘舰队的工具与私有交付记录已从原 `../edge-nodes/` 迁入本项目。 +### 当前生产约定(2026-10-04) + +最新盘点、完整组顺序与成员、节点启用范围和 Session 变更索引见 [当前生产状态](docs/CURRENT-STATE.md)。日期化体检/变更文档保留当时的数量与验证结果;维护前经管理 API 重读,不能用历史快照或初始化模板覆盖最新生产数据。 + +- 当前登记 30 个节点,12 个启用、18 个停用;12 个启用组、21 个显式成员、130 条规则(128 启用)。停用节点保留凭据,不因文档整理、排序、部署或导入清单核对而自动启用。应用的节点启用状态不等于边缘 VPS 的运行状态。 +- 普通代理流量交给 `🚀 Proxy`,只有它引用 `⚡️ Automatic`。Automatic 为 url-test,成员依次为新加坡、美国、日本;规则不直接指向 Automatic。`🤖 AI` 为 select,成员依次为美国、新加坡、日本、`🧠 AI Nodes`,不引用 Automatic。 +- `📋 All Nodes` 与 `🧠 AI Nodes` 都是独立 select 组,开启 includeAllNodes、没有筛选正则或显式成员;AI Nodes 紧接 AI 显示。两组收纳相同的订阅范围内可用节点,各自保存客户端选择,不能用引用 All Nodes 代替独立的 AI Nodes。 +- 两组按完整节点名称字符串升序输出,保留 Direct/Warp 出口词。现有节点 sortOrder 全为默认 0,IR 按 sortOrder、name 读取;新增/导入节点保持默认值。非零权重会先于名称排序,调整它影响节点声明及其他动态列表,不只是这两个组。Web 节点页仍按站点、协议与出口展示。 +- Streaming 当前承接 Disney、Netflix、TikTok、YouTube、GoogleSearch 及欧洲/韩国/香港/台湾流媒体集合;美国/日本流媒体集合直接交给对应地区组,Emby 交给 Proxy。组显示顺序不决定规则匹配顺序。 +- 规则优先级保持:STUN 与 googlevideo UDP 防护 → Unbreak DIRECT → 启用的 AI 规则 → 业务例外/广告与其余分流 → 唯一 FINAL。FINAL 仍为 `🎯 NoAuto,dns-failed`,NoAuto → Mainland → DIRECT。保留 Google 更新/翻译直连例外;全局 PROTOCOL DOH 与 Dia 关键词规则继续停用。 +- Apple/Global Services/Speedtest/Other 及无节点的香港/台湾/韩国/英国组已删除,不因模板仍存在而恢复。App Store DIRECT、Apple News 美国、Apple Relay AI 例外继续保留。 + ### 节点部署管理 - `edge-nodes/README.md` 是节点工作区入口;`edge-nodes/build-delivery-doc.py` 从 Git 忽略的 `edge-nodes/private/fleet.json` 读取清单并离线生成汇总交付页,不在源码里保存真实站点信息。 @@ -47,8 +59,11 @@ bash .claude/skills/deploy/scripts/deploy.sh all # 本机离线生成边缘舰队交付页(清单与产物均在 Git 忽略的私有目录) python3 edge-nodes/build-delivery-doc.py -# 校验生成的 Clash 配置(最有价值的一条端到端验证) -curl -s "http://localhost:3000/sub//clash" > /tmp/c.yaml && mihomo -t -f /tmp/c.yaml +# 校验实际 Clash 配置;生产验收改用实际 HTTPS 入口,真实 URL 仅在本机使用 +umask 077 +check_dir="$(mktemp -d /private/tmp/proxy-station-check.XXXXXX)" +curl -fsSL --max-time 45 "http://localhost:3000/sub//clash" -o "$check_dir/clash.yaml" && \ + test -s "$check_dir/clash.yaml" && mihomo -t -d "$check_dir" -f "$check_dir/clash.yaml" ``` 本机开发及当前生产部署使用 Node 22。依赖使用 pnpm 与已提交的 `pnpm-lock.yaml`;不要混用 npm/yarn 或无故更新锁文件。pnpm 在无 TTY 时安装需要 `CI=true`,原生模块(better-sqlite3、esbuild、vue-demi)的构建许可写在 `pnpm-workspace.yaml` 的 `allowBuilds`。 @@ -96,7 +111,7 @@ IR 层承担三件防护,改动时不要绕过: `includeAllNodes` 与 `filterRegex` 可以共存,生成器里 **filterRegex 分支优先**: -- 有 `filterRegex` → 动态筛选:Surge 保留显式节点/内置成员,全部成员组作为 `include-other-group` 来源,并可同时启用 `include-all-proxies=true`;正则只筛动态来源,不筛显式成员。Clash 用 `include-all-proxies: true` + `filter`;ShadowRocket 用 `policy-regex-filter`。新增同地区节点会自动进组 +- 有 `filterRegex` → 动态筛选:Surge 保留显式节点/内置成员,全部成员组作为 `include-other-group` 来源,并可同时启用 `include-all-proxies=true`;正则只筛动态来源,不筛显式成员。Clash 用 `include-all-proxies: true` + `filter` 并保留 `proxies` 显式成员;ShadowRocket 保留显式成员并用 `policy-regex-filter` 动态收节点。新增同地区节点会自动进组 - 仅 `includeAllNodes` → Surge 静态展开全部节点名,Clash/ShadowRocket 仍用动态引用 **地区组靠节点名称正则匹配,不看 `region` 字段**。节点命名规范与新增地区的完整步骤见 `docs/ADDING-REGIONS.md`——命名不对就进不了地区组。 @@ -111,7 +126,9 @@ Surge 不支持组级 `url=`;组的 `testUrl` 仅用于 Clash/ShadowRocket ### ShadowRocket 分两步导入 -ShadowRocket 的原生订阅格式只承载节点,因此拆成两个端点:`/shadowrocket`(节点 base64 URI 列表)与 `/shadowrocket-conf`(策略组与规则的 .conf)。**ShadowRocket 没有 Surge 的 `use=` 订阅引用语法**(真机验证过:`use=true` 会被当成解析不了的成员残留),策略组收节点只能靠 `policy-regex-filter` 从 App 内全部节点里筛(「收纳全部节点」输出 `policy-regex-filter=.*`),因此订阅名随意,但用户在 App 里挂的其他订阅的节点也会被筛进来。ShadowRocket 不支持 `DOMAIN-SET`/`AND`/`OR`/`NOT`/`URL-REGEX`/`PROTOCOL`,生成时跳过并回报 `skippedRules`。**ShadowRocket 的 conf 语法没有引号机制**:策略名(含空格/emoji)一律裸写,加引号会被当作名字的一部分,导致规则指向不存在的策略组而整条失效、流量落到默认行为走代理——Surge 生成器可以加引号,ShadowRocket 生成器绝不可以。 +ShadowRocket 的原生订阅格式只承载节点,因此拆成两个端点:`/shadowrocket`(节点 base64 URI 列表)与 `/shadowrocket-conf`(策略组与规则的 .conf)。节点订阅按完整名称字符串升序排列,保留出口词;该序列化器对副本排序,不修改 IR/数据库,跨客户端名称排序由上述节点权重约定保证。**ShadowRocket 没有 Surge 的 `use=` 订阅引用语法**(真机验证过:`use=true` 会被当成解析不了的成员残留),策略组动态收节点靠 `policy-regex-filter` 从 App 内全部节点里筛(「收纳全部节点」输出 `policy-regex-filter=.*`),显式成员也要保留。因此订阅名随意,但用户在 App 里挂的其他订阅的节点也会被筛进来;这些节点及 App 自定义排序不由本服务控制。本生成器当前保守跳过 `DOMAIN-SET`/`AND`/`OR`/`NOT`/`URL-REGEX`/`PROTOCOL`,并回报 `skippedRules`;这不是客户端不支持这些类型的断言。官方更新记录已确认存在逻辑规则与 QUIC 匹配,具体配置语法仍须以 App 内帮助和真机编译验证,详见 `docs/MOBILE-CLIENT-AUDIT-2026-10-04.md`。**ShadowRocket 的 conf 策略名不可用 Surge 引号机制**:策略名(含空格/emoji)一律裸写,加引号会被当作名字的一部分,导致规则指向不存在的策略组而整条失效、流量落到默认行为走代理——Surge 生成器可以加引号,ShadowRocket 生成器绝不可以。 + +Clash 节点必须明确下发 UDP 能力:VMess/Trojan/纯 TCP SS/HY2 输出 `udp: true`,SS WS 插件仅转发 TCP,输出 false,不能假设 nginx 的同端口提供 SS UDP。VMess cipher 按 mihomo 命名映射、未知 cipher 明确排除;SS plugin 的 TLS 参数在 `plugin-opts` 内。RULE-SET 保留 no-resolve;逻辑规则逐个转换子类型,仅把 PROTOCOL TCP/UDP 映射 NETWORK,任一子条件无法表达时整条跳过。两端 url-test 保留零 tolerance,fallback 不输出 tolerance。完整体检需分别核对客户端能力与生成器范围,原生解析成功不等于设备连接或隐私验收。 ### 鉴权与订阅 @@ -138,7 +155,9 @@ schema 变更同时更新 `shared/src/schemas.ts`/DTO(涉及对外字段时 ## 初始化与已有数据 -`server/src/db/seed.ts` 只在策略组为空时从 `server/assets/surge-template.conf` 初始化策略组、规则和 Surge 段落。修改模板不会更新已有数据库;既有订阅仍以数据库为准。生产数据变更使用 `update-db` 技能,经管理 API 修改并先做 SQLite 在线备份,避免绕过校验和级联更新。组成员与排序 API 有全量替换语义,修改前先读取完整当前值。 +`server/src/db/seed.ts` 只在策略组为空时从 `server/assets/surge-template.conf` 初始化策略组、规则和 Surge 段落。修改模板不会更新已有数据库;既有订阅仍以数据库为准。生产数据变更使用 `update-db` 技能,经管理 API 修改并先做 SQLite 在线备份,确认备份 root 属主、600 权限,避免绕过校验和级联更新。 + +组的 members 是整体替换;排序请求始终先读完整列表并提交全量、无重复 ids。规则 reorder API 强制校验完整性与末尾 FINAL;当前组 reorder API 只逐条写顺序,调用方须自行核对完整性。组 PUT 不更新 sortOrder,显示排序使用独立 `/api/groups/reorder`。节点 PUT 使用私有文件提交完整原字段,排序调整后重读确认除 sortOrder/updatedAt 外没有变化。 `server/src/env.ts` 直接读 `process.env`,没有 dotenv 加载;仅复制 `.env.example` 成 `.env` 并不会让 `pnpm dev` 自动读取它。变量须由启动环境提供(Docker Compose 与 systemd 有各自注入方式)。相对 `DATA_DIR` 始终基于 `server/` 解析。 @@ -148,6 +167,8 @@ schema 变更同时更新 `shared/src/schemas.ts`/DTO(涉及对外字段时 手写 Host 项优先;生成的域名 DNS 映射按连接规则顺序保留 AI 之前的非 AI 域名/规则集例外,不把 Host 当成能表达进程、协议或 IP 条件的路由系统。宽泛 AI 域名规则仍须审阅覆盖范围。 +Net.Coffee 的 Claude 页通过随机 `*.d.ip.net.coffee` 探测递归 DNS 出口,不直接查询 Claude 域名;使用 `DOMAIN-SUFFIX,ip.net.coffee,🤖 AI` 同时覆盖页面和探测子域。Surge 派生根域/通配 Host 到 Cloudflare DoH,并随 AI 出口。本机 Chrome+Surge 已复测为美国 Cloudflare「未检测到泄露」,仅代表该探测路径的实际结果,不能推广为所有客户端/域名/远端解析器已验收,见 `docs/NET-COFFEE-DNS-2026-10-04.md`。 + 规则变更先读 `docs/SURGE-CHANGES.md`。API 必须保住一条启用且末尾的 FINAL;禁止停用、删除、改类型或创建重复 FINAL,排序必须提交完整且无重复的 ids。IR 对遗留坏数据防御性补齐 FINAL,其失效策略降级 DIRECT。重置规则在事务内保留自定义规则,不重复插入原主键,也不能用 reset 代替定向调整生产规则。 规则按首条匹配执行(另留意 pre-matching)。业务例外须在覆盖它的广告/通用规则集之前,特定 UDP 拦截须在对应服务域名规则之前。靠前的 IP 规则集是否加 `no-resolve` 必须按意图决定:它能避免主动解析,但会改变未解析域名的 IP 拦截覆盖。普通规则值与策略引用用 `surge-format.ts` 编码,逻辑表达式保留结构;不要手拼含逗号的正则或含换行的字段。 @@ -167,6 +188,10 @@ schema 变更同时更新 `shared/src/schemas.ts`/DTO(涉及对外字段时 涉及节点、组、规则、DNS 或 Surge 段落时,在上述检查之外对实际订阅运行 `scripts/check-surge.py`;涉及规则集或顺序用 `--fetch-rulesets`,审阅域名遮蔽提示。脚本调用官方 Surge CLI,不更换活动配置;输出 OK 不表示测速、业务连接或设备隐私已验收。敏感配置用权限 600 的临时文件,不进入仓库或对话。生产写入先在线备份,再 API 更新、重读、HTTPS 拉订阅与 CLI 验证;必要时同时用 mihomo 校验受影响的 Clash 产物。 +取订阅后先核对下载成功、非空及预期结构,再交给原生解析器。mihomo 在目标配置不存在时可能创建默认配置并报告成功,不能用这个结果验收订阅;所有检查遇到失败即停止,避免前一步失败而最后一条命令成功掩盖错误。 + +区分数据库、实际 HTTPS 产物、客户端活动配置与设备网络结果。用户授权的同机复测需刷新既有托管配置时,先核对托管 URL、备份并读取最新组选择,用 `surge-cli managed-profile update` 后检查 effective 配置与实际选项。组改名会改变客户端保存选择的身份;必要时仅将原选定节点及父组路径迁移到新名称,保留 All Nodes/AI Nodes 的独立选择,不擅自切换出口或活动配置。客户端模块可覆盖生成的组类型与参数,不能把本机模块效果写回生产数据库。 + `db/index.ts` 在模块加载时就执行 `export const db = createDb()`,包括只导入 `createDb` 的情形;临时数据库测试/启动验证应在加载模块前设置绝对路径的临时 `DATA_DIR`,不要拿本地真实 `server/data/` 做试验。现有生成器单测使用内存中的 IR;运行服务还会执行初始化。 改生成器时检查节点过滤、组成员引用、规则顺序和 FINAL 兜底;Clash 可用 mihomo 校验,Surge/ShadowRocket 的客户端语法与实际连接仍需真机验收。`excluded`/`skippedRules` 是生成器返回字段,订阅 HTTP 响应只返回配置正文。 diff --git a/README.md b/README.md index c073d8f..300cbaa 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ **多协议节点管理**——VMess、Trojan、Shadowsocks、Hysteria2,支持粘贴分享链接批量导入(`vmess://`、`trojan://`、`ss://` 的 SIP002 与 Xray 两种写法、`hysteria2://`),也支持手动填写与导出。节点按站点分段展示。 -**分流规则与策略组**——首次启动自动从 [ClashConnectRules/Surge](https://github.com/ClashConnectRules/Surge) 模板导入(26 个策略组 + 88 条规则,含广告拦截、AI 服务、流媒体、国内直连等)。规则可排序、改策略、启停、增删;策略组可启停并调整顺序,顺序即客户端里的排列顺序。外部规则集可在界面上实时预览内容。 +**分流规则与策略组**——空库首次启动从内置 Surge 模板初始化广告拦截、AI 服务、流媒体、国内直连等配置。模板参考 [ClashConnectRules/Surge](https://github.com/ClashConnectRules/Surge),已有数据库以管理 API 的实际数据为准,修改模板不会同步更新。规则可排序、改策略、启停、增删;唯一启用的 FINAL 保持末尾,不可停用或删除。策略组可启停、调整成员与显示顺序;显示顺序不决定规则优先级。外部规则集可在界面上预览内容。 **按客户端自动适配**——同一份数据生成三端配置,客户端不支持的协议在下发时自动屏蔽(例如 Surge 的 ss 类型没有 WebSocket 传输,SS over WS 节点会被排除),字段差异自动映射(cipher 命名、VMess AEAD、Hysteria2 混淆参数等)。 @@ -33,14 +33,26 @@ ShadowRocket 的原生订阅格式只承载节点,因此拆成两步:先在 > ClashMeta 侧无 Clash 版本的规则集,由本服务实时拉取 Surge 版并转换后提供,无需外部转换服务。 +三端的规则与 DNS 表达范围有所不同。Shadowrocket 部分规则仍保守跳过;Surge 的 AI Host DNS 映射不自动等同于手机端 DNS 设置。能力、已验证范围与待验项见 [移动端体检](docs/MOBILE-CLIENT-AUDIT-2026-10-04.md)。 + +## 当前配置入口 + +最新节点启用范围、完整组成员、规则服务归属与验证范围集中在 [当前生产状态](docs/CURRENT-STATE.md)。目前普通代理由 **🚀 Proxy** 控制,只有它引用 **⚡️ Automatic**;**🤖 AI** 可选美国、新加坡、日本或 **🧠 AI Nodes**。 + +**📋 All Nodes** 与 **🧠 AI Nodes** 收纳相同的可用节点,按完整名称升序排列,分别保存手动选择。AI Nodes 紧接 AI 显示;在 AI 中选 AI Nodes,再在 AI Nodes 中选择具体节点,即可为 AI 保存独立出口。详见 [独立节点选择](docs/AI-NODES-2026-10-04.md)。 + +AI 规则位于广告与普通分流之前,保留更早的协议防护与 Unbreak 直连例外。Net.Coffee 的随机 DNS 探测子域已纳入 AI;本机 Chrome+Surge 复测显示美国 Cloudflare「未检测到泄露」,验证边界见 [DNS 复测](docs/NET-COFFEE-DNS-2026-10-04.md)。 + ## 快速开始 +使用 Node 22 与 pnpm,按已提交的锁文件安装: + ```bash -pnpm install +CI=true pnpm install --frozen-lockfile pnpm dev # server :3000 + web :5173 ``` -打开 http://localhost:3000 (或开发时的 :5173),然后: +开发时打开 http://localhost:5173,然后: 1. **节点** → 批量导入,粘贴分享链接。命名建议带地区词(如 `🇺🇸 US LA VMess Direct`),地区策略组靠节点名正则匹配 2. **订阅** → 新建订阅,勾选要开放的节点 @@ -50,6 +62,10 @@ pnpm dev # server :3000 + web :5173 ## 部署 +当前 LA 生产服务使用 Node 22+systemd,由 nginx 提供 HTTPS;日常应用发布使用 [deploy 技能](.claude/skills/deploy/SKILL.md),线上节点/组/规则数据通过 [update-db 技能](.claude/skills/update-db/SKILL.md)管理。部署代码不自动覆盖已有数据。详细步骤见 [生产部署](docs/DEPLOY.md)。 + +另提供 Docker Compose 部署方式: + ```bash ADMIN_TOKEN=<随机长字符串> docker compose up -d ``` @@ -61,19 +77,26 @@ SQLite 落在 `./data` 卷。订阅 URL 由请求来源自动推断,无需配 ## 校验 ```bash -# Clash 产物(最有价值的一条端到端验证) -curl -s "http://localhost:3000/sub//clash" > /tmp/c.yaml && mihomo -t -f /tmp/c.yaml - pnpm -r typecheck pnpm test + +# 下载成功后再校验真实产物;生产验收使用实际 HTTPS 订阅 +umask 077 +check_dir="$(mktemp -d /private/tmp/proxy-station-check.XXXXXX)" +curl -fsSL --max-time 45 "http://localhost:3000/sub//clash" -o "$check_dir/clash.yaml" && \ + test -s "$check_dir/clash.yaml" && mihomo -t -d "$check_dir" -f "$check_dir/clash.yaml" + +# 在安装 Surge 的 macOS 上检查实际配置与远程规则快照 +python3 scripts/check-surge.py /private/tmp/.conf --fetch-rulesets ``` -Surge 与 ShadowRocket 需真机导入验收。 +真实配置、URL 与凭据仅保存在私有文件。原生解析成功不等于节点连接或隐私验收;本机 Surge 的组与网站 DNS 复测已有记录,iOS/Android 业务、UDP 与完整 DNS/WebRTC 测试仍需真机完成。完整流程见 [订阅变更与验证](docs/SURGE-CHANGES.md)。 ## 文档 | 文件 | 内容 | |---|---| +| [docs/CURRENT-STATE.md](docs/CURRENT-STATE.md) | 最新生产节点、策略组、规则、客户端下发数量与 Session 接续入口 | | `edge-nodes/README.md` | 节点部署工作区、当前舰队、私有资料与工程迁移记录 | | `.claude/skills/edge-node-multi-protocol/SKILL.md` | 节点部署、重建、维护与交付的共用技能 | | `docs/SEEDING.md` | 从本仓库私有交付文档导入真实节点、命名建议 | @@ -86,6 +109,12 @@ Surge 与 ShadowRocket 需真机导入验收。 | `docs/SURGE-AUDIT-2026-10-03.md` | macOS Surge 生产订阅体检、复现结果与修复建议 | | `docs/SURGE-FIX-2026-10-03.md` | 体检问题修复、生产备份与实际订阅验证结果 | | `docs/SURGE-CHANGES.md` | 节点、组、规则与 DNS 变更的 Surge 官方 CLI 验证流程 | +| [docs/AI-NODES-2026-10-04.md](docs/AI-NODES-2026-10-04.md) | 独立 AI Nodes、emoji、名称排序与客户端选择保留 | +| [docs/PROXY-AUTOMATIC-2026-10-04.md](docs/PROXY-AUTOMATIC-2026-10-04.md) | 普通代理入口与 Automatic 的选择链 | +| [docs/AI-RULE-REVIEW-2026-10-04.md](docs/AI-RULE-REVIEW-2026-10-04.md) | AI 精确补充及 Apple/Global/Bing 专用分流调整 | +| [docs/AI-PRIORITY-2026-10-04.md](docs/AI-PRIORITY-2026-10-04.md) | AI、协议防护、直连例外与广告规则优先级 | +| [docs/NET-COFFEE-DNS-2026-10-04.md](docs/NET-COFFEE-DNS-2026-10-04.md) | 检测域名实现、AI 后缀规则及本机 DNS 复测 | +| [docs/MOBILE-CLIENT-AUDIT-2026-10-04.md](docs/MOBILE-CLIENT-AUDIT-2026-10-04.md) | Shadowrocket/ClashMeta 生成器差异、已部署修复与手机待验项 | ## 安全 diff --git a/docs/ADDING-REGIONS.md b/docs/ADDING-REGIONS.md index 13bfa77..3ecb8d5 100644 --- a/docs/ADDING-REGIONS.md +++ b/docs/ADDING-REGIONS.md @@ -4,12 +4,16 @@ > 背景:策略组的地区筛选靠**节点名称正则**,不靠节点的 `region` 字段。所以节点命名是一切的前提。 -## 当前状态(2026-09-14) +## 当前状态(2026-10-04) + +节点登记/启用数量、完整策略组矩阵和服务规则归属集中见 [当前生产状态](CURRENT-STATE.md)。下面维护地区命名与扩展方法;新增站点不自动启用已有停用线路。 有美国、新加坡、日本三个地区的节点,策略组里对应 `🇺🇸 United States`、`🇸🇬 Singapore`、`🇯🇵 Japan` 三个地区组。 -日本组于 2026-09-14 随 jp 站点(VMISS 东京,八条线路)按本文步骤重建,站点码 `JP`,已加入所有成员里含美国或新加坡的服务组(Proxy/Automatic/Streaming/Apple/AI/Bybit/Global Services)。约定:**凡是成员含美国或新加坡的组,都应同时含日本**;Speedtest/NoAuto 只含大陆与 Automatic,不在此列。 +日本组于 2026-09-14 随 jp 站点(VMISS 东京,八条线路)按本文步骤重建,站点码 `JP`,当前加入的服务组为 Proxy/Automatic/Streaming/AI/Bybit。约定:**凡是成员含美国或新加坡的组,都应同时含日本**;NoAuto 当前只含大陆,不在此列。Speedtest 组及其专用规则已删除。 +2026-10-04 已删除线上 Apple/Global Services 组及指向它们的专用规则,并删除 AI 的 Bing 规则;不再给这两个已删除的服务组添加地区。同日进一步将所有原先直接指向 Automatic 的 20 条规则改为 Proxy,包括 `global`/`global_plus`、CDN 及 Facebook 等;x.com/bing.com 当前自然匹配 `global` → Proxy,由用户在 Proxy 中选择出口。Automatic 改为 `url-test`,仅 Proxy 保留其成员引用;AI/Speedtest/NoAuto 已移除 Automatic。具体约束与验证见 [Proxy 与 Automatic 选择链](PROXY-AUTOMATIC-2026-10-04.md)。App Store DIRECT、Apple News 美国及 Apple Relay AI 例外继续保留。原始初始化模板仍有旧组和旧引用;已有生产数据以管理 API 为准,不通过规则 reset 恢复旧分流。 香港、台湾、英国、韩国的地区组**仍处于删除状态**——留着会是空组,在客户端里显示为无可用节点。 兜底的 `🌍 Other` 组也已删除:所有节点都能被现有三个地区组收纳,它恒为空。 +当前组显示顺序为 Proxy、Automatic、All Nodes、美国、新加坡、日本、AI、🧠 AI Nodes、Streaming、Bybit、Mainland、NoAuto。🧠 AI Nodes 为独立收纳全部节点的 select 组,紧接 AI;其选择与 All Nodes 独立保存,见 [AI Nodes](AI-NODES-2026-10-04.md)。两组的节点按完整名称字符串升序输出;当前节点 sortOrder 统一为默认 0,新增节点保持默认值即可沿用名称排序。组显示顺序不决定分流优先级;实际规则保留协议防护与 Unbreak 例外在前,启用的 AI 规则集中置于广告及普通分流之前,见 [AI 与广告优先级](AI-PRIORITY-2026-10-04.md)。 ## 一、给新节点命名(决定它能被哪个组收纳) @@ -57,7 +61,7 @@ const STATION_LABELS: Record = { | 间隔 | `300` | | 容差 | `50` | -「收纳全部节点」+「筛选正则」的组合会生成 `include-all-proxies=true, policy-regex-filter=…`(Surge)、`include-all-proxies: true, filter: …`(Clash)、`use=true, policy-regex-filter=…`(ShadowRocket),三端都是**动态筛选**——之后再加同地区节点会自动进组,不用改配置。 +「收纳全部节点」+「筛选正则」的组合会生成 `include-all-proxies=true, policy-regex-filter=…`(Surge)、`include-all-proxies: true, filter: …`(Clash)、`policy-regex-filter=…`(ShadowRocket),三端都是**动态筛选**——之后再加同地区节点会自动进组,不用改配置。ShadowRocket 不使用 `use=true`,其正则从 App 内全部节点筛选。 各地区的正则(取自原模板,可直接复制): @@ -73,9 +77,9 @@ const STATION_LABELS: Record = { ## 四、兜底组 `🌍 Other`(当前不存在) -原模板有一个 `🌍 Other` 组,用否定前瞻正则收纳「不属于任何已建地区组」的节点。**当前已删除**,因为现有节点都能被美/新两个组收纳,它恒为空。 +原模板有一个 `🌍 Other` 组,用否定前瞻正则收纳「不属于任何已建地区组」的节点。**当前已删除**,因为现有节点都能被美/新/日三个组收纳,它恒为空。 -只有当你加入的节点**不属于任何已建地区组**(比如加了德国节点但没建德国组)时,才需要重建它: +加入尚无地区组的节点时,优先按本文新建对应地区组。若确需恢复 Other,以下仅是历史模板示例,必须先核对目标客户端的正则能力与实际产物: | 字段 | 值 | |---|---| @@ -87,6 +91,8 @@ const STATION_LABELS: Record = { ^((?!港|🇭🇰|香港|HK|Hong|日|🇯🇵|日本|JP|Japan|坡|🇸🇬|新加坡|狮城|SG|Singapore|美|🇺🇸|美国|US|States|American|Remain|Expired|官网|如需|套餐|去除|剩余|距离|Reset|重置|流量).)+$ ``` +否定前瞻不能视为三个客户端都支持的正则;本系统会把 filterRegex 透传给 Clash 的 filter,不自动改写成等价的排除条件。当前简单地区正则的验证结果不覆盖这个历史示例,跨端恢复需先验证,必要时调整数据模型与生成器。 + **如果重建了 Other,每次新增地区组都必须往这个否定前瞻里追加对应关键字**,否则新地区节点会同时出现在自己的组和 Other 里。 ## 五、把新地区加进服务组的可选项 @@ -95,36 +101,33 @@ const STATION_LABELS: Record = { | 组 | 当前成员 | 建议 | |---|---|---| -| `⚡️ Automatic` | SG、US | 加入所有地区组 | -| `🚀 Proxy` | Automatic、SG、US | 加入所有地区组 | -| `🎬 Streaming` | SG、US | 加入解锁能力强的地区(HK/TW/JP) | -| `🤖 AI` | Automatic、US、SG | 加入 JP、US 等 AI 服务可用地区 | -| `🌐 Global Services` | Automatic、US、SG | 同 AI | -| `🍎 Apple` | Mainland、US、JP | 按需加入 HK | +| `⚡️ Automatic`(url-test) | SG、US、JP | 加入所有地区组;仅由 Proxy 引用,不让规则直接指向它 | +| `🚀 Proxy` | Automatic、All Nodes、SG、US、JP | 加入所有地区组 | +| `🎬 Streaming` | SG、US、JP | 加入解锁能力强的地区(HK/TW/JP) | +| `🤖 AI` | US、SG、JP、🧠 AI Nodes | 加入 AI 服务可用地区;不加入 Automatic | +| `🧠 AI Nodes`(select) | 全部节点 | 保持 includeAllNodes,与 All Nodes 独立选择;新增节点自动进入,sortOrder 保持默认 0 | | `🪙 Bybit` | DIRECT、SG、US、JP | 同 Proxy | +| `🎯 NoAuto` | Mainland | 不加入 Automatic | -成员的顺序就是客户端里的排列顺序,**第一个成员是该组的默认选中项**。 +成员的顺序就是客户端里的排列顺序。对于 `select` 组,客户端没有有效的已保存选择时,**第一个成员是默认选中项**;Automatic 为 `url-test`,按测速结果与容差选择成员。 ## 六、地区流媒体解锁规则(可选) -模板原有 `stream_hk`、`stream_jp`、`stream_tw`、`stream_kr`、`stream_eu` 等按地区解锁的规则集。删除地区组时,这些规则被改指向了 `🎬 Streaming`。 +模板原有按地区解锁的规则集。当前 `stream_us` 的域名/IP 两条已指向美国,`stream_jp` 的两条已指向日本;`stream_hk`、`stream_tw`、`stream_kr`、`stream_eu` 的八条规则仍指向 `🎬 Streaming`,相关地区组当前不存在。 如果新建了对应地区组,可以在「规则」页把这些规则的策略改回该地区组,解锁效果更精准: | 规则集 | 建议策略 | |---|---| | `stream_hk`(non_ip + ip 两条) | `🇭🇰 Hong Kong` | -| `stream_jp`(两条) | `🇯🇵 Japan` | +| `stream_jp`(两条) | `🇯🇵 Japan`,当前已应用 | | `stream_tw`、`stream_kr`、`stream_eu` | 对应地区组 | ## 七、验证 1. 「策略组」页:新地区组的**命中**一栏应列出该地区的所有节点 2. 「节点」页:新站点应有独立分段,标题显示国旗 -3. 命令行校验 Clash 产物: - ```bash - curl -s "http:///sub//clash" > /tmp/c.yaml && mihomo -t -f /tmp/c.yaml - ``` +3. 从真实 HTTPS 订阅入口获取各端产物,先确认下载成功及配置结构,再按 [订阅变更与验证](SURGE-CHANGES.md)运行 Surge CLI/规则集快照与独立目录的 `mihomo -t`;结构通过后仍需客户端连接验收。 4. 若重建过 `🌍 Other`,确认它的命中里**不再包含**新地区节点 ## 删除某个地区(反向操作) diff --git a/docs/AI-COLLABORATION.md b/docs/AI-COLLABORATION.md index a54880d..bcdc0e8 100644 --- a/docs/AI-COLLABORATION.md +++ b/docs/AI-COLLABORATION.md @@ -2,6 +2,8 @@ 项目开发约定以根目录 `AGENTS.md` 为唯一来源。原 `CLAUDE.md` 中的架构、协议适配、前端设计、数据与部署约定已迁入,并根据当前代码补充了初始化、环境变量、AI DNS 与验证说明。 +最新生产盘点统一维护在 [CURRENT-STATE.md](CURRENT-STATE.md),较长的 Session 变更证据保存在其链接的专题文档。AGENTS.md 维护持续适用的约束;专题记录保留原阶段数量,不能用旧快照替代新的 API 盘点。 + ## 指令入口 | 工具 | 项目指令 | 项目技能 | @@ -11,7 +13,7 @@ Claude Code v2.1.277 起支持直接读取 `AGENTS.md`,但默认在当前目录或祖先目录已有 `CLAUDE.md`/`CLAUDE.local.md` 时优先读取 Claude 文件。保留 `CLAUDE.md` 的显式导入,使支持导入的旧版本与新版本都能读取同一份规则;无需修改用户全局配置。原生支持不可用的部分旧版本会话也可通过导入加载。 -文件名使用大写、复数的 `AGENTS.md`。不要新增 `agent.md`、`AGENT.md` 或复制一份独立的 Codex 规则。Claude 的 `@path` 是其导入语法,Codex 项目入口本身包含完整规则,无须依赖此语法。 +文件名使用大写、复数的 `AGENTS.md`。不要新增 `agent.md`、`AGENT.md`、`agents.jmd` 或复制一份独立的 Codex 规则。Claude 的 `@path` 是其导入语法,Codex 项目入口本身包含完整规则,无须依赖此语法。 新开会话时从仓库根目录启动工具。Claude Code 可用 `/memory` 或 `/context` 检查项目指令;Codex 可询问当前项目约定,并用 `/skills` 查看发现的技能。若当前会话未发现新技能,重开会话;不需要额外安装项目技能。全局个人指令与机器设置继续由各工具管理,不写入本仓库。 @@ -45,11 +47,14 @@ Git 应保存这些目录链接;在 Windows 上克隆时需要启用符号链 ## 维护与接续 - 架构或业务约束改动更新 `AGENTS.md`;较长的专题说明放入 `docs/` 并按任务引用。 +- 生产任务先经 update-db 只读盘点,区分登记总量、启用数量、token 范围和客户端排除;当前节点、组成员与显示顺序集中更新 CURRENT-STATE.md,阶段性的数量/备份/验收仍保留在原专题文档。 - 运维流程改动更新 `.claude/skills//SKILL.md` 及其脚本;`.agents/skills/` 只维护链接。 - 节点与应用在本仓库统一维护,节点目录与当前舰队见 `edge-nodes/README.md`。通用工具可入库,真实配置/交付产物放 `edge-nodes/private/` 并用受控备份转交;原 `../edge-nodes/` 仅保留迁移前备份。旧工程指令保存在私有归档中,不作为当前约定。 - 普通开发从 `shared` 的类型/校验、`server` 的 API 与 IR、`web` 的状态与页面定位改动。新增客户端先扩展客户端类型与能力矩阵,再增加独立生成器和订阅端点。 - 修改规则模板只影响初始化,已有库的配置更新走管理 API;代码部署与数据更新分别执行。 - 本地验证按改动范围选择类型检查、单测或构建。临时启动与数据库验证在进程启动前设置临时 `DATA_DIR`,避免触碰真实节点数据。 +- 接续时区分本地源码、已部署源码、生产数据库、客户端活动配置/模块及实际网络结果。未提交源码可能已经在用户授权下部署,部署记录不是新一轮上线授权;文档修改也不触发业务写入、订阅刷新或节点启用。 +- All Nodes/AI Nodes 独立选择、零权重名称排序、Proxy 为唯一 Automatic 父组及 AI 规则优先级属于当前业务约定。改组名需跟踪 API 级联引用和客户端保存选择的迁移,不能仅看生成文本通过校验。 - 交接说明记录已完成改动、验证结果与未完成项;可复用的认识写入项目文件,不依赖某个工具的私有会话记忆。 ## 官方依据 diff --git a/docs/AI-NODES-2026-10-04.md b/docs/AI-NODES-2026-10-04.md new file mode 100644 index 0000000..34476ff --- /dev/null +++ b/docs/AI-NODES-2026-10-04.md @@ -0,0 +1,33 @@ +# 🧠 AI Nodes 独立节点选择(2026-10-04) + +本记录包含创建、改名及排序两个阶段;完整最新盘点见 [当前生产状态](CURRENT-STATE.md)。登记节点为 30 个,其中 12 个启用、18 个停用;全节点组仍受启用状态、订阅范围与客户端能力过滤。 + +按用户要求,经生产管理 API 新增名为 `AI Nodes` 的启用 select 组,同日后续添加 emoji 改名为 `🧠 AI Nodes`,显示在 `🤖 AI` 下方。它与 `📋 All Nodes` 一样启用 `includeAllNodes`,没有正则和显式成员,直接收纳订阅范围内、该客户端支持的全部启用节点。 + +`🤖 AI` 的成员顺序现在为美国、新加坡、日本、🧠 AI Nodes;原 All Nodes 引用已替换为该独立组,改名时 API 级联更新引用。All Nodes 继续作为 Proxy 的选项。 + +两个组的节点列表相同,但组名和客户端策略身份独立,分别保存手动选择。用户在 AI 中选择 🧠 AI Nodes 后,再在 🧠 AI Nodes 中选择具体节点,即可让 AI 流量使用独立于 All Nodes 的节点选择。新增或停用节点时,两组由同一订阅生成流程更新,不维护两份静态节点清单。 + +当前组显示顺序:Proxy → Automatic → All Nodes → United States → Singapore → Japan → AI → 🧠 AI Nodes → Streaming → Bybit → Mainland → NoAuto。 + +## 名称排序与 emoji 优化 + +用户进一步要求给 AI Nodes 添加合适的 emoji,并将它与 All Nodes 中的节点按名称字符串排列。`🧠` 表示 AI 专用节点选择,和 `🤖 AI` 主服务策略组区分。 + +已有 IR 按节点 `sortOrder`、完整 `name` 升序读取。原排序权重偏向协议顺序,导致日本、新加坡节点交错;本次经节点管理 API 将 28 个非零权重改为默认 0,现有 30 个节点统一同权重,由完整名称决定顺序。节点名称、凭据、协议与开关不变,更新只影响 sortOrder 和 updatedAt。这也使节点声明及依赖同一节点来源的动态地区列表按名称排列。Web 节点页仍使用其既有站点、协议与出口排序。 + +Surge 的全部节点组继续静态展开支持的节点;Clash 从按名称排列的 proxies 动态收纳;Shadowrocket 的节点订阅原本已经按完整名称排序,conf 继续用 `policy-regex-filter=.*` 收纳 App 内节点。Shadowrocket 中其他订阅或 App 自定义排序的显示次序不由此服务控制。 + +新建或导入节点的 schema 默认 sortOrder 为 0,保持默认即可沿用名称排序;显式设置非零权重会先按权重排列。本次使用既有数据字段和生成流程,未修改或部署生成器代码。优化前另有在线备份:`/opt/proxy-station/data/backups/before-polish-ai-nodes-name-order-20261004-113612.db`,root 属主、600 权限。 + +优化后重读确认实体数量不变,规则与设置完全一致;组只发生名称及引用级联更新,节点只发生权重和更新时间调整。真实 HTTPS Surge 两组各 12 个支持的节点均按完整名称升序排列,Clash 的 proxies 来源与 Shadowrocket 节点订阅同样通过名称排序核对。Surge 官方 CLI、49/49 规则集快照与 `mihomo -t` 再次通过,域名遮蔽提示为 0。刷新本机同一托管配置后,有效配置及两个组的运行时选项都已确认顺序;为改名后的新客户端组身份恢复了之前选择的同一节点,原 All Nodes 选择与 AI 实际路径保持不变。 + +## 创建阶段的备份与验证 + +- 写入前 SQLite 在线备份:`/opt/proxy-station/data/backups/before-add-ai-nodes-selector-20261004-112540.db`,root 属主、600 权限。 +- 重读确认 12 个组、21 个成员、30 个节点、130 条规则;仅新增 AI Nodes、替换 AI 的该项引用及调整显示顺序。其他原组内容保持一致,原节点、规则和设置与变更前相同。 +- 从真实 HTTPS 入口读取 Surge、Clash、Shadowrocket conf,逐端确认 AI Nodes 与 All Nodes 的内容完全一致、AI 只引用 AI Nodes,且新组紧接 AI。Surge 当前两组各有 12 个客户端支持的节点,节点数不是数据库总节点数。 +- Surge 官方 CLI、49/49 远程规则集快照校验及 Clash `mihomo -t` 通过;静态域名遮蔽提示为 0。 +- 本机同一托管配置已确认生效,有效配置与运行时均提供独立 AI Nodes 手动选择组;其运行时节点选项与 All Nodes 完全一致,既有 AI 与 All Nodes 的已保存选择保持不变。没有通过切换节点来做独立性测试。 + +本次只修改生产数据与文档,未部署代码。Shadowrocket 仅检查订阅结构,未做手机真机编译或连接测试;其他设备刷新订阅后取得新组。 diff --git a/docs/AI-PRIORITY-2026-10-04.md b/docs/AI-PRIORITY-2026-10-04.md new file mode 100644 index 0000000..1fd71bc --- /dev/null +++ b/docs/AI-PRIORITY-2026-10-04.md @@ -0,0 +1,46 @@ +# AI、广告与其他规则的优先级(2026-10-04) + +本文保留删除 Speedtest、集中排列 AI 规则时的阶段记录。后续加入 Net.Coffee 规则及独立 `🧠 AI Nodes` 后,现状为 12 组、130 条规则、31 条启用 AI 规则;完整盘点见 [当前生产状态](CURRENT-STATE.md)。 + +用户要求删除 Speedtest 策略组及其规则,调整组显示顺序,并让 AI 优先于广告和普通分流。审阅现有宽泛 googleapis.com AI 规则与协议防护后,用户明确选择保留 STUN 阻断及 Google 更新/翻译直连例外。本次通过管理 API 删除与排序,未修改规则匹配范围、策略、参数或启用状态,未部署代码。 + +## 本阶段策略组显示顺序 + +Proxy → Automatic → All Nodes → United States → Singapore → Japan → AI → Streaming → Bybit → Mainland → NoAuto。 + +前六组相对顺序保持原状。Speedtest 组与其一条 Speedtest RULE-SET 已删除,测速服务继续匹配后续规则。组显示顺序不决定规则优先级;规则从上到下首条匹配生效,pre-matching 另有更高优先级,依据 [Surge 规则说明](https://manual.nssurge.com/rules/overview.html)。此次启用的顶层规则与已下载规则集都没有 pre-matching 参数。 + +## 本阶段实际连接规则顺序 + +1. STUN REJECT,以及 googlevideo.com 的 UDP REJECT-NO-DROP。 +2. Unbreak DIRECT;保留包括 update.googleapis.com、translate.googleapis.com 等 Google 例外。当前规则集与 AI 域名样例的交叉检查没有发现其他 AI 服务域名被 Unbreak 覆盖。 +3. 全部 30 条启用的 AI 规则集中排列,保留原有相对顺序。两条已停用的 AI 规则仍停用,全局 DOH 接管没有启用。 +4. 原有业务例外,如 PayPal 指定美国、Bybit 的 inapps.appsflyer.com、api.amplitude.com 的 Proxy 等。 +5. 广告与隐私规则。四条广告列表的相对顺序保持原状;IP 拦截集原有 no-resolve 参数保留,不新增泛化遥测放行。 +6. 其余服务、地区、国内、CDN 和 global 等规则,彼此相对顺序保持原状。 +7. 唯一启用 FINAL 末尾兜底,仍指向 NoAuto,并保留 dns-failed。 + +这是现有 AI 匹配范围的优先级调整,不将整个共享服务平台扩展为 AI。googleapis.com、api.github.com 及原有共享域名的范围沿用既有配置。外部规则集仍会动态更新,后续业务规则变更需重做覆盖审阅。 + +## DNS 与客户端范围 + +Surge 的 AI DNS 映射仍由连接规则派生;Unbreak 的默认 DNS 映射在 AI 映射之前,AI 域名使用 Cloudflare DoH 并随 AI 出口。手写 Host、节点引导 IP 和 General 均未改动,不改变全局 DNS 或端口 53 的路由。 + +STUN 的 PROTOCOL 规则仍仅在能表达它的客户端下发生效;现有 Clash/Shadowrocket 生成器跳过该应用协议类型,此次没有新增其他客户端的 STUN 防护。Host 不能表达协议/进程/IP 条件,生成配置与静态域名匹配也不能证明设备 DNS/WebRTC 没有泄漏。 + +## 本阶段备份与验证 + +写入前在线备份:`/opt/proxy-station/data/backups/before-remove-speedtest-ai-priority-20261004-105853.db`,root 属主、600 权限。敏感 API 快照和订阅仅保存在私有临时目录。 + +重读确认 11 个组、129 条规则、21 个组成员、30 个节点;除 Speedtest 删除与两个排序外,其余实体字段保持原状。当前没有直接指向 Automatic 的规则,只有 Proxy 引用它,Automatic 仍为 url-test。 + +候选 Surge 配置先通过官方 CLI,写入后从实际 HTTPS 入口读取三个客户端的订阅: + +- 三端组显示顺序均与上述一致,Speedtest 组及引用已消失,AI 均位于广告规则之前。 +- Surge/Shadowrocket 以当前规则集快照检查 126 个 AI 域名样例,全部首条域名匹配 AI;包括 Claude/OpenAI、附件/MCP、Statsig、已确认的 Datadog/Sentry/LiveKit 主机及 api.github.com。Google 更新/翻译例外为 DIRECT,x.com/bing.com 为 Proxy,doubleclick.net 仍 REJECT,Bybit 与 Amplitude 业务例外保持原策略。 +- Surge self URL 正确,Host 中 Unbreak 在 AI 集之前,Statsig 的 AI DNS 映射正确;官方 CLI 与 49/49 规则集快照再次通过,静态域名遮蔽提示为 0。 +- Clash 结构与 `mihomo -t` 通过,FINAL/MATCH 末尾兜底保留。 + +样例检查不模拟 IP、进程、协议、CNAME 或设备缓存。本阶段 Shadowrocket 未做真机编译,所有客户端均未做业务连接、测速或 DNS/WebRTC 实测;未切换本机活动配置或修改设备已保存选择。 + +同日后续已完成本机 Net.Coffee DNS 复测,见 [检测机制与结果](NET-COFFEE-DNS-2026-10-04.md);随后创建独立 `🧠 AI Nodes`、统一节点默认权重为 0,并验证两组完整名称排序与保存选择,见 [AI Nodes](AI-NODES-2026-10-04.md)。这些后续验证不构成手机或其他业务路径的验收。 diff --git a/docs/AI-RULE-REVIEW-2026-10-04.md b/docs/AI-RULE-REVIEW-2026-10-04.md new file mode 100644 index 0000000..4e4c536 --- /dev/null +++ b/docs/AI-RULE-REVIEW-2026-10-04.md @@ -0,0 +1,86 @@ +# 🤖 AI 规则审阅与精确补齐(2026-10-04) + +本文按阶段保留规则补齐及后续删组、调整优先级的证据,数量与选择链均以各段发生时为准。最新规则、组与客户端产物见 [当前生产状态](CURRENT-STATE.md),不按历史阶段恢复旧配置。 + +本次按用户提供的 [Claude 清单](https://ip.net.coffee/claude/site.html) 与 [ChatGPT 清单](https://ip.net.coffee/gpt/site.html) 审阅线上规则。用户选择「按 AI 专用域名精确补齐;共享服务只加入已确认的主机」。已有远程规则集继续自动更新,缺项通过管理 API 添加到同一 `🤖 AI` 策略。规则集 API 只读,不能修改第三方托管文件,也不通过直接写库替换它们。 + +## 原有覆盖 + +| 原有规则 | 审阅结果 | +|---|---| +| Sukka `non_ip/ai.conf` | 实际下载版本为 2026-10-03、53 条;已有 Anthropic、Claude、clau.de、ChatGPT、chat.com、Sora、oaistatic 等域名。`DOMAIN-KEYWORD,openai` 已覆盖所列 OpenAI/Azure/CDN 中包含 openai 的主机,但缺少 oaiusercontent、oaistatsig、MCP 和语音等域名。 | +| Blackmatrix7 `Bing.list` | 9 条,包含 Bing 搜索与相关服务;精确补齐时保留,随后按用户要求移除,见文末后续调整。不是 Claude/OpenAI 专项规则集。 | +| `DOMAIN-SUFFIX,googleapis.com` | 范围包括非 AI Google APIs;保留原业务行为。Unbreak 中靠前的 Google 更新、翻译等 DIRECT 例外继续优先。 | +| Apple Relay 两条后缀及 `apple-relay` 关键词 | 后缀与关键词重叠;保留现有 Apple Intelligence/Relay 行为。 | +| Dia 后缀与关键词 | 停用 `DOMAIN-KEYWORD,diabrowser.engineering`,保留 `DOMAIN-SUFFIX,diabrowser.engineering`,避免关键词匹配域名之外的同名片段。 | +| 已停用的全局 `PROTOCOL,DOH` | 继续停用;仅从 AI 域名规则生成 Surge DNS 映射。 | + +原有 Sukka 集还包含 `api.github.com`、多种其他 AI 服务及历史域名。此次保持原引用,精确原则用于新增规则,不声称整个原有 AI 组已收敛为 Claude/OpenAI 专属。 + +## 新增的 24 条规则 + +以下全部指向 `🤖 AI`。22 条域名规则放在现有 `api.amplitude.com` 之后、第一条广告拦截规则集之前;保留更早的协议阻断、业务例外和 Unbreak。2 条 IP 兜底放在原有 AI/Bing 域名规则之后,均使用 `no-resolve`。其他原有规则的相对顺序保持不变,唯一启用的 FINAL 保持末尾。 + +| 类型 | 匹配值 | +|---|---| +| DOMAIN-SUFFIX | claudemcpclient.com | +| DOMAIN-SUFFIX | claudemcpcontent.com | +| DOMAIN-SUFFIX | claudeusercontent.com | +| DOMAIN-SUFFIX | oaiusercontent.com | +| DOMAIN-SUFFIX | crixet.com | +| DOMAIN-SUFFIX | oaistatsig.com | +| DOMAIN-SUFFIX | chatgpt.livekit.cloud | +| DOMAIN | servd-anthropic-website.b-cdn.net | +| DOMAIN | anthropic.com.cdn.cloudflare.net | +| DOMAIN | anthropic.auth0.com | +| DOMAIN | anthropic-com.ghost.io | +| DOMAIN | browser-intake-us5-datadoghq.com | +| DOMAIN | http-intake.logs.us5.datadoghq.com | +| DOMAIN | client-api.arkoselabs.com | +| DOMAIN | host.livekit.cloud | +| DOMAIN | turn.livekit.cloud | +| DOMAIN | o33249.ingest.sentry.io | +| DOMAIN | o207216.ingest.sentry.io | +| DOMAIN | browser-intake-datadoghq.com | +| DOMAIN | rum.browser-intake-datadoghq.com | +| DOMAIN-SUFFIX | statsig.anthropic.com | +| DOMAIN-KEYWORD | chatgpt-async-webps-prod- | +| IP-CIDR | 160.79.104.0/23 | +| IP-CIDR6 | 2607:6bc0::/48 | + +`statsig.anthropic.com` 原本已经被 Anthropic 后缀覆盖,但更早的广告域名集包含它;这条是优先级例外,而非新增域名覆盖。`browser-intake-datadoghq.com` 及其 `rum` 主机也原先被广告域名集拦截,补充后优先交给 AI。用户选择接受这些已确认 AI 遥测主机,不据此放行所有遥测服务。 + +ChatGPT 动态 Azure 主机的范围参考 [v2fly OpenAI 原始清单](https://raw.githubusercontent.com/v2fly/domain-list-community/master/data/openai)。当前数据模型没有顶层 DOMAIN-WILDCARD;本次使用 ChatGPT 服务专用前缀关键词,避免添加整个 `webpubsub.azure.com` 后缀。关键词匹配不是锚定的完整域名正则,也会匹配其他包含相同字符串的主机;这是一项明确的表达能力限制。它可以在三个现有客户端产物中下发,并触发 Surge 域名 DNS 映射。 + +## 没有照搬的项目 + +- 不新增整个 `sentry.io`、`statsigapi.net`、`intercom.io`、`intercomcdn.com` 或泛化 `datadog`/`sentry`/`sift` 关键词;不将通用 `cdn.usefathom.com` 当作 AI 专属。这些会改变其他应用的流量。只添加上表确认的遥测主机;Claude CLI 的两个 Datadog 主机由 [官方网络要求](https://code.claude.com/docs/en/network-config#network-access-requirements) 确认,OpenAI 的两个 Sentry 与 rum 主机由 [官方网络建议](https://help.openai.com/en/articles/9247338-network-recommendations-for-chatgpt-errors-on-web-and-apps) 确认。 +- Claude 页面提供的 `/21` 和 IPv6 `/32` 比 API 接收请求的网段宽。[Anthropic 官方 IP 文档](https://platform.claude.com/docs/en/api/ip-addresses) 将 inbound 标为 `160.79.104.0/23`、`2607:6bc0::/48`,而 `/21` 是服务端向外发起工具请求的出口范围。本次使用 inbound 范围,不加扩大覆盖的 ASN 兜底。 +- 不添加全局 NTP 分流。页面称 NTP 返回代理所在地时区,这不符合 [RFC 5905](https://www.rfc-editor.org/rfc/rfc5905.html) 的 UTC 时间模型;转发 NTP 也不会修改设备时区。 +- IPv6 `/48` 是路由兜底,不启用客户端或边缘 IPv6。边缘继续遵守 IPv4-only,命中规则不保证 IPv6 字面目标可以连接。 + +## 验证与限制 + +变更前、候选配置与写入后的实际 HTTPS Surge 订阅均通过官方 Surge CLI;每次 60/60 远程规则集快照下载成功并再次通过 CLI,静态域名遮蔽提示为 0。写入前在线备份,新增规则先以停用状态创建、提交全量顺序后逐条启用,再停用 Dia 关键词,逐条核对 HTTP 状态。 + +线上已重读确认规则从 116 增至 140,其中启用 138 条、启用的 AI 规则 31 条;其他原有规则字段与相对顺序、节点、组、设置及规则集引用均未改变。实际 HTTPS 的 Surge、Clash、Shadowrocket conf 均包含全部 24 条新增规则,节点与组产物与变更前一致,FINAL/MATCH 末尾兜底验证通过;Surge 的 self URL 和新增域名 DNS 映射正确。38 个 AI 域名样例的首条域名匹配均为 AI,3 个 Google Unbreak DIRECT 例外仍保留。Clash 实际订阅另通过独立临时目录的 `mihomo -t`;Shadowrocket 为结构验证,未做真机编译。文档变更通过 `git diff --check`。 + +备份:`/opt/proxy-station/data/backups/before-ai-claude-chatgpt-precision-20261004-102138.db`,已收紧为 root 属主、600 权限。含凭据的 API 快照及订阅仅保存在权限受控的私有临时目录,不写入仓库。本次只有生产数据与文档变更,没有代码部署。 + +本次候选校验还发现:`AND,((DOMAIN-SUFFIX,webpubsub.azure.com),(DOMAIN-KEYWORD,chatgpt-async-webps-prod-)),"🤖 AI"` 被本机 Surge CLI 拒绝,同一规则将末尾策略裸写为 `🤖 AI` 则通过。当前生成器为含空格的策略输出引号,因此本次没有写入这条 AND。以后修改逻辑规则生成器时,应单独补原生 CLI 用例并核实带空格策略的处理;本次不部署代码。 + +DNS 映射随新域名规则生成,手写 Host 例外继续优先;IP 规则不生成域名 DNS 映射。现有全局 STUN 阻断可能影响语音 WebRTC/TURN,添加语音域名不表示高级语音已经可用。CLI 和静态覆盖验证不能替代设备上的登录、附件、MCP、语音与 DNS/WebRTC 验收。策略组成员和设备已保存的出口选择均保持现状。 + +## 后续调整:移除 Apple/Global Services 与 Bing 专用分流 + +2026-10-04 按用户要求,经管理 API 删除 `🍎 Apple`、`🌐 Global Services` 两个组,以及指向它们的 9 条规则(apple_cdn、AppleTV、Apple、Twitter、Telegram 域名/IP/ASN、OneDrive、microsoft);另删除 `🤖 AI` 的 Bing RULE-SET。没有将这些专用规则改挂其他策略,后续规则自然接管。用户明确选择保留 `global`/`global_plus` 的 Automatic 策略;当前规则集静态域名匹配确认 x.com、twitter.com、bing.com、www.bing.com 均由 `global` → Automatic。当前 Sukka AI 集不含 Bing/Copilot 名称规则。App Store DIRECT、Apple News 美国、Apple Relay AI 例外继续保留。 + +重读确认组从 14 减至 12、规则从 140 减至 130,组成员剩 25;其余组、规则字段与相对顺序、节点和设置均保持原状,唯一启用 FINAL 仍在末尾。实际 HTTPS Surge、Clash、Shadowrocket conf 均已无这两个组和 Bing 规则引用;Surge self URL 正确,官方 CLI 与 50/50 远程规则集快照校验通过,静态域名遮蔽提示为 0;Clash 的 `mihomo -t` 通过。Shadowrocket 仅验证产物结构,未进行真机编译或业务连接验证。 + +本次写入前在线备份为 `/opt/proxy-station/data/backups/before-remove-apple-global-bing-20261004-103623.db`。只调整生产数据与相关文档,未部署代码或修改初始化模板。 + +同日后续按用户明确要求统一代理入口:原先指向 Automatic 的 20 条规则(包括 global/global_plus)全部改为 Proxy,Automatic 从 select 改为 url-test;AI/Speedtest/NoAuto 去掉 Automatic 成员。因此 x.com/bing.com 的已验证路径变为 global → Proxy;当时 AI 的可选成员为美国、新加坡、日本、All Nodes。该调整覆盖上文“保留 Automatic”的阶段性状态,详见 [Proxy 与 Automatic 选择链](PROXY-AUTOMATIC-2026-10-04.md)。 + +同日再按用户要求删除 Speedtest 组与其一条专用规则,并调整组显示顺序。当时 30 条启用 AI 规则集中排在广告与普通分流之前;用户明确选择保留 STUN 阻断及 Google 更新/翻译直连例外,因此两条协议防护及 Unbreak 在 AI 之前。实际 HTTPS Surge/Shadowrocket 的 126 个 AI 域名样例均首条匹配 AI,Google 例外仍为 DIRECT,49/49 规则集快照与 Surge CLI、Clash 原生校验通过。该阶段为 11 组、129 条规则、21 个成员,详见 [AI 与广告优先级](AI-PRIORITY-2026-10-04.md)。 + +随后新增 `DOMAIN-SUFFIX,ip.net.coffee → AI`,本机 DNS 探测从中国电信变为美国 Cloudflare;再将 AI 中的 All Nodes 替换为独立 `🧠 AI Nodes`,紧接 AI 显示,两个全节点组按完整名称排列且各自保存选择。最新为 12 组、130 条规则、31 条启用 AI 规则,见 [DNS 复测](NET-COFFEE-DNS-2026-10-04.md)与 [AI Nodes](AI-NODES-2026-10-04.md)。 diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md new file mode 100644 index 0000000..eb4b83f --- /dev/null +++ b/docs/CURRENT-STATE.md @@ -0,0 +1,105 @@ +# 当前生产状态与接续入口 + +核对日期:2026-10-04(UTC+8;管理 API 盘点时间 11:46)。来源为 LA 管理 API 与四类实际 HTTPS 订阅;本次仅更新文档,未执行部署或生产业务数据变更。完整响应、token、凭据与配置仅保存在权限受限的本机临时目录。 + +本文件集中维护最新生产盘点;日期化体检与变更报告保留各阶段证据。后续任务先重读 API,再更新这里,不能把模板、历史数量或本机模块当作生产现状。项目协作约定见 [AGENTS.md](../AGENTS.md),技能入口见 [双工具协作](AI-COLLABORATION.md)。 + +## 节点与订阅范围 + +| 站点 | 登记节点 | 启用 | 停用 | 当前启用协议 | +|---|---:|---:|---:|---| +| JP(日本) | 8 | 4 | 4 | VMess/Trojan × Direct/Warp | +| SG(新加坡) | 8 | 4 | 4 | VMess/Trojan × Direct/Warp | +| LA(美国) | 6 | 4 | 2 | VMess/Trojan × Direct/Warp | +| RN(美国) | 8 | 0 | 8 | 无 | +| 合计 | 30 | 12 | 18 | VMess 6、Trojan 6,均为 WS | + +全部 8 个 SS、6 个 HY2 以及 RN 的 4 个 VMess/Trojan 当前停用。停用是应用数据库状态,不代表 VPS 或远端线路已经移除/故障;不因整理文档、部署代码或重新生成交付页而启用。边缘舰队仍为 SG/LA/RN/JP,部署资料与 IPv4-only 约束见 [节点工作区](../edge-nodes/README.md)及 [IPv4 约定](EDGE-IPV4-ONLY.md)。舰队交付线路数与本应用登记/启用数不是同一指标。 + +现有 1 个启用订阅,nodeIds 为空数组,即下发全部启用且客户端支持的节点;没有公开默认 token。Surge、Clash、Shadowrocket 节点订阅当前均输出 12 个节点;这不是客户端只能支持这两种协议的断言。将来启用 SS/HY2 后仍须按 [客户端能力矩阵](../shared/src/protocols.ts)重新核对过滤与连接。 + +所有节点 sortOrder 现为默认 0。[IR](../server/src/services/ir.ts)按 sortOrder、name 升序读取,当前完整名称顺序为日本 → 新加坡 → 美国,同地区内 Trojan → VMess,同协议 Direct → Warp。新增节点保持默认权重即可按名称自然插入;非零权重会改变该全局来源的排序。Web 节点页另按站点、协议与出口展示。 + +## 策略组:显示顺序与有序成员 + +12 个组全部启用,共 21 个显式成员。下表顺序就是数据库 sortOrder,也是生成配置的组显示顺序。 + +| 顺序 | 名称 | 类型 | 成员/动态节点来源 | +|---:|---|---|---| +| 1 | 🚀 Proxy | select | Automatic、All Nodes、新加坡、美国、日本 | +| 2 | ⚡️ Automatic | url-test | 新加坡、美国、日本 | +| 3 | 📋 All Nodes | select | includeAllNodes,全部可用节点 | +| 4 | 🇺🇸 United States | url-test | includeAllNodes+美国名称正则 | +| 5 | 🇸🇬 Singapore | url-test | includeAllNodes+新加坡名称正则 | +| 6 | 🇯🇵 Japan | url-test | includeAllNodes+日本名称正则 | +| 7 | 🤖 AI | select | 美国、新加坡、日本、🧠 AI Nodes | +| 8 | 🧠 AI Nodes | select | includeAllNodes,全部可用节点 | +| 9 | 🎬 Streaming | select | 新加坡、美国、日本 | +| 10 | 🪙 Bybit | select | DIRECT、新加坡、美国、日本 | +| 11 | 🇨🇳 Mainland | select | DIRECT | +| 12 | 🎯 NoAuto | select | Mainland | + +- 普通代理规则指向 Proxy,只有 Proxy 包含 Automatic;没有直接指向 Automatic 的规则。 +- AI 不引用 Automatic。All Nodes 与 AI Nodes 使用独立组身份保存节点选择;两组无正则、无显式成员,节点来源与名称升序列表相同。AI Nodes 紧接 AI,不能通过引用 All Nodes 来实现独立选择。 +- Automatic 和三个地区组的数据库类型均为 url-test,interval=300、tolerance=50。组 testUrl 为 `http://cp.cloudflare.com/generate_204`,仅 Clash/Shadowrocket 使用;Surge 使用 General/节点测速地址。客户端自身模块可能覆盖组类型或参数。 +- select 在没有有效保存选择时使用首个成员;名称排序和改名可能影响这个默认值。已保存的实际选择需在授权的客户端更新任务中保留。 +- FINAL → NoAuto → Mainland → DIRECT。Bybit 的首个候选同为 DIRECT,实际使用仍由客户端保存的选择决定。 + +Apple、Global Services、Speedtest、Other,以及香港/台湾/韩国/英国地区组当前不存在。App Store DIRECT、Apple News 美国、Apple Relay AI 例外保留。原初始化模板还有历史组,修改模板或 reset 规则不能代替定向更新生产数据。 + +## 规则与服务归属 + +数据库有 130 条规则:128 启用、2 停用;登记 62 个规则集,实际配置引用 49 个不同远程 URL。唯一启用 FINAL 位于末尾,保留 dns-failed。 + +连接规则按首条匹配,当前顺序为: + +1. STUN REJECT,以及 googlevideo.com 的 UDP REJECT-NO-DROP。 +2. Unbreak DIRECT,保留已授权的 Google 更新/翻译直连例外。 +3. 31 条启用的 AI 规则集中排列,包含原 AI 集、精确补充域名、Anthropic IP 兜底及 `ip.net.coffee` 后缀。 +4. 业务例外、广告/隐私规则及其余服务、地区、国内、CDN、global 分流。 +5. FINAL → NoAuto。 + +两条停用规则为全局 `PROTOCOL,DOH → AI` 和 `DOMAIN-KEYWORD,diabrowser.engineering → AI`;Dia 的 DOMAIN-SUFFIX 规则仍启用。普通代理入口统一到 Proxy;x.com/bing.com 的已验证路径是 global → Proxy,Bing 专用 AI RULE-SET 已删除。具体域名仍受更早规则及动态外部规则集影响,后续变更须重审遮蔽。 + +直接指向 Streaming 的启用 RULE-SET 共 13 条;相关服务的地区分流与 Emby 归属一并列在下表: + +| 来源 | 策略 | +|---|---| +| Disney、Netflix、TikTok、YouTube、GoogleSearch | 🎬 Streaming,各 1 条 | +| stream_eu、stream_kr、stream_hk、stream_tw | 🎬 Streaming,各域名/IP 两条 | +| stream_us | 🇺🇸 United States,域名/IP 两条 | +| stream_jp | 🇯🇵 Japan,域名/IP 两条 | +| Emby | 🚀 Proxy,1 条 | + +该表表示规则指定策略,不保证某服务的所有请求都走该组;例如先匹配 AI 的 Google API 请求或被协议防护拦截的请求仍执行先前规则。 + +## DNS、客户端差异与验收范围 + +Surge AI DNS 从启用 AI 域名规则派生 Host 到 `https://1.1.1.1/dns-query`,并生成 DoH 跟随出口开关与 1.1.1.1/32 → AI 规则。手写 Host、Unbreak 和默认国内 DNS 例外继续保持顺序。数据库 General 中原始开关为 false,生成配置派生为 true;原文设置与最终产物须分别核对。没有启用全局 DOH/全端口 53 AI 接管,边缘 IPv4-only 约束继续保留。 + +Net.Coffee 的 Claude 页通过随机 `*.d.ip.net.coffee` 记录递归解析器,使用 `DOMAIN-SUFFIX,ip.net.coffee → AI` 覆盖页面及子域。前面 Session 的本机 Chrome+Surge 实测已从中国电信「可能泄露」变为美国 Cloudflare「未检测到泄露」;`dns trace` 确认 Cloudflare DoH。此结果限于该探测路径,不代表其他设备、域名或远端代理解析器已验收。 + +| 实际 HTTPS 产物 | 节点 | 组 | 规则 | 说明 | +|---|---:|---:|---:|---| +| Surge | 12 | 12 | 131 | 128 条启用数据库规则+3 条派生 DNS 出口规则 | +| Clash | 12 | 12 | 126 | 49 个 provider;跳过 STUN 与 SMTP 文本 URL-REGEX | +| Shadowrocket 节点订阅 | 12 | — | — | 完整名称升序的 base64 URI 列表 | +| Shadowrocket conf | — | 12 | 122 | 保守跳过 6 条:STUN、googlevideo AND、reject/download/cdn DOMAIN-SET、SMTP 文本 URL-REGEX | + +HTTP 订阅只有正文,没有 excluded/skippedRules 字段;总节点数与实际节点数不同,要区分停用、token 范围与客户端过滤。Shadowrocket 的全节点正则会收纳 App 内其他订阅,App 排序和其他节点不由本服务控制。 + +前面 Session 最近一次验证已通过 Surge 官方 CLI、49/49 规则集快照及 Clash `mihomo -t`,静态域名遮蔽提示为 0;本机 Surge 已验证新组名、两个列表的名称顺序和独立选择,改名后保留原节点及 AI 路径。本轮仅重读最新数据/产物并验证文档一致性,没有重复进行网络泄漏测试。iOS/Android 真机编译、节点连接、UDP、完整业务与隐私验收仍有未完成项。 + +## Session 变更记录与接续 + +| 主题 | 记录 | 后续维护要点 | +|---|---|---| +| Surge 体检与防护修复 | [体检](SURGE-AUDIT-2026-10-03.md)、[修复](SURGE-FIX-2026-10-03.md) | 保留 API 校验、FINAL、引用图、配置编码及 Host 顺序防护 | +| 移动端生成器体检与已授权后端部署 | [移动端体检](MOBILE-CLIENT-AUDIT-2026-10-04.md) | 能力矩阵与生成器范围独立检查,保留手机真机待验项 | +| AI 精确补充与删除 Apple/Global/Bing 专用分流 | [AI 规则审阅](AI-RULE-REVIEW-2026-10-04.md) | 精确补充主机;历史阶段保留 Automatic 的记录已被后续 Proxy 入口调整覆盖 | +| Proxy 统一普通代理入口、Automatic 测速 | [选择链](PROXY-AUTOMATIC-2026-10-04.md) | Proxy 为唯一 Automatic 父组,AI/NoAuto 独立 | +| 删除 Speedtest、AI 优先于广告 | [AI 优先级](AI-PRIORITY-2026-10-04.md) | 该阶段为 11 组/129 规则,后续新增项以本文件为准 | +| Net.Coffee 探测域名与 DNS 复测 | [DNS 复测](NET-COFFEE-DNS-2026-10-04.md) | 覆盖随机子域,区分网站探测与 Claude 实际域名 | +| 独立 AI Nodes、emoji 与名称排序 | [AI Nodes](AI-NODES-2026-10-04.md) | 独立 select、同节点来源、默认零权重、保留客户端选择 | + +备份文件名与原阶段验收保留在各专题记录中,不清理远端备份。新增业务约束更新 AGENTS.md;当前盘点更新本文件;操作方法更新对应专题。代码部署、生产数据更新和设备配置更新分别核对授权,纯文档整理不触发这些动作。 diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 97730d6..99c2b03 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -4,6 +4,8 @@ 本文描述 Proxy Station 应用发布。边缘 VPS 的部署、重建与协议维护已合入本仓库,见 [节点工作区](../edge-nodes/README.md) 与 `edge-node-multi-protocol` 技能;其私有资料不随应用部署上传。LA 同时承载节点与本应用,维护 nginx、证书或 WARP 时需核对共享服务影响。 +当前配置与变更接续入口见 [生产状态](CURRENT-STATE.md)。应用部署只更新代码/产物,不重置生产数据库,不恢复已停用节点,也不自动修改客户端的活动配置与保存选择。 + ## 拓扑 ``` @@ -38,14 +40,19 @@ SSH 别名必须用小写 `la`,大写会被当作域名解析。 ## 迁移数据库 -用 sqlite3 在线备份导出一致副本,避免直接拷贝带 WAL 的库文件: +普通节点、组、规则与设置调整使用 update-db 管理 API,并先做生产 SQLite 在线备份。整库迁移/恢复只属于用户明确授权的相应任务;按技能恢复流程安排停服窗口、回滚备份和后续 HTTPS 验收,不用替库代替定向业务修改。 + +需要从本机导出时,使用在线备份取得一致副本,文件保存在私有临时目录: ```bash -sqlite3 server/data/proxy-station.db ".backup /tmp/ps.db" -scp /tmp/ps.db la:/tmp/ps.db -ssh la 'systemctl stop proxy-station && install -m 644 -o proxy-station -g proxy-station /tmp/ps.db /opt/proxy-station/data/proxy-station.db && rm -f /opt/proxy-station/data/proxy-station.db-wal /opt/proxy-station/data/proxy-station.db-shm /tmp/ps.db && systemctl start proxy-station' +umask 077 +migration_dir="$(mktemp -d /private/tmp/proxy-station-migration.XXXXXX)" +sqlite3 server/data/proxy-station.db ".backup $migration_dir/proxy-station.db" +chmod 600 "$migration_dir/proxy-station.db" ``` +远端备份确认 root 属主、600 权限;恢复后的活动数据库属运行用户,保留仅必要的访问权限。不要直接拷贝运行中的主库而漏掉 WAL,也不把含真实凭据的库放进 Git 或共享日志。 + ## 反向代理注意点 - aaPanel 的反代模板不带 `X-Forwarded-Proto`/`X-Forwarded-Host`,已手工补进反代配置。服务端 `routes/sub.ts` 的 `baseUrl()` 依赖这两个头还原外部地址,缺失时 Surge 的 `#!MANAGED-CONFIG` 与 Clash 的 provider URL 会变成 `http://`。 @@ -56,6 +63,7 @@ ssh la 'systemctl stop proxy-station && install -m 644 -o proxy-station -g proxy ## 验证 ```bash -curl -s https://ps.la.njcqtechaicoding.com/api/health -curl -s "https://ps.la.njcqtechaicoding.com/sub//clash" > /tmp/c.yaml && mihomo -t -f /tmp/c.yaml +curl -fsS --max-time 10 https://ps.la.njcqtechaicoding.com/api/health ``` + +部署脚本核对服务状态、源码/前端指纹、公网健康及管理接口鉴权。涉及生成器或 shared 后,还要读取最新节点范围与真实 HTTPS 订阅,按 [订阅验证](SURGE-CHANGES.md)检查 Surge/Clash 及受影响的 Shadowrocket 产物;先确保下载成功及文件非空,不能把 mihomo 为缺失文件创建的默认配置当成验收结果。与部署前候选比对时使用同一最新生产快照。 diff --git a/docs/MOBILE-CLIENT-AUDIT-2026-10-04.md b/docs/MOBILE-CLIENT-AUDIT-2026-10-04.md new file mode 100644 index 0000000..862d03c --- /dev/null +++ b/docs/MOBILE-CLIENT-AUDIT-2026-10-04.md @@ -0,0 +1,73 @@ +# Shadowrocket/ClashMeta 体检:2026-10-04 + +本文保留体检开始时的快照、代码修复及随后已授权部署的证据。下文 18 个启用节点、14 个组等数量属于历史阶段;最新为 12 个启用节点、12 个组,完整盘点见 [当前生产状态](CURRENT-STATE.md)。 + +体检阶段只读盘点生产数据、拉取实际 HTTPS 订阅,对照官方资料审阅生成器,并在本地修复已确认的序列化问题。当时没有部署、修改生产业务数据、提交或推送;订阅访问会正常更新访问统计,自托管规则资源拉取可更新既有缓存。用户后续授权的部署及验收见末尾记录。 + +## 之前检查到哪里 + +[2026-10-03 验证记录](SURGE-FIX-2026-10-03.md) 已包含实际 Clash 订阅的 `mihomo -t`,以及 Shadowrocket conf 的 HTTPS 响应与候选一致性。已有测试覆盖两端的 PROTOCOL 处理、Surge AI DNS 不误传给其他端,以及 Shadowrocket 策略名不加引号;还没有两端节点、组、规则的系统体检或 iOS/Android 真机验收记录。 + +## 官方依据及边界 + +- Android 的目标是 **ClashMeta/mihomo**,不是早期 Clash 内核。依据 [mihomo 节点通用字段](https://wiki.metacubex.one/config/proxies/)、[VMess](https://wiki.metacubex.one/config/proxies/vmess/)、[SS](https://wiki.metacubex.one/en/config/proxies/ss/)、[TLS](https://wiki.metacubex.one/config/proxies/tls/)、[Hysteria2](https://wiki.metacubex.one/config/proxies/hysteria2/)、[策略组](https://wiki.metacubex.one/config/proxy-groups/)、[url-test](https://wiki.metacubex.one/config/proxy-groups/url-test/)、[fallback](https://wiki.metacubex.one/config/proxy-groups/fallback/)、[规则](https://wiki.metacubex.one/config/rules/)、[规则资源](https://wiki.metacubex.one/config/rule-providers/content/),必要时核对 [官方规则解析源码](https://github.com/MetaCubeX/mihomo/blob/Meta/rules/parser.go)。本机验证内核为 mihomo 1.19.32;它不能代替用户 Android App 内核版本及 VPN 接管测试。 +- Shadowrocket 官方来源从 [App Store 开发者页面](https://apps.apple.com/us/app/shadowrocket/id932747118) 指向 [shadowlaunch.com](https://shadowlaunch.com/)。开发者站只提供产品与联系入口,本次没有找到可核实的完整官方配置语法手册。`help.shadowrocket.net` 本次不可访问,也不能从开发者入口验证其归属;第三方手册不冒充官方依据。官方更新记录明确提到逻辑规则、QUIC 匹配和 Hysteria2,足以纠正“客户端不支持逻辑规则”的绝对断言,但不足以证明某一 Surge 写法可以直接透传。 +- SS 插件传输的依据为 [Shadowsocks 官方 SIP003](https://shadowsocks.org/doc/sip003.html):插件只转发 TCP。不能因客户端支持 UDP 就宣称 nginx WS 同端口也提供 SS UDP 服务。 + +## 体检开始时生产快照(历史) + +| 项目 | 结果 | +|---|---| +| 节点 | 30 个,18 个启用,12 个停用 | +| 实际订阅节点范围 | 日本、新加坡、美国各 6 个;VMess/Trojan/SS 各 6 个,均为 WS;当时 HY2 与 RN 节点停用 | +| 组与规则 | 14 个启用组;116 条规则,其中 115 启用;62 个规则集记录 | +| Shadowrocket 节点订阅 | 18 个,排除 0 个;原顺序中 JP/SG 按协议交错 | +| Shadowrocket conf | 14 个组,108 条规则,跳过 7 条 | +| Clash | 18 个节点,14 个组,113 条规则,60 个 provider;跳过 2 条 | + +四类实际 HTTPS 产物均成功拉取;修复前本地生成的 Surge/Clash/Shadowrocket 节点订阅/conf 与线上逐字节一致,确认审阅的是当前下发逻辑。真实密码、UUID、域名、Host IP、token、配置和快照只保存在权限受限的临时目录,不进入本报告或 Git。 + +## 已修复的问题 + +| 问题 | 修复及实际影响 | +|---|---| +| Shadowrocket JP/SG 混排 | 节点订阅对完整名称做确定性的字符串升序排序,不改变 IR、数据库排序或其他客户端顺序。当前顺序为 JP → SG → US;每国内 SS → Trojan → VMess,同协议 Direct → Warp。未来启用 HY2 时按名称自然排入 | +| Clash 未启用 UDP | 原产物 18 个节点均省略 `udp`,VMess/Trojan 默认不能承载 UDP。现在 12 个 VMess/Trojan 显式 `udp: true`;6 个 SS WS 显式 `false`。纯 TCP SS 和 HY2 的占位用例也覆盖;UDP 是否成功穿过远端仍需连接验证 | +| Clash 丢失 provider 的 no-resolve | 当前 IP reject RULE-SET 的 `no-resolve` 被丢弃,可能提前解析。现在保留这个参数,不改变规则顺序或业务策略 | +| Clash 逻辑规则只做文字替换 | 原来任意 PROTOCOL 都变成 NETWORK,遇到 STUN/DOH 子条件会产生无效规则,还可能改写匹配值中的文字。现在按结构递归映射 TCP/UDP,支持明确的等价子类型,任一子条件无法表达时跳过整个表达式;保留当前 googlevideo UDP AND 规则 | +| 动态组丢失显式成员 | Clash 正则分支、Shadowrocket 正则/全节点分支原来会丢节点、组或内置成员;现在保留完整有序显式成员,并继续动态收节点。当前生产动态组没有显式成员,所以这是潜在缺陷修复 | +| 测速容差 | 两端 url-test 显式零容差不再被省略;fallback 不输出 url-test 专用 tolerance。当前生产容差为 50,不受零值修复影响 | +| Clash TLS/VMess 字段 | 补上 VMess TLS 与 SS plugin 内层的 skip-cert-verify 映射;VMess IETF cipher 别名转回 mihomo 的名称,未知 cipher 明确排除并过滤组引用。当前节点均 auto、skipCertVerify=false,因此这些修复不额外排除生产节点 | + +Shadowrocket conf 的动态筛选仍以 App 内全部节点为来源;显式成员保留的文本和结构经过测试,但没有 iOS 编译器的实际验证。Clash 正则仅作用于动态引入来源,不能把显式成员误当作需要筛掉的动态节点。 + +## 仍存在的客户端差异与待验项 + +1. **体检时 Shadowrocket 的 7 条规则缺口**:本生成器保守跳过 `PROTOCOL,STUN`、googlevideo 的 UDP AND、4 个 DOMAIN-SET(reject/apple_cdn/download/cdn)和 SMTP 文本 URL-REGEX。同日后续删除 apple_cdn 规则后,最新产物跳过 6 条,生成器范围没有因此扩大。对应广告拦截、CDN 策略和 UDP 防护不能声称与 Surge 等价。官方版本记录表明逻辑规则支持实际存在;后续应以 App 内帮助和 iOS 编译/匹配日志核实具体语法,再缩小跳过范围。本次已修正源码注释和 AGENTS.md 的错误能力断言。 +2. **Shadowrocket RULE-SET,LAN 与 PROCESS-NAME**:当前产物保留一个 LAN 内置引用及一个 macOS 进程名规则,尚无官方语法证据或 iOS 匹配结果。56 个远程规则集 URL 可达并不证明这两个本地规则有效;iOS 也不能据 macOS 进程名期待相同匹配。 +3. **Clash 的 2 条缺口**:STUN 应用协议识别没有 NETWORK 等价物;SMTP 文本 URL-REGEX 被跳过。当前 googlevideo UDP AND 已正确转换。不能用全局拒绝 UDP 代替 STUN 规则,否则会改变业务。 +4. **SS WS 没有插件 UDP 转发**:选到这类节点时,UDP 无法由 WS 线路承载。mihomo 会继续向后匹配;可能走其他策略或 FINAL。未擅自新增全局 UDP 阻断、UDP-over-TCP 或改变默认选择。需在设备侧分别核对 VMess/Trojan 与 SS 候选的 UDP 行为。 +5. **DNS 与原文段落不等价**:Clash 使用自己的 fake-ip/DoH 配置,不读取 Surge Host、重写、MITM、Script;Shadowrocket 仅复制 General 白名单及 Host/URL Rewrite/MITM,不下发 Header Rewrite/Script。Surge 自动 AI DNS 映射不会应用到两端。当前保留的 Host/MITM 语法、证书安装、DNS/WebRTC 行为仍需设备侧检查。 +6. **节点 URI 的扩展字段**:VMess/Trojan 分享链接编码未完整保留 skipCertVerify;HY2 URI 未表达 up/down 限速;SS plugin 的独立 SNI/WS Host 不一定能同时表达。HY2 解析目前还把 pinSHA256 当成跳过证书验证,却没有保存指纹,应在后续协议字段修复中拒绝或完整保留这类输入。当前启用节点 skipCertVerify 全 false、HY2 停用,18 个节点的实际连接字段往返核对通过;这不构成这些潜在输入的支持证明。 +7. **特殊字符与资源语义**:Shadowrocket 正则内逗号、规则扩展参数及复用 Surge 原文段落没有完整官方语法验证;Clash 的 Go 正则也不完全等同 JS 正则。当前地区正则只是普通或分支,可以正常解析。映射到不同维护者的规则列表存在内容差异;资源可达/语法通过不保证逐条业务语义与 Surge 相同。 + +## 体检阶段验证 + +- 全仓类型检查通过,51 项 Vitest 通过;新增测试覆盖四种节点协议、完整名称排序与 IR 不变、动态组显式成员、零容差、provider 命名冲突/no-resolve、逻辑子规则映射/整条跳过、TLS/cipher、排除后的组引用,并调用本机 mihomo 原生解析器。 +- 线上旧 Clash 配置与本地修复候选都通过 `mihomo -t`。60/60 Clash provider 下载成功;把当前资源展开为 152,295 条辅助规则后也通过原生解析,覆盖资源内的 IP-ASN 等类型。展开检查用于核对子规则语法,不替代 Android provider 初始化、域名匹配语义或规则更新行为。 +- Shadowrocket 18 个 URI 与原始节点的相关协议字段逐项核对,名称顺序正确;14 个组、108 条规则引用完整,FINAL 唯一且末尾,没有 `use=`。56 个远程规则集属于 Surge 检查已下载的资源。没有官方 iOS CLI,所以这是结构和序列化检查,不能称为 iOS 编译通过。 +- 实际 HTTPS Surge 运行 `scripts/check-surge.py --fetch-rulesets`:官方 CLI OK,60/60 资源下载,本地快照 CLI OK,静态域名遮蔽提示 0。候选 Surge 与实际产物一致。 +- `git diff --check` 通过。未切换活动配置、启动代理服务、导入 CA、测速或测试节点连接;iOS/Android 的订阅更新、编译、默认选择、规则命中、DNS/WebRTC、TCP/UDP 连接仍需真机验收。 + +后续本地变更至少执行 `pnpm -r typecheck`、`pnpm test`;Clash 使用独立临时目录 `mihomo -t -d <目录> -f <配置>`。涉及真实订阅仍按 [Surge 变更验证](SURGE-CHANGES.md) 检查实际 HTTPS 产物,生产部署和数据变更分别遵循 deploy/update-db 技能。 + +## 后续部署与验收:2026-10-04 + +用户明确授权后,通过 deploy 技能执行 `deploy.sh server --skip-checks`。跳过脚本内重复检查前,已重新完成全仓类型检查、51 项测试、最新生产快照的候选 Surge/Clash 解析与规则集快照校验。 + +- 部署完成:`server @ 349ab83`,包含工作区未提交的修复;后端源码指纹前缀 `b3aaeb92cba2` 与远端一致,旧源码保留为 `src.old`。systemd active、公网健康及未带 token 的管理接口返回 401 均正常。 +- 部署前最新生产节点范围已变为 12 个:日本/新加坡/美国各 4 个 VMess/Trojan 节点。本次保留当前启用状态,没有恢复体检快照里的其他节点或修改业务数据。 +- 部署后四类实际 HTTPS 订阅与最新已验证候选逐字节一致。Shadowrocket 的 12 个节点按完整名称升序排列,国家顺序 JP → SG → US,国内 Trojan → VMess,同协议 Direct → Warp。 +- 实际 Surge:官方 CLI OK、60/60 规则资源下载成功、本地快照 CLI OK、静态域名遮蔽提示 0。实际 Clash:mihomo 解析通过。 + +设备需要重新更新节点订阅;此次确认的是线上响应顺序,尚未操作 iOS/Android 客户端。前述客户端能力缺口与真机验收边界仍然适用。没有创建 Git 提交或推送。 diff --git a/docs/NET-COFFEE-DNS-2026-10-04.md b/docs/NET-COFFEE-DNS-2026-10-04.md new file mode 100644 index 0000000..fcb87f8 --- /dev/null +++ b/docs/NET-COFFEE-DNS-2026-10-04.md @@ -0,0 +1,48 @@ +# Net.Coffee Claude 页面 DNS 检测与复测(2026-10-04) + +本文保留当日页面脚本和本机实测证据;后续独立 AI Nodes 与当前订阅范围见 [当前生产状态](CURRENT-STATE.md)。页面实现及解析器出口可能变化,后续复测仍需重新核对。 + +用户要求检查 [Claude 检测页](https://ip.net.coffee/claude/) 的 DNS 检测机制,若使用该站域名,则将它加入 AI 分流并在本机复测。本次仅新增一条生产规则并刷新本机既有托管订阅,未部署应用代码。 + +## 实际检测机制 + +检查当日页面引用的 [claude-page.js](https://ip.net.coffee/claude/claude-page.js?v=20260916a),`detectDNSLeak()` 的实现为: + +1. 用 `Math.random()` 生成本次测试 token。 +2. 依次加载 `https://-1.d.ip.net.coffee/pixel.gif` 与 `https://-2.d.ip.net.coffee/pixel.gif`,通过新主机名触发 DNS 查询。 +3. 等待后查询同源 `/api/dns/result/`,读取 `dns_servers` 数组,即该站权威 DNS 观察到的递归解析器 IP。 +4. 查询这些解析器的地理信息;若其中有中国解析器,而页面认定的 Claude 出口不在中国,则显示「可能泄露」。此判断不要求 DNS 解析器 IP 等于 Claude 连接出口 IP,也不要求两者来自同一家运营商。 + +源码前面的注释仍提到 ipleak.net,但实际请求已使用该站自托管的 `d.ip.net.coffee`。检测没有查询 Claude 域名本身;在按域名分流的环境里,探测域名使用普通国内 DNS,不能单凭此结果认定 Claude 域名也使用国内 DNS。若接口未返回解析器,该页面会显示「DNS 加密或未暴露出口」;空结果也不能证明没有泄漏。 + +## 生产规则 + +经管理 API 新增启用的自定义规则: + +```ini +DOMAIN-SUFFIX,ip.net.coffee,"🤖 AI" +``` + +后缀匹配同时覆盖页面根域和随机探测子域,精确 `DOMAIN,ip.net.coffee` 无法覆盖后者。新规则放在现有 AI 规则段末尾、广告和普通业务分流之前;既有协议防护、Unbreak、其他规则的相对顺序及唯一启用的末尾 FINAL 保持原状。 + +Surge 生成器由该规则自动派生: + +```ini +ip.net.coffee = server:https://1.1.1.1/dns-query +*.ip.net.coffee = server:https://1.1.1.1/dns-query +``` + +已有 `encrypted-dns-follow-outbound-mode` 与 `1.1.1.1/32 → AI` 规则使该 DoH 查询随 AI 出口。没有修改全局 DNS、手写 Host、策略组成员或节点。 + +## 备份与验收 + +写入前在线备份:`/opt/proxy-station/data/backups/before-add-ai-net-coffee-dns-20261004-111830.db`,已收紧为 root 属主、600 权限。敏感 API 快照、实际订阅和活动配置备份仅保存在权限受限的本机临时目录。 + +- 重读确认规则从 129 条变为 130 条,仅新增上述规则;原规则字段和相对顺序保持一致,组、节点及设置重读与变更前相同。 +- 从实际 HTTPS 入口拉取 Surge 与 Clash 订阅,核对托管 self URL、新规则和两条派生 Host 映射。 +- `scripts/check-surge.py --fetch-rulesets` 通过官方 Surge CLI 和 49/49 规则集快照校验,静态域名遮蔽提示为 0;Clash 的 `mihomo -t` 通过。 +- 本机原活动配置的托管 URL 与此次生产订阅一致。备份后用 `surge-cli managed-profile update` 更新同一托管配置,并在重载生效后确认有效配置包含新规则及 DNS 映射,原 AI 选择保持不变。 +- 本机 `rule explain` 确认根域与探测子域命中 AI;`dns trace` 确认探测子域使用 `https://1.1.1.1/dns-query`。 +- 同一个 Chrome 检测页修改前刷新显示中国电信 DNS「可能泄露」;修改后刷新显示美国 Cloudflare DNS「未检测到泄露」,Claude 连接出口保持原美国线路。WebRTC 显示「已禁用或无泄露」。 + +这是该网站、该浏览器和本机 Surge 的实际 DNS 复测;其他设备、其他客户端及远端代理解析器没有在此次测试中验收。网站测试实现与外部规则集后续可能变化,维护时应重新检查探测主机名。 diff --git a/docs/PROXY-AUTOMATIC-2026-10-04.md b/docs/PROXY-AUTOMATIC-2026-10-04.md new file mode 100644 index 0000000..0389a77 --- /dev/null +++ b/docs/PROXY-AUTOMATIC-2026-10-04.md @@ -0,0 +1,41 @@ +# Proxy 与 Automatic 选择链(2026-10-04) + +最新节点范围、组成员及规则数量集中在 [当前生产状态](CURRENT-STATE.md)。下文业务约束已衔接后续修改;操作与备份记录保留统一代理入口时的阶段证据。 + +当前生产配置按用户要求由 Proxy 控制普通代理流量的出口。已有数据库通过管理 API 调整,原始初始化模板未修改,不用规则 reset 恢复旧引用。 + +## 业务约束 + +- 不允许规则直接指向 `⚡️ Automatic`;普通代理规则指向 `🚀 Proxy`。 +- 只有 Proxy 的成员列表包含 Automatic。AI、NoAuto 和其他组不独立引用 Automatic。Speedtest 组与其专用规则在同日后续调整中已删除。 +- Proxy 保持 `select`,成员顺序为 Automatic、All Nodes、新加坡、美国、日本。用户在客户端选择 Automatic 时,匹配 Proxy 的业务流量才沿 Automatic 选出口;选择其他成员则使用对应成员。 +- Automatic 为 `url-test`,保留新加坡、美国、日本三个地区组;这三个地区组本身也为 url-test。沿用 `interval=300`、`tolerance=50`。Surge 用 General 的 `proxy-test-url` 或节点 `test-url`,不输出组级 `url=`;Clash/Shadowrocket 使用组的 `http://cp.cloudflare.com/generate_204`。 +- AI 保持 select,成员为美国、新加坡、日本、🧠 AI Nodes。🧠 AI Nodes 是同日后续新增的独立 select 组,直接收纳全部节点,节点列表与 All Nodes 相同,按完整名称升序输出,两个组各自保存选择;见 [AI Nodes](AI-NODES-2026-10-04.md)。NoAuto 保持 select,只含 Mainland。 +- AI、Streaming、Bybit、直连、拦截和 FINAL 等原有分流继续独立生效;此次没有把全部流量统一改到 Proxy。FINAL 仍指向 NoAuto。 + +以上约束针对业务请求的策略路径,不等于关闭客户端测速或手动测试。 + +当前组显示顺序为 Proxy、Automatic、All Nodes、美国、新加坡、日本、AI、🧠 AI Nodes、Streaming、Bybit、Mainland、NoAuto。同日后续删除 Speedtest、集中排列 AI 规则、加入 Net.Coffee 检测域名并新增 AI Nodes 后,现状为 12 组、130 条规则、21 个成员;详情见 [AI 与广告优先级](AI-PRIORITY-2026-10-04.md)、[DNS 复测](NET-COFFEE-DNS-2026-10-04.md)与 [AI Nodes](AI-NODES-2026-10-04.md)。下文备份与数量记录的是本次统一代理入口时的阶段性状态。 + +## 统一代理入口阶段的数据变更 + +将 20 条原先指向 Automatic 的规则改到 Proxy: + +| 类型/规则集 | 范围 | +|---|---| +| RULE-SET/DOMAIN-SET | Emby、Facebook、Instagram、Threads、Discord、GitHub、substore、CDN 两条、global_plus、global | +| DOMAIN/DOMAIN-SUFFIX | api.amplitude.com、vercel.app、nssurge.com、surgee.me、www.torrentmac.net、cloudflare.com、jable.tv、m.cmx.im、api.themoviedb.org | + +另将 Automatic 类型改为 url-test,从 AI/Speedtest/NoAuto 各移除一个 Automatic 成员。重读确认 12 个组、130 条规则、22 个组成员;没有直接指向 Automatic 的规则,唯一父组为 Proxy。其他规则字段与相对顺序、其他组配置、节点和设置均保持原状,唯一启用 FINAL 位于末尾。 + +x.com、twitter.com、bing.com、www.bing.com 原先由 global 接到 Automatic,当前 global 策略已改为 Proxy;后续出口由 Proxy 的客户端选择决定。Apple/Global Services 组及其专用规则、AI 的 Bing 专用规则仍维持已删除状态。 + +## 统一代理入口阶段的备份与验证 + +在线备份:`/opt/proxy-station/data/backups/before-proxy-gates-automatic-20261004-104820.db`,root 属主、600 权限。敏感 API 快照和实际订阅只存放在权限受控的私有临时目录。 + +实际 HTTPS Surge、Clash、Shadowrocket conf 均确认 Automatic 为 url-test、没有规则直接指向 Automatic、Proxy 为唯一父组、AI/Speedtest/NoAuto 已移除该选项,global 指向 Proxy。Surge self URL 正确;官方 Surge CLI 与 50/50 远程规则集快照校验通过,静态域名遮蔽提示为 0;Clash 结构与 `mihomo -t` 通过,FINAL/MATCH 末尾兜底保留。 + +Shadowrocket 仅做产物结构检查,没有真机编译或连接验收。未切换本机活动配置或修改设备已保存的策略选择;设备需要刷新订阅后获取新配置。此次只修改生产数据与文档,未部署代码。 + +测速参数含义以 [Surge url-test 官方说明](https://manual.nssurge.com/policy-groups/url-test.html) 为准:interval 是结果有效期,Surge 在组使用且结果过期等条件下重测,不表示固定每 300 秒执行;tolerance 用于减少相近延迟引起的反复切换。 diff --git a/docs/SEEDING.md b/docs/SEEDING.md index 55b1997..b8764c8 100644 --- a/docs/SEEDING.md +++ b/docs/SEEDING.md @@ -2,6 +2,8 @@ 节点部署管理已迁入本仓库的 `edge-nodes/`,真实配置与交付产物位于 Git 忽略的 `edge-nodes/private/`。Proxy Station 应用运行时不自动读取该目录,节点仍通过 Web 界面批量粘贴分享链接导入。工作区说明见 [edge-nodes/README.md](../edge-nodes/README.md)。 +最新登记、启用与订阅范围见 [当前生产状态](CURRENT-STATE.md)。已有库导入新节点前先读当前节点并预览,避免把整份交付清单重复导入;部署脚本、分享链接存在或重新生成交付页都不表示该节点应被启用。 + ## 从本仓库私有交付文档导入 每个节点目录 `edge-nodes/private/-edge-node-docs/` 已生成好分享链接;以当前部署的 `scripts/node-links/` 为准,避免同时混入旧交付链接与重复副本。RN 当前使用 `rn2-edge-node-docs/`,旧 `rn-edge-node-docs/` 只作历史记录。 @@ -34,13 +36,18 @@ 模板的地区策略组按**节点名正则**筛选(如 `坡|🇸🇬|新加坡|狮城|SG|Singapore`、`美|🇺🇸|美国|US`)。命名时带上地区词与出口词,例如: -- `SG-vmess-direct`、`SG-trojan-warp`(命中 Singapore 组) -- `US-LA-trojan-direct`、`US-RN-vmess-warp`(命中 United States 组;只写 LA/RN 不会命中任何地区组,会落入 Other) +- `🇸🇬 SG VMess Direct`、`🇸🇬 SG Trojan Warp`(命中 Singapore 组) +- `🇯🇵 JP VMess Direct`(命中 Japan 组) +- `🇺🇸 US LA Trojan Direct`、`🇺🇸 US RN VMess Warp`(命中 United States 组;只写 LA/RN 不会命中地区正则,当前也没有 Other 兜底组) 名称含 `direct`/`warp` 会自动打出口标签,线路矩阵按此归位(琥珀 = 直连,青 = WARP)。 +保留完整出口词;客户端没有 Web 的出口标签。新节点 sortOrder 保持默认 0,All Nodes/🧠 AI Nodes 从同一来源按完整名称升序收纳。名字决定地区正则,region 填站点码用于 Web 分段,详见 [新增地区](ADDING-REGIONS.md)。 + ## 导入后核对 - 「节点」页线路矩阵:每个站点的协议 × 出口格子是否齐全 - 「策略组」页:地区组的「命中」行是否圈中了预期节点 - 「订阅」页预览:Surge 端确认 SS over WS 节点已被自动屏蔽(预期行为,Surge 不支持该组合) +- 保留已有停用状态;当前 RN、SS、HY2 的停用记录不通过导入或整理清单恢复。明确授权启用后再分别核对各端节点范围、UDP 能力、动态组引用与实际连接。 +- 生产写入前在线备份,完成后 API 重读及真实 HTTPS 订阅校验,按 [Surge 变更验证](SURGE-CHANGES.md)执行;解析/导入成功不能代替设备连接验证。 diff --git a/docs/SURGE-AUDIT-2026-10-03.md b/docs/SURGE-AUDIT-2026-10-03.md index 31a4cc5..13ba457 100644 --- a/docs/SURGE-AUDIT-2026-10-03.md +++ b/docs/SURGE-AUDIT-2026-10-03.md @@ -1,6 +1,6 @@ # Surge macOS 订阅体检:2026-10-03 -本文保留修复前的体检结论;其中行号指体检时的源码与订阅快照。已实施的代码/生产修复和后续验证见 [修复记录](SURGE-FIX-2026-10-03.md)。 +本文保留修复前的体检结论;其中行号、缺陷与数量指体检时的源码与订阅快照。已实施的代码/生产修复和后续验证见 [修复记录](SURGE-FIX-2026-10-03.md),最新盘点见 [当前生产状态](CURRENT-STATE.md)。不要把下文历史缺陷与快照当作当前状态。 实际订阅通过 Surge 6.9.1 的原生语法校验,生产数据与当前生成器一致。但存在规则被遮蔽、DNS 与连接策略不一致,以及管理 API 能接受最终导致无效配置或丢失规则的数据操作。 @@ -104,7 +104,7 @@ Loop reference is found in the policy group: A. 修复建议:在能力层拒绝无法表达的旧协议组合,或按官方参数转换;不能让用户输入被静默忽略。所有输入字段都应有明确的支持/转换/拒绝行为。 -## 当前生产配置中的行为问题 +## 体检时生产配置中的行为问题 ### D1 · P1:两条例外规则被更早的拦截集覆盖 diff --git a/docs/SURGE-CHANGES.md b/docs/SURGE-CHANGES.md index b477941..5adc39a 100644 --- a/docs/SURGE-CHANGES.md +++ b/docs/SURGE-CHANGES.md @@ -2,9 +2,12 @@ 2026-10-03 的缺陷与证据见 [体检报告](SURGE-AUDIT-2026-10-03.md)。后续节点、组、规则和设置变更使用本文件的检查;具体 API 与生产备份操作见 update-db 技能。 +最新节点启用范围、组成员、名称排序与规则优先级见 [当前生产状态](CURRENT-STATE.md)。历史验收成功不能替代新一轮 API 重读;本机模块和已保存策略选择也不等同于数据库配置。 + ## 写入前 - 获取最新全量节点、组、规则和设置,明确改动对象及受影响订阅范围。保留策略选择和出口语义;名称正则、节点范围、客户端排除不能只按数据库行数检查。 +- 分别核对节点登记总量、enabled、订阅 nodeIds 和客户端能力过滤。当前停用记录继续保留;All Nodes/AI Nodes 使用同一节点来源和独立选择,排序调整先核对 sortOrder,再看完整名称。 - 节点/组名称不能包含配置分隔符、控制字符或与内置策略及另一类实体冲突。导入预览先验证字段;组更新替换完整 members,校验存在性、成员类型及完整图无环。 - 保住唯一启用、末尾 FINAL;完整排序,不通过停用/删除 FINAL 处理兜底。不用 reset 调整生产;reset 的真实含义是事务内恢复模板并保留自定义规则。 - 用官方规则顺序检查业务例外与广告/服务规则集的遮蔽,协议规则不放在提前覆盖其目标的域名规则后。IP 黑名单的 `no-resolve` 会改变未解析目标的拦截覆盖,要记录取舍。 @@ -24,10 +27,18 @@ python3 scripts/check-surge.py /private/tmp/.conf --fetch-rulesets 脚本先调用本机官方 `surge-cli --check`,再检查 FINAL、无效组级 url、公开控制口令和 MITM 参数。第二条下载当前规则集,以临时本地快照再次调用官方 CLI,并提示更早不同策略对后面域名规则样例的覆盖。提示需要结合业务审阅,不能自动将所有广告域名放行。它不模拟 IP/协议/进程或客户端缓存,规则集非法行也可能被 Surge 跳过;有疑点时查看设备资源加载警告及请求匹配日志。 -生产变更先在线备份,经管理 API 逐条核对状态,失败停止;在新代码防护部署后再写需要新防护的数据。写入后重读变更对象、核对数量与完整顺序,从实际 HTTPS 入口拉订阅并核对 MANAGED-CONFIG 的 self URL,然后重复以上校验。通过回环 API 拉的订阅可能带内网 self URL,不能拿它替代实际 HTTPS 验收。配置中的 token、密码、UUID、Host IP 不落仓库或共享日志。 +生产变更先在线备份,确认本次文件为 root 属主、600 权限;不要仅因脚本退出成功就假定权限正确。经管理 API 逐条核对状态,失败停止;在新代码防护部署后再写需要新防护的数据。组成员整体替换,排序使用完整且无重复的 ids;节点权重更新以私有文件保留全部原字段,重读排除排序/更新时间后比对,避免改变协议或凭据。写入后重读变更对象、核对数量与完整顺序,从实际 HTTPS 入口拉订阅并核对 MANAGED-CONFIG 的 self URL,然后重复以上校验。通过回环 API 拉的订阅可能带内网 self URL,不能拿它替代实际 HTTPS 验收。配置中的 token、密码、UUID、Host IP 不落仓库或共享日志。 -Clash 受影响时,用独立临时目录运行 `mihomo -t -d <临时目录> -f <配置>`;不要覆盖正在使用的配置或启动代理服务。没有客户端 CLI 的产物用对应单测和结构检查,明确尚缺的真机验收。 +下载使用 `curl -fsSL --max-time ...`,完成后检查文件存在、非空、预期段落或 YAML 结构,再运行原生校验。串行验证遇错停止;如果管道会隐藏前一步失败,启用 pipefail 或分别读取返回码。mihomo 在配置路径不存在时可能创建默认配置并报告成功,这种输出不能算真实订阅通过。 -不主动切换活动 Surge 配置、修改保存的组选择或导入 CA。CLI 成功只证明解析;节点连接、测速结果、DNS/WebRTC 和业务连接仍需设备侧验证。 +Clash 受影响时,用独立临时目录运行 `mihomo -t -d <临时目录> -f <配置>`;不要覆盖正在使用的配置或启动代理服务。没有客户端 CLI 的产物用对应单测和结构检查,明确尚缺的真机验收。HTTP 正文不包含 excluded/skippedRules,需将启用节点与实际产物逐项比对;配置规则条数也会受派生规则和客户端跳过类型影响。 + +Shadowrocket/Clash 的完整差异与待验项见 [2026-10-04 体检](MOBILE-CLIENT-AUDIT-2026-10-04.md)。除解析外还要检查节点完整名称顺序、UDP 能力、动态组显式成员、RULE-SET 的 no-resolve 和实际跳过规则。Shadowrocket 官方更新记录确认存在逻辑规则,不能把生成器的保守跳过范围当成客户端能力断言。 + +默认不切换活动 Surge 配置、修改保存的组选择或导入 CA。用户授权的本机复测需要更新既有托管配置时,先核对托管 URL 与当前配置,备份并读取最新组选择,再执行 `surge-cli managed-profile update`。该命令可能仅报告已安排重载;须随后读取 `dump profile effective` 和 `policy-group get` 确认真正生效,不把紧接更新的旧规则查询当作最终结果。 + +组改名时 API 会级联生产引用,但客户端保存选择按组身份存储。核对新组、父组路径和两个独立节点组的最新实际选择;仅迁移原选定节点到新组名,不借验证更换线路。用 `profile diff` 区分原订阅与模块覆盖;数据库 url-test 与设备有效 Smart 等类型可能不同,不直接用客户端覆盖修改数据库。 + +CLI 成功只证明解析;节点连接、测速结果、DNS/WebRTC 和业务连接仍需设备侧验证。Net.Coffee 的已完成复测限定于本机 Chrome+Surge 的随机探测域名路径,详见 [DNS 复测](NET-COFFEE-DNS-2026-10-04.md),不能代替其他设备或 Claude 全部业务请求的验收。 依据:[匹配与 DNS](https://manual.nssurge.com/rules/overview.html)、[FINAL](https://manual.nssurge.com/rules/final.html)、[成员导入](https://manual.nssurge.com/policy-groups/policy-including.html)、[测速](https://manual.nssurge.com/policy-groups/url-test.html)、[Host](https://manual.nssurge.com/dns/local-dns-mapping.html)、[引用格式](https://manual.nssurge.com/profile/format.html)、[CLI](https://manual.nssurge.com/tools/cli.html)。 diff --git a/docs/SURGE-FIX-2026-10-03.md b/docs/SURGE-FIX-2026-10-03.md index 060dc8a..822a002 100644 --- a/docs/SURGE-FIX-2026-10-03.md +++ b/docs/SURGE-FIX-2026-10-03.md @@ -1,6 +1,6 @@ # Surge 订阅修复与验证:2026-10-03 -本次完成 [体检报告](SURGE-AUDIT-2026-10-03.md) 中已确认缺陷的代码修复和生产定向调整。原体检报告保留为修复前证据,后续变更流程见 [Surge 变更验证](SURGE-CHANGES.md)。 +本次完成 [体检报告](SURGE-AUDIT-2026-10-03.md) 中已确认缺陷的代码修复和生产定向调整。原体检报告保留为修复前证据,本文数量与验收属于修复当日;最新盘点见 [当前生产状态](CURRENT-STATE.md),后续变更流程见 [Surge 变更验证](SURGE-CHANGES.md)。 ## 修复结果 diff --git a/docs/SURGE-PRIVACY.md b/docs/SURGE-PRIVACY.md index c777898..a5e7c5c 100644 --- a/docs/SURGE-PRIVACY.md +++ b/docs/SURGE-PRIVACY.md @@ -1,5 +1,13 @@ # Surge AI 域名 DNS 分流 +当前数据库与实际 HTTPS 产物、节点/组选择链见 [生产状态](CURRENT-STATE.md)。AI DNS 由生成器派生,检查时区分数据库 Surge 原文、最终订阅及设备模块后的有效配置。 + +Claude/ChatGPT 的精确补充域名、广告拦截优先级例外、官方 inbound IP 兜底与不采用全局 NTP 分流的理由,见 [2026-10-04 AI 规则审阅](AI-RULE-REVIEW-2026-10-04.md)。该次补充保留已有 AI 规则集;新增域名同样由下述流水线派生 DNS 映射。 + +同日后续已将全部启用的 AI 规则集中排到广告及普通分流之前,按用户选择保留 STUN 阻断和 Google 更新/翻译直连例外。Unbreak 仍在 AI 之前,生成的 Host 映射也保持该顺序;手写 Host 未改动。具体排序、验证及限制见 [AI 与广告优先级](AI-PRIORITY-2026-10-04.md)。策略组显示顺序不决定连接规则或 DNS 匹配优先级。 + +Net.Coffee 的 Claude 页面通过随机 `*.d.ip.net.coffee` 子域名检测 DNS,而不是查询 Claude 域名。线上已按用户要求加入 `DOMAIN-SUFFIX,ip.net.coffee,🤖 AI`;精确 DOMAIN 根域规则无法覆盖这些探测子域。页面与探测子域名采用 AI 连接及派生 DNS 映射,本机更新同一托管订阅后,由中国电信 DNS「可能泄露」变为美国 Cloudflare「未检测到泄露」。页面检测权威 DNS 观察到的递归解析器,并按地区判断,不要求解析器 IP 等于代理出口 IP;无返回结果也不能证明无泄漏。检测机制、备份与验收边界见 [Net.Coffee DNS 复测](NET-COFFEE-DNS-2026-10-04.md)。 + 默认 DNS 基于 https://github.com/ClashConnectRules/Surge 的 Surge.conf:阿里、腾讯 DoH 与原来的国内 Host 映射保持直连;不再将全局 DNS 送到 AI,也不再新增全端口 53 接管。 Surge 生成器从启用的 AI 策略规则自动生成 Host 映射。组名支持 `AI` 和 `🤖 AI`。DOMAIN 精确匹配;DOMAIN-SUFFIX 同时生成根域与子域匹配;DOMAIN-KEYWORD 生成通配符;RULE-SET / DOMAIN-SET 直接绑定相同远程列表,由 Surge 更新,无需维护国内域名集合。IP、进程及复合规则无法直接转换为域名 DNS 映射,不生成。Host 与连接规则是独立匹配系统,复杂优先级或例外可能不同。 @@ -14,6 +22,8 @@ DOMAIN,dns.alidns.com,DIRECT AI Host 映射使用 `server:https://1.1.1.1/dns-query`。手写 Host 项在前,保留精确 DNS 例外;随后按连接规则顺序生成至最后一条 AI 域名规则的映射,先前非 AI 域名/规则集使用 General 的普通 DNS。这样 Unbreak 等先前例外不会被后面的宽泛 AI 映射覆盖。选择其他 AI 线路后 Cloudflare 查询也跟随同一策略组。该 IP 规则也会将访问 1.1.1.1 的其他连接交给 AI 组。 +AI 可以选择地区组或独立的 `🧠 AI Nodes`;后者与 All Nodes 的节点来源相同、保存选择独立,详见 [AI Nodes](AI-NODES-2026-10-04.md)。2026-10-04 数据库 General 原文中的 encrypted-dns-follow-outbound-mode 仍为 false,生成器将最终配置派生为 true;不需要为这个差异重复修改全局 DNS 设置。 + 本功能控制 Surge 自己执行的 DNS 查询。代理目标域名可能由远端代理解析;官方说明 use-local-host-item-for-proxy 仅适用于 IP 映射,不适用于 server: 映射,因此不能声称远端代理的解析器被改为 Cloudflare。浏览器自带 DoH 也不自动按 AI 域名匹配这些 Host 项。 代理服务器域名的解析可能触发加密 DNS 循环依赖,需核对 Surge 日志。线上保留已验证的节点 Host IP 映射供引导使用;节点 IP 变化后更新映射,私有节点 IP 不提交仓库。 diff --git a/edge-nodes/README.md b/edge-nodes/README.md index bfb0097..cac460c 100644 --- a/edge-nodes/README.md +++ b/edge-nodes/README.md @@ -34,6 +34,8 @@ edge-nodes/ 站点信息是迁移时的工作记录;维护前仍需读取私有交付文档并核对实际机器。`rn-edge-node-docs/` 的旧链接不作为当前 RN 节点导入来源。 +应用内的登记/启用节点与舰队交付线路是不同统计范围。最新应用盘点为 30 个登记节点、12 个启用、18 个停用,见 [当前生产状态](../docs/CURRENT-STATE.md);停用不表示远端线路故障,维护节点或生成交付页不自动恢复应用开关。 + 2026-10-03 四台现役节点已统一使用 IPv4:系统接口禁用 IPv6,代理层拒绝 IPv6 目标(含 WARP),Cloudflare 不保留节点 AAAA。后续重建与扩展遵守 [IPv4-only 维护约定](../docs/EDGE-IPV4-ONLY.md);具体备份与验收在各站点的私有 `04-IPv4-only.md`。 ## 日常工作 diff --git a/server/src/services/generators/clash.ts b/server/src/services/generators/clash.ts index 4fba4f3..d0f3843 100644 --- a/server/src/services/generators/clash.ts +++ b/server/src/services/generators/clash.ts @@ -1,15 +1,21 @@ import YAML from 'yaml'; -import { clientSupports, normalizeSsCipher, type ExcludedNode, type NodeDto } from '@proxy-station/shared'; -import type { ProfileIR, RuleIR } from '../ir.js'; +import { clientSupports, normalizeSsCipher, splitRuleExpression, type ExcludedNode, type NodeDto } from '@proxy-station/shared'; +import type { ProfileIR } from '../ir.js'; export function clashProxy(n: NodeDto): Record { - const base: Record = { name: n.name, server: n.server, port: n.port }; + const base: Record = { + name: n.name, server: n.server, port: n.port, + // SIP003 的 WS 插件只转发 TCP,不能把 UDP 发往 nginx 的同端口。 + udp: !(n.protocol === 'shadowsocks' && n.network === 'ws'), + }; switch (n.protocol) { case 'vmess': { - Object.assign(base, { type: 'vmess', uuid: n.uuid, alterId: n.alterId, cipher: n.security || 'auto' }); + const cipher = n.security === 'chacha20-ietf-poly1305' ? 'chacha20-poly1305' : n.security || 'auto'; + Object.assign(base, { type: 'vmess', uuid: n.uuid, alterId: n.alterId, cipher }); if (n.tls) { base.tls = true; if (n.sni) base.servername = n.sni; + if (n.skipCertVerify) base['skip-cert-verify'] = true; } if (n.network === 'ws') { base.network = 'ws'; @@ -42,6 +48,7 @@ export function clashProxy(n: NodeDto): Record { tls: n.tls, host: n.wsHost || n.sni || n.server, path: n.wsPath, + ...(n.skipCertVerify ? { 'skip-cert-verify': true } : {}), }; } break; @@ -68,9 +75,39 @@ function mapPolicy(policy: string): string { return policy; } -/** Surge 逻辑规则里的子类型 → mihomo 命名 */ -function mapLogicValue(value: string): string { - return value.replace(/\bPROTOCOL\b/g, 'NETWORK'); +/** 逐个转换逻辑子规则;任何不可表达的子条件使整条规则跳过,避免扩大匹配范围。 */ +function mapLogicValue(value: string): string | null { + value = value.trim(); + if (!value.startsWith('(') || !value.endsWith(')')) return null; + const children = splitRuleExpression(value.slice(1, -1)); + if (!children?.length) return null; + const mapped: string[] = []; + for (const child of children) { + if (!child.startsWith('(') || !child.endsWith(')')) return null; + const parts = splitRuleExpression(child.slice(1, -1)); + if (!parts || parts.length < 2) return null; + let [type, payload] = parts; + if (['AND', 'OR', 'NOT'].includes(type)) { + const nested = mapLogicValue(payload); + if (!nested) return null; + payload = nested; + } else if (type === 'PROTOCOL') { + if (payload !== 'TCP' && payload !== 'UDP') return null; + type = 'NETWORK'; + } else { + const types: Record = { + DOMAIN: 'DOMAIN', 'DOMAIN-SUFFIX': 'DOMAIN-SUFFIX', 'DOMAIN-KEYWORD': 'DOMAIN-KEYWORD', + 'DOMAIN-WILDCARD': 'DOMAIN-WILDCARD', 'IP-CIDR': 'IP-CIDR', 'IP-CIDR6': 'IP-CIDR6', + 'IP-ASN': 'IP-ASN', GEOIP: 'GEOIP', 'PROCESS-NAME': 'PROCESS-NAME', + 'SRC-IP': 'SRC-IP-CIDR', 'DEST-PORT': 'DST-PORT', 'SRC-PORT': 'SRC-PORT', + }; + if (!types[type] || /["'\\]/.test(payload)) return null; + type = types[type]; + } + if (parts.slice(2).some((p) => p !== 'no-resolve')) return null; + mapped.push(`(${[type, payload, ...parts.slice(2)].join(',')})`); + } + return `(${mapped.join(',')})`; } const CLASH_UNSUPPORTED_RULE_TYPES = new Set(['URL-REGEX', 'USER-AGENT']); @@ -100,17 +137,17 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }): if (g.filterRegex) { group['include-all-proxies'] = true; group.filter = g.filterRegex; + if (members.length) group.proxies = [...new Set(members)]; } else if (g.includeAllNodes) { group['include-all-proxies'] = true; - const extra = members.filter((m) => !usableNames.has(m)); - if (extra.length) group.proxies = extra; + if (members.length) group.proxies = [...new Set(members)]; } else { group.proxies = members.length ? members : ['DIRECT']; } if (g.type !== 'select') { group.url = g.testUrl || 'http://cp.cloudflare.com/generate_204'; group.interval = g.interval ?? 300; - if (g.tolerance) group.tolerance = g.tolerance; + if (g.type === 'url-test' && g.tolerance !== null) group.tolerance = g.tolerance; } return group; }); @@ -158,7 +195,7 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }): } const policy = mapPolicy(r.policy); if (r.ruleset) { - ruleLines.push(`RULE-SET,${providerNameByRulesetId.get(r.ruleset.id)},${policy}`); + ruleLines.push(['RULE-SET', providerNameByRulesetId.get(r.ruleset.id), policy, ...r.params.filter((p) => p === 'no-resolve')].join(',')); continue; } // Surge 内置规则集(非 URL):LAN → 私网 GEOIP;SYSTEM 无对应物,跳过 @@ -171,7 +208,9 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }): continue; } if (r.type === 'AND' || r.type === 'OR' || r.type === 'NOT') { - ruleLines.push(`${r.type},${mapLogicValue(r.value || '')},${policy}`); + const value = mapLogicValue(r.value || ''); + if (value) ruleLines.push([r.type, value, policy, ...r.params.filter((p) => p === 'no-resolve')].join(',')); + else skippedRules.push(`${r.type},${r.value},${r.policy}`); continue; } const params = r.params.filter((p) => p === 'no-resolve'); diff --git a/server/src/services/generators/mobile-clients.test.ts b/server/src/services/generators/mobile-clients.test.ts new file mode 100644 index 0000000..05da042 --- /dev/null +++ b/server/src/services/generators/mobile-clients.test.ts @@ -0,0 +1,148 @@ +import { afterAll, describe, expect, it } from 'vitest'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import YAML from 'yaml'; +import type { NodeDto } from '@proxy-station/shared'; +import type { GroupIR, ProfileIR, RuleIR } from '../ir.js'; +import { generateClash, clashProxy } from './clash.js'; +import { generateShadowrocket } from './shadowrocket.js'; +import { generateShadowrocketConf } from './shadowrocket-conf.js'; +import { parseShareLink } from '../sharelink/parse.js'; + +const node: NodeDto = { + id: 'placeholder', name: '🇯🇵 JP VMess Direct', protocol: 'vmess', + server: 'node.example.com', port: 443, uuid: '00000000-1111-2222-3333-444444444444', + password: 'placeholder:password', method: 'chacha20-poly1305', network: 'ws', + tls: true, sni: 'node.example.com', skipCertVerify: false, wsHost: 'node.example.com', wsPath: '/ws', + alterId: 0, security: 'auto', obfsType: null, obfsPassword: null, upMbps: null, downMbps: null, + region: null, tags: [], enabled: true, sortOrder: 0, createdAt: 0, updatedAt: 0, +}; +const group: GroupIR = { + name: 'Filtered', type: 'url-test', testUrl: 'https://example.com/generate_204', + interval: 300, tolerance: 0, filterRegex: '^🇯🇵', includeAllNodes: true, + members: [{ kind: 'builtin', name: 'DIRECT' }, { kind: 'node', name: node.name }, { kind: 'group', name: 'Source' }], +}; +const final: RuleIR = { type: 'FINAL', value: null, policy: 'DIRECT', params: ['dns-failed'], ruleset: null }; +const base: ProfileIR = { nodes: [node], groups: [], rules: [final], sections: {} }; +const generate = (ir: ProfileIR) => generateClash(ir, { convertBaseUrl: 'https://example.com/sub/placeholder' }); +const directory = mkdtempSync(path.join(tmpdir(), 'proxy-station-mobile-')); +const cli = process.env.MIHOMO_CLI ?? 'mihomo'; +const nativeAvailable = spawnSync(cli, ['-v'], { timeout: 10000 }).status === 0; +afterAll(() => rmSync(directory, { recursive: true, force: true })); + +describe('mobile client node subscriptions', () => { + it('sorts complete Shadowrocket names by string, retains all four protocols and does not mutate IR', () => { + const names = ['🇸🇬 SG VMess Warp', '🇯🇵 JP Trojan Warp', '🇺🇸 US LA SS Direct', + '🇸🇬 SG VMess Direct', '🇯🇵 JP HY2 Direct', '🇯🇵 JP Trojan Direct']; + const protocols: NodeDto['protocol'][] = ['vmess', 'trojan', 'shadowsocks', 'vmess', 'hysteria2', 'trojan']; + const nodes = names.map((name, i) => ({ ...node, name, protocol: protocols[i], + ...(protocols[i] === 'hysteria2' ? { network: 'tcp' as const, wsHost: null, wsPath: null, + obfsType: 'salamander', obfsPassword: 'placeholder-obfs' } : {}) })); + const output = generateShadowrocket({ ...base, nodes }); + const uris = Buffer.from(output.content, 'base64').toString('utf8').split('\n'); + expect(uris).toEqual(output.uris); + expect(uris.map((uri) => parseShareLink(uri).name)).toEqual([...names].sort()); + expect(nodes.map((n) => n.name)).toEqual(names); + expect(output.excluded).toEqual([]); + for (const uri of uris) { + const parsed = parseShareLink(uri); + const original = nodes.find((n) => n.name === parsed.name)!; + expect(parsed).toMatchObject({ name: original.name, protocol: original.protocol, + server: original.server, port: original.port, network: original.network, + tls: original.tls, sni: original.sni }); + if (original.protocol === 'vmess') expect(parsed.uuid).toBe(original.uuid); + else expect(parsed.password).toBe(original.password); + if (original.protocol === 'hysteria2') expect(parsed.obfsPassword).toBe(original.obfsPassword); + } + }); + + it('enables supported Clash UDP transports, keeps SS WS TCP-only, and preserves TLS flags', () => { + expect(clashProxy({ ...node, skipCertVerify: true })).toMatchObject({ type: 'vmess', udp: true, + tls: true, servername: node.sni, 'skip-cert-verify': true, 'ws-opts': { headers: { Host: node.wsHost } } }); + expect(clashProxy({ ...node, protocol: 'trojan' })).toMatchObject({ type: 'trojan', udp: true, sni: node.sni }); + expect(clashProxy({ ...node, protocol: 'shadowsocks', skipCertVerify: true })).toMatchObject({ + type: 'ss', udp: false, cipher: 'chacha20-ietf-poly1305', plugin: 'v2ray-plugin', + 'plugin-opts': { mode: 'websocket', tls: true, path: '/ws', 'skip-cert-verify': true }, + }); + expect(clashProxy({ ...node, protocol: 'shadowsocks', network: 'tcp' }).udp).toBe(true); + expect(clashProxy({ ...node, protocol: 'hysteria2', network: 'tcp', obfsType: 'salamander', + obfsPassword: 'placeholder-obfs', upMbps: 20, downMbps: 100 })).toMatchObject({ type: 'hysteria2', + udp: true, obfs: 'salamander', 'obfs-password': 'placeholder-obfs', up: '20 Mbps', down: '100 Mbps' }); + }); + + it('maps the VMess cipher alias and reports unsupported nodes without leaving dangling group references', () => { + expect(clashProxy({ ...node, security: 'chacha20-ietf-poly1305' }).cipher).toBe('chacha20-poly1305'); + const result = generate({ ...base, nodes: [{ ...node, security: 'unsupported-placeholder' }], + groups: [{ ...group, filterRegex: null, includeAllNodes: false, members: [{ kind: 'node', name: node.name }] }] }); + const doc = YAML.parse(result.content); + expect(result.excluded).toEqual([{ name: node.name, reason: 'Clash 不支持该 VMess 加密方式:unsupported-placeholder' }]); + expect(doc.proxies).toEqual([]); + expect(doc['proxy-groups'][0].proxies).toEqual(['DIRECT']); + }); +}); + +describe('mobile client groups and rule semantics', () => { + it('preserves explicit members in dynamic groups, zero tolerance, and omits fallback tolerance', () => { + const source = { ...group, name: 'Source', filterRegex: null, includeAllNodes: false, + members: [{ kind: 'node' as const, name: node.name }] }; + const all = { ...group, name: 'All', filterRegex: null }; + const fallback = { ...group, name: 'Fallback', type: 'fallback' as const, tolerance: 50 }; + const ir = { ...base, groups: [source, group, all, fallback] }; + const doc = YAML.parse(generate(ir).content); + expect(doc['proxy-groups'][1]).toMatchObject({ proxies: ['DIRECT', node.name, 'Source'], + 'include-all-proxies': true, filter: group.filterRegex, tolerance: 0, url: group.testUrl }); + expect(doc['proxy-groups'][2].proxies).toEqual(['DIRECT', node.name, 'Source']); + expect(doc['proxy-groups'][3]).not.toHaveProperty('tolerance'); + const sr = generateShadowrocketConf(ir).content; + expect(sr).toContain(`Filtered = url-test, DIRECT, ${node.name}, Source, url=${group.testUrl}, interval=300, tolerance=0, policy-regex-filter=^🇯🇵`); + expect(sr.split('\n').find((l) => l.startsWith('Fallback ='))).not.toContain('tolerance='); + expect(sr).not.toContain('use='); + }); + + it('preserves provider no-resolve, handles duplicate provider names, keeps rule order and FINAL mapping', () => { + const ruleset = { id: 'first', name: 'same', surgeUrl: 'https://example.com/ip.list', clashUrl: null, + clashBehavior: 'classical', clashFormat: 'yaml', needsConvert: true }; + const rules: RuleIR[] = [ + { type: 'RULE-SET', value: 'first', policy: 'REJECT-DROP', params: ['no-resolve'], ruleset }, + { type: 'DOMAIN', value: 'example.com', policy: 'DIRECT', params: [], ruleset: null }, + { type: 'RULE-SET', value: 'second', policy: 'DIRECT', params: [], ruleset: { ...ruleset, id: 'second' } }, final, + ]; + const doc = YAML.parse(generate({ ...base, rules }).content); + expect(doc.rules).toEqual(['RULE-SET,same,REJECT-DROP,no-resolve', 'DOMAIN,example.com,DIRECT', + 'RULE-SET,same-2,DIRECT', 'MATCH,DIRECT']); + expect(doc['rule-providers']['same-2'].url).toBe('https://example.com/sub/placeholder/ruleset/second.yaml'); + }); + + it('converts only transport subrules, preserves payload literals and skips whole unsupported logical expressions', () => { + const values = ['((PROTOCOL,UDP),(DOMAIN-SUFFIX,PROTOCOL.example.com))', + '((OR,((PROTOCOL,TCP),(PROTOCOL,UDP))),(DEST-PORT,443))', + '((PROTOCOL,STUN),(DOMAIN,example.com))', '((PROTOCOL,DOH),(DOMAIN,example.com))', + '((URL-REGEX,^https://example.com),(DOMAIN,example.com))', + '((DOMAIN,example.com,extended-matching),(PROTOCOL,UDP))']; + const result = generate({ ...base, rules: [...values.map((value): RuleIR => ({ + type: 'AND', value, policy: 'REJECT-NO-DROP', params: [], ruleset: null, + })), final] }); + expect(YAML.parse(result.content).rules).toEqual([ + 'AND,((NETWORK,UDP),(DOMAIN-SUFFIX,PROTOCOL.example.com)),REJECT', + 'AND,((OR,((NETWORK,TCP),(NETWORK,UDP))),(DST-PORT,443)),REJECT', 'MATCH,DIRECT', + ]); + expect(result.skippedRules).toHaveLength(4); + }); + + it.skipIf(!nativeAvailable)('passes the native mihomo parser with all node protocols, dynamic groups and nested logic', () => { + const nodes: NodeDto[] = [node, { ...node, name: 'Trojan', protocol: 'trojan' }, + { ...node, name: 'SS', protocol: 'shadowsocks' }, + { ...node, name: 'HY2', protocol: 'hysteria2', network: 'tcp', obfsType: 'salamander', obfsPassword: 'placeholder-obfs' }]; + const ir: ProfileIR = { ...base, nodes, groups: [ + { ...group, name: 'Source', filterRegex: null, includeAllNodes: false, members: [{ kind: 'node', name: node.name }] }, group, + ], rules: [{ type: 'AND', value: '((OR,((PROTOCOL,TCP),(PROTOCOL,UDP))),(DOMAIN,example.com))', + policy: 'REJECT', params: [], ruleset: null }, final] }; + const file = path.join(directory, 'fixture.yaml'); + writeFileSync(file, generate(ir).content, { mode: 0o600 }); + const result = spawnSync(cli, ['-t', '-d', directory, '-f', file], { encoding: 'utf8', timeout: 15000 }); + expect(result.stdout + result.stderr).toContain('test is successful'); + expect(result.status).toBe(0); + }); +}); diff --git a/server/src/services/generators/shadowrocket-conf.ts b/server/src/services/generators/shadowrocket-conf.ts index 6321550..2916bcc 100644 --- a/server/src/services/generators/shadowrocket-conf.ts +++ b/server/src/services/generators/shadowrocket-conf.ts @@ -44,7 +44,7 @@ function shadowrocketGeneral(surgeGeneral: string): string { return lines.join('\n'); } -/** ShadowRocket 不认识的规则类型 */ +/** 尚未验证 Shadowrocket 配置语法的类型;这是生成器的保守范围,不是客户端能力断言。 */ const SR_UNSUPPORTED_RULE_TYPES = new Set(['DOMAIN-SET', 'AND', 'OR', 'NOT', 'URL-REGEX', 'PROTOCOL']); export interface ShadowrocketConfOutput { @@ -70,14 +70,12 @@ export function generateShadowrocketConf(ir: ProfileIR): ShadowrocketConfOutput lines.push('', '[Proxy Group]'); for (const g of ir.groups) { const parts: string[] = [g.type]; - if (!g.filterRegex && !g.includeAllNodes) { - const members = g.members.map((m) => policyName(m.name)); - parts.push(...(members.length ? members : ['DIRECT'])); - } + const members = [...new Set(g.members.map((m) => policyName(m.name)))]; + parts.push(...(members.length ? members : !g.filterRegex && !g.includeAllNodes ? ['DIRECT'] : [])); if (g.type !== 'select') { parts.push(`url=${g.testUrl || 'http://cp.cloudflare.com/generate_204'}`); if (g.interval) parts.push(`interval=${g.interval}`); - if (g.tolerance) parts.push(`tolerance=${g.tolerance}`); + if (g.type === 'url-test' && g.tolerance !== null) parts.push(`tolerance=${g.tolerance}`); } // policy-regex-filter 从 App 内全部节点里筛选;「收纳全部节点」用全匹配正则 if (g.filterRegex) parts.push(`policy-regex-filter=${g.filterRegex}`); diff --git a/server/src/services/generators/shadowrocket.ts b/server/src/services/generators/shadowrocket.ts index 3aae084..f8369da 100644 --- a/server/src/services/generators/shadowrocket.ts +++ b/server/src/services/generators/shadowrocket.ts @@ -11,7 +11,9 @@ export interface ShadowrocketOutput { export function generateShadowrocket(ir: ProfileIR): ShadowrocketOutput { const excluded: ExcludedNode[] = []; const uris: string[] = []; - for (const n of ir.nodes) { + // 按完整名称的字符串顺序排列,保留国家、协议和出口后缀;不改 IR 的数据库顺序。 + const sortedNodes = [...ir.nodes].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); + for (const n of sortedNodes) { const verdict = clientSupports('shadowrocket', n); if (!verdict.ok) { excluded.push({ name: n.name, reason: verdict.reason! }); diff --git a/shared/src/index.ts b/shared/src/index.ts index f55423f..5a28e44 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,3 +1,4 @@ export * from './protocols.js'; export * from './schemas.js'; export * from './dto.js'; +export { splitRuleExpression } from './rule-syntax.js'; diff --git a/shared/src/protocols.ts b/shared/src/protocols.ts index 0abd267..d3e367f 100644 --- a/shared/src/protocols.ts +++ b/shared/src/protocols.ts @@ -48,5 +48,9 @@ export function clientSupports(client: ClientType, node: NodeCapabilityInput): C return { ok: false, reason: `Surge 不支持该 VMess 加密方式:${node.security}` }; } } + if (client === 'clash' && node.protocol === 'vmess' + && !['auto', 'none', 'zero', 'aes-128-gcm', 'chacha20-poly1305', 'chacha20-ietf-poly1305'].includes(node.security ?? 'auto')) { + return { ok: false, reason: `Clash 不支持该 VMess 加密方式:${node.security}` }; + } return { ok: true }; } diff --git a/shared/src/rule-syntax.ts b/shared/src/rule-syntax.ts index b254c07..fafc006 100644 --- a/shared/src/rule-syntax.ts +++ b/shared/src/rule-syntax.ts @@ -1,5 +1,5 @@ /** Split structural commas while preserving quoted values and nested expressions. */ -function split(value: string): string[] | null { +export function splitRuleExpression(value: string): string[] | null { const parts: string[] = []; let start = 0, depth = 0, quote = '', escaped = false; for (let i = 0; i < value.length; i++) { @@ -28,11 +28,11 @@ export const SURGE_PROTOCOLS = ['HTTP', 'HTTPS', 'TCP', 'UDP', 'QUIC', 'STUN', ' export function logicalValueValid(operator: string, value: string, depth = 1): boolean { value = value.trim(); if (depth > 10 || !value.startsWith('(') || !value.endsWith(')')) return false; - const children = split(value.slice(1, -1)); + const children = splitRuleExpression(value.slice(1, -1)); if (!children?.length || (operator === 'NOT' && children.length !== 1)) return false; return children.every((child) => { if (!child.startsWith('(') || !child.endsWith(')')) return false; - const parts = split(child.slice(1, -1)); + const parts = splitRuleExpression(child.slice(1, -1)); if (!parts || parts.length < 2 || !parts[1] || parts.slice(2).some((flag) => !['no-resolve', 'extended-matching'].includes(flag))) return false; if (['AND', 'OR', 'NOT'].includes(parts[0])) return logicalValueValid(parts[0], parts[1], depth + 1); return leafTypes.has(parts[0]) && (parts[0] !== 'PROTOCOL' || SURGE_PROTOCOLS.includes(parts[1]));