Ctrl K
ComunidadGuías…

selfhost/README.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Ekko Rules 本地自托管订阅

在自己的电脑上用 Docker Compose 启动前端和转换引擎。输入一次真实订阅,得到可重复刷新的订阅地址。只在电脑使用时可以保持固定的 `localhost`:

```text
http://localhost:8787/sub/<随机 ID>
```

也可以让同一局域网内的手机、平板或路由器使用运行 Docker 的电脑 IP:

```text
http://192.168.1.100:8787/sub/<同一个随机 ID>
```

稳定的是 `/sub/<随机 ID>` 档案路径;前面的访问地址可以在 Web UI 中切换。普通停止、重启或 `docker compose down` 不会改变档案 ID、真实订阅映射和高级选项。

Web 端口默认像常见 Docker 服务一样发布到宿主机所有网卡,所以同一可信局域网可以直接访问;转换引擎端口仍然只存在于 Compose 内部网络。默认部署不应直接暴露到公网。

另一种形态是放到服务器上做**开放转换站**(`SELFHOST_MODE=public`):服务器不保存任何东西,每个访问者在浏览器里拼出自己的链接(`/sub?url=…`),互相看不到对方的内容。代价是真实订阅写在链接里,页面会明确提示不要转发。见 [部署开放转换站](docs/vps.md)。

想让真实订阅留在自己机器上、用固定 `/sub/<随机 ID>` 地址,就用本页这套默认形态——不存在把别人的订阅存在公网服务器上的第三种形态。

## 运行要求

- Docker Desktop,或带 Compose v2 的 Docker Engine;
- 能够从 Docker 拉取基础镜像;
- 默认发布宿主机端口 `8787`,本机和同一可信局域网均可访问;可在 `.env` 中修改绑定地址和端口。

## 快速启动

Windows 首次部署推荐运行 `setup.cmd`。它会启动 Compose,并为当前 Windows 用户注册局域网 IP 登录任务:

```bash
git clone https://github.com/ZaunEkko/ekko-rules.git
cd ekko-rules/selfhost

# Windows:首次运行一次
setup.cmd

# macOS / Linux
sh ./start.sh
```

`setup.cmd` 完成后不需要每次开机再次运行。Compose 中的 Web 与转换器都使用 `restart: unless-stopped`:Docker Desktop 自动恢复容器,Windows 登录任务自动恢复局域网 IP 检测,用户仍然可以在 Docker Desktop 中可视化启动、停止和重启容器。

若启动时电脑尚未联网或暂时识别不到局域网 IP,Compose 仍会正常启动;后台检测器会等待可用网络,并在识别成功后自动刷新 Web UI 中的局域网地址。

局域网地址元数据超过 30 秒未刷新就会视为失效,避免换网或断网后继续复制旧 IP。转换期间的原始订阅只写入 Web 容器的临时内存文件系统,容器停止后不会作为 Docker 数据卷保留。

三个 Windows 入口的区别:

| 入口 | 启动 Docker | 自动识别当前 IP | 以后登录自动恢复检测 |
|---|---|---|---|
| `setup.cmd` | 是 | 是 | **是,只需首次运行一次** |
| `start.cmd` | 是 | 是 | 否,仅当前登录会话 |
| `docker compose up --build -d` | 是 | 容器无法读取 Windows 物理网卡 | 否 |

只需要基础转换、不安装宿主机助手时,可以始终直接运行:

```bash
docker compose up --build -d
```

只有需要修改端口、访问密码等设置时才复制 `.env.example`。已经位于仓库根目录且是 Windows 首次部署时:

```bash
cd selfhost
setup.cmd
```

打开 [http://127.0.0.1:8787](http://127.0.0.1:8787):

1. 粘贴真实订阅地址;
2. 选择目标客户端,按需展开高级选项;
3. 复制固定 URL,或用手机扫描本地生成的二维码后导入客户端。弹窗里可以在「客户端里扫」(二维码内容是原始订阅地址)与「系统相机扫」(内容是客户端自己的导入 scheme)之间切换,默认选中该客户端真正能导入的那一种。两条路径最终都创建同一个可更新的远程订阅。

Web 端口 `8787` 默认发布到宿主机所有网卡,与常见 Docker Web 服务一致;转换引擎的 `25500` 端口仅存在于 Compose 网络。只在本机使用时仍然打开和复制 `localhost` 地址即可。

Clash / Mihomo 完整配置会自动保留源订阅和节点提供者中的 `hosts`。若所有节点都依赖域名别名,使用一条本机节点 DNS 入口;混合订阅或模板带有自定义节点解析策略时,仅为相关域名补齐策略。两种写法都会保留必要的 fake-IP 排除,使用生成配置自身的 DNS 监听地址。没有 `hosts` 的订阅不增加这些别名处理。

默认模板的常规解析、国内域名策略、直连解析与节点默认解析使用国内 DoH;`default-nameserver` 保留 IP 地址用于解析 DoH 服务器自身的域名。

国外域名策略和 fallback 使用 Cloudflare / Google DoH,并通过 `#RULES` 按已有分流规则连接。节点别名的真实入口会自动保留独立引导解析,防止“解析需要代理、代理启动又需要解析”的循环;已有独立的自定义入口解析策略会保留。

## 手机与路由器使用局域网订阅

默认部署已经允许可信局域网访问,不需要重新配置端口。Windows 首次运行 `setup.cmd` 后,登录任务会读取当前默认物理网卡,并在开机登录及网络变化后持续更新 Web UI 中的局域网 IP。手机或路由器可以访问例如:

```text
http://192.168.1.100:8787
```

地址工具位于“本地订阅地址”列表上方。第一次点击 `使用局域网 <当前 IP>` 后,页面会记住“局域网模式”。以后电脑换到新的 Wi-Fi 或有线网络,只要宿主机助手识别到新 IP,页面就会在下一次状态刷新时自动更新所有档案的显示、复制与二维码;`本机 localhost`、`当前访问地址`、自定义前缀和曾用地址不会被自动覆盖。

下面这些环境变量都是可选的:

```dotenv
WEB_BIND_HOST=0.0.0.0
WEB_PORT=8787
LAN_BASE_URL=http://192.168.1.100:8787
ACCESS_PASSWORD=可选的本地管理密码
```

- `WEB_BIND_HOST` 默认已经是 `0.0.0.0`;只有明确希望禁止局域网访问时才改为 `127.0.0.1`。
- `LAN_BASE_URL` 仅用于预设页面显示、复制和二维码中的地址前缀,留空时使用打开 Web UI 的地址;它不参与后端授权或订阅校验。只要某个地址能访问本服务,就可以用该地址拼接同一个 `/sub/<随机 ID>`。
- `ACCESS_PASSWORD` 是可选的管理保护,设置后会保护创建、读取列表和删除操作;手机或路由器访问 `/sub/<随机 ID>` 时不需要额外请求头。开放形态没有管理接口,该变量在那里被忽略。

开放形态额外使用下面这些变量,完整说明见 [docs/vps.md](docs/vps.md) 与 `.env.vps.example`:

```dotenv
SELFHOST_MODE=public
PUBLIC_BASE_URL=https://sub.example.com
TRUST_PROXY_HEADERS=1
RATE_LIMIT_MANAGE_PER_MINUTE=30
RATE_LIMIT_SUBSCRIBE_PER_MINUTE=60
```

- `SELFHOST_MODE=public` 关闭全部档案存储:`/api/profiles` 与 `/sub/<随机 ID>` 一律 404,只剩 `/sub?url=…`;
- `PUBLIC_BASE_URL` 取代 `LAN_BASE_URL` 作为页面显示、复制与二维码的前缀,必须是纯 origin;
- `TRUST_PROXY_HEADERS` 让限速按反向代理给出的真实客户端 IP 统计;
- 两个 `RATE_LIMIT_*` 是每分钟每 IP 的请求上限,设为 `0` 关闭。本地形态默认关闭。

Web UI 提供:

- `本机 localhost`:只在电脑客户端使用时保持最稳定;
- `当前访问地址`:页面通过局域网 IP 打开时直接采用该 IP;
- `使用局域网 <当前 IP>`:立即切换,并在此后自动跟随宿主机探测到的新 IP;
- 自定义前缀:在家里、公司等网络切换后直接输入新的电脑 IP;
- 曾用地址:在当前浏览器保留最近 8 个前缀,一键切换所有档案的显示、复制与二维码。

**Shadowrocket 使用两步原生导入。** 客户端把首页节点订阅和原生配置保存为两个独立对象,无法由一条地址同时获得两者全部能力。页面因此同时生成具名 `.yaml` 与 `.conf`:先在首页导入节点订阅,获得名称、节点刷新、流量/到期横幅和唯一一份节点定义;再在「配置」页导入原生 `.conf`,由内置 `PROXY` 默认调用首页当前节点,并在策略组里按名称引用首页节点,保留规则、`♻️ 手动切换`、`DIRECT` / `REJECT`、策略组嵌套和默认选择。节点增删或改名后还需刷新配置。页面的一键导入和二维码都按 ①/② 标明顺序,两步缺一不可。实机回归与最终取舍见 [Shadowrocket 导入记录](docs/shadowrocket-import.md)。

二维码放 `/i` 地址,由它按来客作答:

- **客户端来拉** → 返回配置,与 `/sub` 逐字节相同;
- **浏览器来开**(系统相机扫到后打开的就是浏览器)→ 返回一个极简页面,自动跳客户端的导入 scheme,并附一个按钮(Safari 常常要点一下)、完整地址与「直接下载配置」。

判定条件是三者同时成立:`Sec-Fetch-Mode: navigate`、`Accept` 含 `text/html`、`User-Agent` 以 `Mozilla/5.0` 开头。代理客户端不会同时具备这三样;判错的代价是给客户端发了 HTML,所以宁可漏判成客户端。

二维码使用的远程地址把转换参数打包为 base64url 参数(通常是 `/i?p=…`),避免嵌套 scheme 时丢失机场 token。Shadowrocket 分别使用 `/i/<名称>.yaml?srhome=1&p=…` 和 `/i/<名称>.conf?p=…`;文件名决定客户端中的显示名称,内部 `p` 参数仍是 base64url。第一个地址只交给首页节点订阅,第二个地址只交给配置页,两者不会互相冒充。

**一键导入按钮**在手机上直接访问站点时最省事,覆盖各家自己公开的 scheme:Clash / Mihomo `clash://install-config?url=`、sing-box `sing-box://import-remote-profile?url=…#名称`、Surge `surge:///install-config?url=`、Loon `loon://import?sub=`、Surfboard `surfboard:///install-config?url=`。Shadowrocket 单独提供两个按钮:① `shadowrocket://add/<具名 .yaml 地址>` 建立首页节点订阅;② `shadowrocket://config/add/<原生 .conf 地址>` 建立分流配置。对应二维码保持普通 HTTPS 地址,分别交给首页和配置页扫码器。Quantumult X 的 `update-configuration` 只接受远程资源而不是整份配置,Quantumult 与 Mellow 没有公开 scheme,这三个只给复制地址。

换网络不会改变 `/sub/<随机 ID>`,Web UI 与二维码会在局域网模式下自动显示新 IP;但已经导入手机或路由器的旧 URL 无法跨网络自己修改主机部分。到达新网络后,请用自动更新后的二维码重新导入一次,或只修改客户端中的地址前缀。若希望完全避免修改,可在路由器中为电脑设置固定 DHCP 地址、使用可靠的局域网主机名,或使用 Tailscale 等具有稳定地址的虚拟局域网。

Docker Desktop 通常会直接完成端口发布,就像其他 Compose Web 服务一样。如果手机打不开电脑 IP,先确认两台设备位于同一局域网且 Windows 当前网络属于“专用网络”;只有仍被拦截时,才需要在系统防火墙中允许 Docker Desktop 或 TCP `8787`。不要在路由器上把该端口转发到公网。

自动识别不从浏览器或容器猜测 IP:容器只能看到 Docker 虚拟网卡,现代浏览器也不会可靠暴露本机地址。Windows 登录任务在宿主机读取实际默认路由,把结果写入忽略版本控制的 `.runtime/lan-address.json`;它不启动网络服务、不额外监听端口、不读取真实订阅,也不会把地址发送到外部。若直接运行 `docker compose up`,页面仍可通过当前访问地址或自定义前缀正常使用。

不再需要自动识别时,双击:

```text
uninstall-helper.cmd
```

它只取消 Windows 登录任务并停止当前 IP 检测器,不停止 Docker、不删除固定订阅和数据卷。之后仍可继续用 Docker Desktop 或 `docker compose up -d` 管理服务。仓库移动到新目录后,应在新目录重新运行一次 `setup.cmd`,让登录任务使用新路径。

常用管理命令:

| 操作 | 命令 | 固定地址是否保留 |
|---|---|---|
| 查看状态 | `docker compose ps` | 是 |
| 查看日志 | `docker compose logs -f web subconverter` | 是 |
| 停止服务 | `docker compose down` | 是 |
| 更新并重建 | `docker compose up --build -d` | 是 |
| 删除服务和全部本地档案 | `docker compose down -v` | **否,无法恢复** |

### 开机后 Docker 已运行,但页面没有新的局域网 IP

先确认当前 Windows 用户至少成功运行过一次 `setup.cmd`。该命令会注册名为 `Ekko Rules LAN address watcher` 的当前用户登录任务;它与 Docker 容器相互独立,因此通过 Docker Desktop 重启容器不会移除检测器。仓库被移动或登录任务被系统清理后,在当前目录重新运行一次 `setup.cmd` 即可更新任务路径。

## 固定地址怎样更新

本地 URL 是可重复请求的转换入口,不是一次性缓存文件。

```text
客户端刷新 /sub/<随机 ID>
        ↓
本地服务重新拉取真实订阅
        ↓
转换成完整配置并立即返回
```

- Docker 停止时,客户端已经导入的现有配置仍可继续使用;
- 重新启动 Docker 后,同一个本地 URL 可以再次更新;
- `docker compose down` 会保留固定地址;
- `docker compose down -v` 会删除保存地址的 Docker 数据卷。

## 输出格式

所有入口都生成带 Ekko Rules 的完整配置,而不只是孤立节点列表。

| 输出 | 常见客户端 | 文件 |
|---|---|---|
| Clash / Mihomo | Clash Verge Rev、Mihomo Party、FlClash | YAML |
| Shadowrocket | Shadowrocket | CONF |
| sing-box | sing-box、SFI、SFA | JSON |
| Surge 4+ | Surge for macOS / iOS | CONF |
| Loon | Loon | CONF |
| Quantumult X | Quantumult X | CONF |
| Surfboard | Surfboard | CONF |
| Quantumult | Quantumult | CONF |
| Mellow | Mellow | CONF |

Shadowrocket 的原生 `.conf` 转换保留 `[Proxy]`、`[Proxy Group]`、`[Rule]`,但不再复制 AnyTLS、TUIC、VLESS Reality 等节点定义;节点只由首页订阅持有。每个策略组保留 Shadowrocket 内置 `PROXY` 作为稳定出口,按名称明确引用首页节点,不再写入使原生策略失效的 `include-all-proxies=1`;已有第三方筛选条件仍保留。`select` 组写入 `policy-select-name`,`♻️ 手动切换`不写 `hidden`,广告和 NSFW 仍以 `REJECT` 开始,国内规则组仍保留 `DIRECT` 默认。`site-v0.4.31` 已由用户真机确认:两步导入后,配置模式联网、首页节点选择、`DIRECT` 和其他组引用 `♻️ 手动切换` 均可用。内置完整版、精简版、第三方预设和允许的自定义远程配置共用转换流程,但并非每种预设和协议均经真机逐项验证。节点增删或改名后需刷新 `.conf`。

输入协议由锁定的转换引擎自动识别,页面不会让用户逐个选择协议。已用合成节点验证 Mihomo 与 sing-box 输出可以保留 AnyTLS、VLESS Reality、Hysteria2 和 TUIC。其他输出仍会先识别这些输入,再按目标客户端本身的协议与字段能力过滤;转换器不能让一个客户端支持它尚未实现的协议。

## 高级选项

高级选项会随固定地址保存,每次客户端刷新同一个 URL 时继续生效:

- Emoji 国旗;
- 强制启用 UDP、TCP Fast Open 或 TLS 1.3;
- 为 Mihomo / sing-box 的 VLESS、VMess 节点强制使用 XUDP;关闭时保留订阅与转换引擎的自动判断;Shadowrocket 原生格式暂不展示这个未经真机验证的开关;
- 跳过证书验证(默认关闭);
- 节点名称排序、协议类型前缀和不支持节点过滤;
- 包含/排除节点正则和节点重命名规则;
- 拉取上游订阅时使用的自定义 User-Agent;
- 可完全关闭的自动更新;开启后可设置 1 到 168 小时的刷新间隔;
- sing-box IPv6 开关,直接控制 FakeIP、TUN IPv6 地址与 AAAA 解析。

Mihomo 输出始终使用客户端要求的新字段名,完整配置始终展开 Ekko Rules,因此这两项不再重复提供开关。“插入默认节点”依赖额外预置节点源,本项目不会替用户注入第三方节点;Mihomo 已随完整配置提供 DNS 接管与加密上游,也不沿用旧版 Subconverter Web 的模板型 DoH 按钮。

Mihomo 固定地址每次被客户端刷新时,都会在本机即时拉取真实订阅并把节点直接内联到完整配置中。生成结果不会包含真实机场订阅 URL,也不会再引用仅容器内部可达的 provider 地址;客户端拿到一个文件即可获得节点、DNS、策略组与规则。

订阅响应会通过具名 `.yaml` / `.conf` 路径、`Profile-Title` 和 `Content-Disposition` 传递用户填写的名称。Shadowrocket 首页与配置页分别使用自己的稳定地址;`site-v0.4.31` 用户真机确认 `PROXY` 桥接可让配置模式联网,显式节点引用不会挤掉原生 `DIRECT` 或组间手动切换。完全相同的配置再次复制、扫码或一键导入时只复用现有浏览器记录,不会重复新增。

固定地址会把上游 `Subscription-Userinfo` 中 `upload`、`download`、`total` 和 `expire` 数字字段安全透传给客户端。若上游没有响应头,但正文有格式完整的 `STATUS=↑/↓/TOT/Expires` 行,则会转换为同样的响应头;单凭“剩余流量”节点名不能反推套餐总量。Shadowrocket 第 1 步的首页节点订阅负责显示这些流量与到期信息,第 2 步的原生 `.conf` 负责完整规则和策略联动。刷新时沿用目标格式安全的上游 User-Agent。

自动更新使用独立开关,默认关闭;更新间隔始终是正常的 `1` 到 `168` 小时数值,只有开关启用后才会生效。关闭时服务不会下发 `Profile-Update-Interval` 响应头;固定地址依然长期有效,需要更新时在客户端手动刷新即可。

## 保存什么

为了让固定 URL 跨普通重启继续有效,服务会在本机 Docker 命名卷 `profiles_data` 中保存最少映射:

- 随机 ID;
- 显示名称;
- 真实订阅地址;
- 输出格式;
- 高级选项;
- 创建时间。

不会保存生成后的配置、节点快照、转换历史或请求正文日志。页面和 `GET /api/profiles` 也不会返回真实订阅地址。

随机 ID 相当于本地订阅的访问凭据,请不要公开分享。若在 `.env` 中设置 `ACCESS_PASSWORD`,它会保护创建、读取列表和删除操作;客户端使用的 `/sub/<随机 ID>` 仍依靠随机 ID 自身访问,确保订阅客户端可以直接更新。

## 安全边界

- 真实订阅通过同源 JSON 请求体提交,不进入浏览器地址栏;
- 网关只接受 HTTP(S) 源地址,阻止环回、私网、链路本地和常见元数据地址;
- 源响应有超时与大小限制,重定向默认拒绝;
- 非 Mihomo 输出由网关先拉取,再通过短生命周期内部文件交给引擎;
- 订阅把节点放在 `proxy-providers` 里时,网关会自己去拉那些 provider(最多 8 个),走与订阅同一套私网地址检查和体积上限,再把节点交给引擎;
- 临时订阅正文每次转换结束后立即删除;
- Web 与转换引擎以非 root 用户运行,根文件系统只读;引擎缓存使用限额内存盘;
- Web 端口默认对宿主机和可信局域网发布;可选 `ACCESS_PASSWORD` 用于保护管理操作;
- 管理接口只接受属于本部署的 Host(回环、私网、CGNAT、`.local`、容器名或自己配置的域名),阻断把公网域名解析到内网地址的 DNS 重绑定。

开放形态在此之上还会:

- 关闭全部档案存储,没有任何可枚举的列表;
- 管理接口只接受 `PUBLIC_BASE_URL` 里那一个域名;
- 应用端口只绑定 `127.0.0.1`,由机器上已有的反向代理终止 TLS;
- 按客户端 IP 对管理接口和订阅刷新限速;
- 在反向代理和应用两层拒绝来自公网的 `/api/internal/*` 引擎交接请求;
- 通过 `robots.txt` 与 `X-Robots-Tag` 拒绝搜索引擎收录。

## 远程配置

转换时使用哪套规则由 `config` 决定。本仓库的两套永远排在最前:`ekko` 是完整版(42 个策略组),`ekko-lite` 是精简版(10 个策略组,分流行为与完整版完全一致,代价是不能按服务分别挑节点)。不传 `config` 时用完整版。页面另外内置了 ACL4SSR 的常用几套(全分组 / 无测速 / 去广告 / 精简 / 多国家等),同样通过短 id 选择:

```text
/sub?url=<真实订阅>&target=clash&config=ekko-lite
/sub?url=<真实订阅>&target=clash&config=acl4ssr-full
```

选中第三方配置时,那次转换的分组、规则与 base 模板**全部**来自对方项目,Ekko Rules 不参与;文件由转换引擎在请求时从对方服务器拉取,本仓库只保存引用,不复制也不再分发(见 [NOTICE.md](../NOTICE.md))。

| 变量 | 作用 |
|---|---|
| `THIRD_PARTY_REMOTE_CONFIGS=0` | 移除内置的第三方预设,只留 Ekko Rules 的完整版与精简版 |
| `REMOTE_CONFIGS` | 追加自己的预设,`[{"id":"mine","label":"My rules","url":"https://..."}]` |
| `ALLOW_CUSTOM_REMOTE_CONFIG=0` | 禁止访客直接粘贴配置地址 |

允许粘贴地址时,拉取动作由**服务器**而不是访客浏览器发出,因此仍然强制:只接受 https、不接受带凭据的 URL、长度上限 512,并且和订阅地址走同一套私网/回环/云元数据拦截。引擎侧另有 `max_allowed_rulesets` 与 `max_allowed_download_size` 上限,避免一份恶意配置引用上千个远程文件,把本机变成扇出抓取器。

远程配置与其规则集在引擎内缓存(配置 30 分钟、规则集 24 小时),否则每次客户端刷新都会重新拉取上游全部文件。

## 这是单用户服务

以下只适用于会保存档案的 `lan` 形态;开放形态不保存任何东西,也就没有可被别人读取的列表。

服务没有账号、没有会话、也没有档案归属:`profiles_data` 是一份共享列表,`GET /api/profiles` 返回其中**全部**档案,`DELETE /api/profiles/<id>` 可以删除其中任意一个。返回值里的 `/sub/<随机 ID>` 本身就是凭据,拿到它就能取回该档案的完整配置和节点密码。

由此得到两条硬性结论:

- **能打开管理页面的人 = 能拿走全部订阅的人。** 局域网部署如果不设 `ACCESS_PASSWORD`,同一网络内的任何设备(合租室友、公司网络、被入侵的路由器)都能列出并使用你的所有固定地址。检测到这种组合时,页面顶部会给出红色提醒;在不完全可信的网络里请设置密码,或把 `WEB_BIND_HOST` 改回 `127.0.0.1`。
- **不能给第二个人用。** 共用一个 `ACCESS_PASSWORD` 就等于共用一个账号:对方看得到你的档案,你也看得到对方的,任何一方都能删除全部。

要真正做到多用户,缺的不是一个开关,而是一整套:账号与会话、档案上的 owner 字段并按 owner 过滤列表与删除、每用户配额、存储侧对真实订阅的加密、审计与封禁、以及配套的条款与责任。本项目不做这件事——它面向个人自用,不应作为面向他人的公共转换服务发布。

## 常见问题

### 分流不准,或者国内网站走了代理

先看客户端里那几个会改写配置的开关是不是开着:「DNS 覆写 / DNS 设置」会顶掉整段
`dns:`(国内外分开解析、fake-ip、加密上游),「Smart 内核 / 智能分组」会改掉策略组的
类型与选路方式,「全局扩展脚本 / Merge / Script」则能改配置的任意部分。

完整配置里这几部分是配套的,规则要准就依赖它们。其中 `direct-nameserver` 让命中
`DIRECT` 的域名改用国内 DNS 重新解析:走代理的连接由代理服务器解析,只有直连需要本地
答案,而境外加密 DNS 在国内经常连不上,直连的海外域名(Steam 下载与登录、微软、苹果等)
就会卡住。不了解就让这些开关保持关闭;确实
需要自己接管,再在客户端里调。

### 公开转换站转不出来

转换站要先替你把订阅拉下来,所以拉取动作是从站点的服务器发出的,不是从你的电脑。
机场那边看到的是一个陌生 IP,于是可能拒绝,或者只认特定客户端的 User-Agent。

先试自定义 User-Agent,很多机场卡的只是这个。不行再自建——而且
要跑在自己的电脑或家里的机器上,拉取才会从你自己的家宽发出。部署到 VPS 不解决这个
问题:那仍然是一个机房 IP。

### 机场后台没开启订阅

不少机场默认关着订阅功能,或者换过套餐、重置过链接之后订阅处于停用状态。这时订阅
地址本身是通的,返回的却是空内容或一句提示,转换自然出不来节点。

先去机场的用户中心确认订阅已启用、链接是当前那一条,必要时重置一次再复制。开启之后
直接刷新原来的固定地址即可,不需要重新创建档案。

### 固定地址返回 502 或无法刷新

先确认 Docker 正在运行:

```bash
docker compose ps
docker compose logs --tail=100 web subconverter
```

如果服务健康但仍返回 502,通常是上游订阅当时不可用、需要在机场后台重新启用,或机场限制了获取订阅所需的客户端 User-Agent。重新启用上游后直接刷新原固定地址,不需要重新创建档案。

### 客户端没有显示流量、容量或到期时间

这些信息优先来自上游 `Subscription-Userinfo`,其次来自完整的 `STATUS=↑/↓/TOT/Expires` 行。Shadowrocket 在第 1 步导入的首页节点订阅中展示流量横幅;第 2 步的配置页 `.conf` 不负责这个 UI,而是负责原生规则与策略组。只有“剩余流量”节点名时无法可靠推算全部用量。

### 某些节点没有出现在目标配置中

输入协议会自动识别,但每种输出只能表达目标客户端实际支持的协议和字段。优先使用 Mihomo 或 sing-box 接收 AnyTLS、VLESS Reality、Hysteria2、TUIC 等现代协议;同时检查是否启用了“不支持节点过滤”、包含/排除正则或节点重命名规则。

### 另一台设备打不开 `127.0.0.1` 地址

`127.0.0.1` 永远指向当前设备自身。默认部署已经监听局域网,但手机或路由器必须把订阅前缀改成运行 Docker 的电脑 IP,例如 `http://192.168.1.100:8787`。如果仍无法打开,再检查 Wi-Fi 客户端隔离、Windows 网络类型和防火墙;不要把端口映射到公网。

## API

| 方法 | 路径 | 用途 |
|---|---|---|
| `GET` | `/api/health` | 服务与引擎状态 |
| `GET` | `/api/capabilities` | 输出格式和本地档案行为 |
| `GET` | `/api/profiles` | 列出不含源地址的本地档案 |
| `POST` | `/api/profiles` | 验证订阅并创建固定地址 |
| `DELETE` | `/api/profiles/<id>` | 删除固定地址 |
| `GET` | `/sub` | 无状态订阅入口,参数即配置(`public` 形态使用) |
| `GET` | `/sub/<id>` | 客户端完整配置更新入口(`lan` 形态) |
| `GET` | `/sub/<id>/nodes` | 旧版 Mihomo provider 配置的兼容节点入口 |
| `GET` | `/sub/<id>?download=1` | 下载当前完整配置 |
| `POST` | `/api/convert` | 不保存档案的一次性下载入口 |

创建档案:

```json
{
  "name": "手机主力订阅",
  "subscriptionUrl": "https://example.com/sub",
  "target": "clash",
  "options": {
    "emoji": true,
    "udp": true,
    "filterUnsupported": true,
    "autoUpdate": false,
    "updateIntervalHours": 24
  }
}
```

`autoUpdate` 明确控制是否自动更新;`updateIntervalHours` 只接受 `1` 到 `168`,并且只在开关启用时使用。

## 更新规则快照

仓库规则变更后,在仓库根目录执行:

```bash
node selfhost/scripts/sync-rulesets.mjs
cd selfhost
docker compose up --build -d
```

规则文件在镜像构建时复制进去,转换请求不会临时从 GitHub 拉取规则。

## 两条发布线

规则和站点是两样东西,改动节奏也不同,所以各走各的 tag:

| tag | 重建 | 页面上的数字 |
|---|---|---|
| `rules-v1.2.3` | 引擎镜像(rulesets、config、base) | 规则 1.2.3 |
| `site-v1.2.3` | web 镜像(网关与页面) | 站点 1.2.3 |
| `selfhost-v1.2.3` | 两个都建(拆分前的老 tag,仍可用) | 两个都变 |

这样改一行页面文案不会再重建引擎、也不会在每台跟随 `latest` 的服务器上把引擎重启一次;
反过来规则单独发版时,页面上的规则号会跟着变,而站点号不动。

**规则版本由引擎自己回答。** 它构建时在镜像里写下 `public/rules-version.json`,
网关在内网向它请求这个文件并缓存五分钟。页面因此报的是**正在跑的那份规则**,而不是
web 镜像构建时身边碰巧是哪一版;引擎没起来时页面就不显示规则号,而不是显示一个它站不住
的数字。该文件所在目录是引擎唯一对外可读的目录,`pref.yml`、规则 ini 都仍然是 404,
而且这个端口从不发布到宿主机。

## 验证

应用单测与构建:

```bash
cd selfhost/app
npm install
npm test
npm run typecheck
npm run build
```

完整 Docker 验证:

```bash
cd selfhost
python -m pip install -r ../requirements.txt
node scripts/e2e-local.mjs
```

服务器部署的自动更新(CI 构建镜像推 GHCR,服务器定时拉取,不向 GitHub 开放任何入口)见 [docs/vps.md](docs/vps.md#自动部署ci--cd)。

远程配置验证(会访问公网,逐个拉取第三方配置):

```bash
cd selfhost
node scripts/verify-remote-configs.mjs
```

它会遍历 `/api/capabilities` 返回的全部远程配置,断言 Ekko Rules 永远排在首位且为内置项,对每一套跑一次完整 Mihomo 转换;内置完整版与精简版会验证 Shadowrocket 的固定策略默认值,第三方预设会覆盖包括 Shadowrocket 在内的 9 种客户端格式,允许自定义地址时还会验证自定义配置的 Mihomo 与 Shadowrocket 输出。最后确认云元数据、回环、私网、明文 HTTP 和不存在的预设都被拒绝。预设列表变动或准备上线开放部署前跑一次。

端到端脚本会验证 9 种完整输出、Mihomo 与 sing-box 的 AnyTLS 等现代协议、Shadowrocket 首页/配置页两条独立地址及其对应 scheme、无重复节点的原生 `PROXY` 桥接、每组明确引用首页节点及策略组语义、Emoji/UDP/筛选/重命名等高级选项、真实源地址不出现在 Mihomo 完整配置中、Mihomo 配置语法,以及固定 URL 在普通 Compose 重启后的可用性。可通过 `MIHOMO_BIN` 指定本机 Mihomo 可执行文件。

## 第三方组件

转换引擎使用 [Aethersailor/SubConverter-Extended](https://github.com/Aethersailor/SubConverter-Extended) `v1.3.0`,Docker 基础镜像固定到多架构镜像摘要;Emoji 映射同步自该版本随附规则。规则策略、分组、DNS 与完整基础配置由 Ekko Rules 的本地快照定义。