Levon's Blog

微信: L6241425

在同一 Wi-Fi 下把 Clash 订阅本地转换成 Quantumult X 订阅

这篇教程适合已经在用 Clash、ClashX、Clash Verge 或 mihomo 订阅,但希望在 iPhone 的 Quantumult X 里使用同一份节点的人。

目标不是把 Mac 变成代理网关,而是让 Mac 在同一 Wi-Fi 下临时充当一个本地订阅转换器:

1
2
3
4
5
更新订阅时:
iPhone Quantumult X -> Mac 上的 Subconverter -> 上游 Clash 订阅 -> 返回 Quantumult X 节点列表

实际代理和测速时:
iPhone Quantumult X -> 远程代理节点 -> 目标网站

也就是说,订阅更新会访问 Mac;节点测速、日常代理、App 流量不会经过 Mac。

适用场景

这个方案适合以下情况:

  • 上游给的是 Clash、ClashX、Clash Verge 或 mihomo 可用的订阅。
  • 你希望 iPhone 上的 Quantumult X 使用同一批节点。
  • 你不想把订阅地址提交给公共订阅转换站。
  • iPhone 和 Mac 可以在同一 Wi-Fi 下更新订阅。
  • 你接受 Mac 开机且 Docker 容器运行时,手机才能刷新这条订阅。

它不适合以下情况:

  • 希望 iPhone 在任意网络下都能更新订阅。
  • 希望 Mac 持续为 iPhone 转发全部代理流量。
  • 希望生成完整 Quantumult X 分流配置、重写、MITM、策略组体系。

本文生成的是 Quantumult X 的服务器资源,也就是节点列表;分流规则、策略组、重写和证书相关配置仍然应该在 Quantumult X 自己的配置里维护。

准备条件

你需要:

  • 一台 Mac。
  • Mac 上安装 Docker。
  • iPhone 安装 Quantumult X。
  • iPhone 和 Mac 连接同一个 Wi-Fi。
  • 一条上游 Clash 订阅地址。

为了避免泄露订阅,本文统一使用占位符:

1
2
3
<MAC_LAN_IP>                       Mac 的局域网 IP
<UPSTREAM_CLASH_SUB_URL> 原始 Clash 订阅地址
<ENCODED_UPSTREAM_CLASH_SUB_URL> URL 编码后的 Clash 订阅地址

不要把真实订阅 URL、token、UUID、password、节点服务器域名或 IP 写进公开文章。

启动本地 Subconverter

在 Mac 上运行:

1
2
3
4
docker run -d --restart=always \
--name subconverter \
-p 25500:25500 \
tindy2013/subconverter:latest

这里使用 -p 25500:25500,是为了让同一 Wi-Fi 下的 iPhone 能访问 Mac 的 25500 端口。

如果只在 Mac 本机测试,可以绑定到 127.0.0.1

1
2
3
4
docker run -d --restart=always \
--name subconverter \
-p 127.0.0.1:25500:25500 \
tindy2013/subconverter:latest

但这种方式 iPhone 无法访问,因为 iPhone 上的 127.0.0.1 指的是 iPhone 自己,不是 Mac。

验证容器是否运行:

1
docker ps --filter name=subconverter

验证 Subconverter 是否可用:

1
curl http://127.0.0.1:25500/version

如果正常,会看到类似:

1
subconverter v0.9.0-xxxxxxx backend

获取 Mac 局域网 IP

在 Mac 上执行:

1
ipconfig getifaddr en0

如果没有输出,可以试:

1
ipconfig getifaddr en1

得到的地址就是后面 URL 里的 <MAC_LAN_IP>,例如:

1
192.168.x.x

在 iPhone Safari 里打开:

1
http://<MAC_LAN_IP>:25500/version

如果 iPhone 能看到 Subconverter 版本号,说明手机已经能访问 Mac 上的本地转换服务。

构造 Quantumult X 订阅链接

先把上游 Clash 订阅地址做 URL 编码。

原始订阅:

1
<UPSTREAM_CLASH_SUB_URL>

编码后:

1
<ENCODED_UPSTREAM_CLASH_SUB_URL>

可以用 Python 编码并复制到剪贴板:

1
2
3
4
5
6
python3 - <<'PY' | pbcopy
import urllib.parse

raw = "<UPSTREAM_CLASH_SUB_URL>"
print(urllib.parse.quote(raw, safe=""))
PY

如果不想让订阅地址留在 shell 历史里,可以先临时关闭命令历史,或者把命令放进只在本机使用的临时脚本里,执行后立即删除。

最终给 Quantumult X 使用的服务器资源订阅 URL:

1
http://<MAC_LAN_IP>:25500/sub?target=quanx&list=true&udp=true&tfo=true&sort=true&exclude=%28Traffic%7CExpire%7C%E6%B5%81%E9%87%8F%7C%E5%AE%98%E7%BD%91%7C%E5%89%A9%E4%BD%99%7C%E8%BF%87%E6%9C%9F%7C%E9%87%8D%E7%BD%AE%7CReset%7CDate%7CDays+Left%7CG+%5C%7C%29&url=<ENCODED_UPSTREAM_CLASH_SUB_URL>

这个 URL 可以直接填入 Quantumult X 的服务器资源订阅。

参数解释

核心参数如下:

1
target=quanx

把订阅转换成 Quantumult X 格式。

1
list=true

只输出节点列表。这个参数很重要。如果不加 list=true,Subconverter 可能输出完整 Quantumult X 配置,从 [general] 开始;这不适合作为服务器资源订阅导入。

1
udp=true

为支持的节点开启 UDP relay。

1
tfo=true

为支持的节点开启 TCP Fast Open。

1
sort=true

按节点名排序,方便在 Quantumult X 里查看。

1
exclude=...

排除流量、到期时间、官网、重置时间等可能被误识别为节点的订阅信息行。

本文示例使用的排除规则含义是:

1
(Traffic|Expire|流量|官网|剩余|过期|重置|Reset|Date|Days Left|G \|)

实际写进 URL 时需要 URL 编码。

1
url=<ENCODED_UPSTREAM_CLASH_SUB_URL>

上游 Clash 订阅地址。这里必须放 URL 编码后的结果。

在 Quantumult X 中添加订阅

在 Quantumult X 里添加服务器资源,填入上面构造的 URL:

1
http://<MAC_LAN_IP>:25500/sub?target=quanx&list=true&udp=true&tfo=true&sort=true&exclude=...&url=<ENCODED_UPSTREAM_CLASH_SUB_URL>

添加后刷新资源。如果转换成功,Quantumult X 会拿到一批节点。

如果你直接用配置文件形式,也可以把它写成类似:

1
2
[server_remote]
http://<MAC_LAN_IP>:25500/sub?target=quanx&list=true&udp=true&tfo=true&sort=true&exclude=...&url=<ENCODED_UPSTREAM_CLASH_SUB_URL>, tag=本地转换节点, update-interval=86400, opt-parser=false, enabled=true

验证方式

先验证手机能访问 Mac:

1
http://<MAC_LAN_IP>:25500/version

再验证订阅 URL 能返回 Quantumult X 节点列表。正常内容会类似:

1
trojan = <SERVER_HOST>:<PORT>, password=<PASSWORD>, over-tls=true, ...

公开文章不要展示真实节点行。节点行里通常包含服务器地址、端口、密码、TLS host 等敏感信息。

最后在 Quantumult X 里更新订阅并测试节点延迟或速度。

测速时的数据链路是:

1
iPhone Quantumult X -> 远程代理节点 -> 测速目标

不再经过 Mac 上的 Subconverter。Mac 只参与订阅更新。

常见故障排查

打不开 http://<MAC_LAN_IP>:25500/

Subconverter 没有可用的首页 UI。测试服务是否正常应该访问:

1
http://<MAC_LAN_IP>:25500/version

Mac 本机能打开,iPhone 打不开

优先检查:

  • iPhone 和 Mac 是否在同一个 Wi-Fi。
  • iPhone 是否开着 VPN 或代理,导致局域网请求被转发走。
  • Mac 的局域网 IP 是否变了。
  • Docker 是否仍在运行。
  • Mac 防火墙是否阻止了入站连接。

可以在 Mac 上重新查看 IP:

1
ipconfig getifaddr en0

并查看容器:

1
docker ps --filter name=subconverter

Quantumult X 导入后内容像完整配置

检查 URL 里有没有:

1
list=true

没有这个参数时,target=quanx 可能输出完整配置,而不是服务器资源需要的纯节点列表。

订阅里出现流量、到期时间这类伪节点

增加或调整 exclude 参数。例如:

1
(Traffic|Expire|流量|官网|剩余|过期|重置|Reset|Date|Days Left|G \|)

写进 URL 前要 URL 编码。

手机离开家后更新失败

这是预期结果。这个方案只适合同一 Wi-Fi 下更新。

如果需要外网更新,可以考虑 Tailscale、ZeroTier、内网穿透或自建带认证的转换服务。但这已经不是本文的 “ 同一 Wi-Fi 本地转换 “ 方案,安全边界也完全不同。

QX 测速是不是走 Mac

不是。Mac 只提供订阅转换服务。

订阅更新时:

1
iPhone -> Mac Subconverter -> 上游订阅

实际代理和测速时:

1
iPhone -> 远程节点

安全和脱敏建议

不要把以下内容写入公开文章、截图或 Issue:

  • 原始订阅 URL。
  • URL 编码后的订阅 URL。
  • token、UUID、password。
  • 节点服务器域名或 IP。
  • 完整 Quantumult X 节点行。

建议公开文章只写占位符:

1
2
3
4
5
<MAC_LAN_IP>
<UPSTREAM_CLASH_SUB_URL>
<ENCODED_UPSTREAM_CLASH_SUB_URL>
<SERVER_HOST>
<PASSWORD>

如果必须截图,先遮住浏览器地址栏、Quantumult X 订阅 URL、节点详情和服务商信息。

也不建议把 25500 端口暴露到公网。本文的默认边界是同一 Wi-Fi 下使用。

我的实际配置示例

我的测试环境是:

1
2
3
4
Mac + Docker + Subconverter
iPhone + Quantumult X
同一 Wi-Fi
上游订阅原本可被 Clash/Clash Verge 使用

当时 Mac 的局域网 IP 是:

1
<MAC_LAN_IP>

最终订阅 URL 结构是:

1
http://<MAC_LAN_IP>:25500/sub?target=quanx&list=true&udp=true&tfo=true&sort=true&exclude=%28Traffic%7CExpire%7C%E6%B5%81%E9%87%8F%7C%E5%AE%98%E7%BD%91%7C%E5%89%A9%E4%BD%99%7C%E8%BF%87%E6%9C%9F%7C%E9%87%8D%E7%BD%AE%7CReset%7CDate%7CDays+Left%7CG+%5C%7C%29&url=<ENCODED_UPSTREAM_CLASH_SUB_URL>

转换成功后,Quantumult X 拿到的是服务器资源节点列表。后续测速和代理连接由 iPhone 直接访问远程节点,Mac 不再参与。

参考资料

当单表突破千万行,查询变慢、索引膨胀、归档困难会集中爆发。PostgreSQL 声明式分区是应对这类增长最直接的手段:应用代码几乎不改,数据库帮你把一张逻辑表拆成多个物理分区,查询时只扫描相关分区。

本文基于 PostgreSQL 14+,覆盖三种分区方式、分区键和粒度选择、EXPLAIN 实测、已有表迁移为分区表,以及自动建分区和归档等生产运维操作。

阅读全文 »

1. 为什么我们没有直接用 Logs Insights

我第一次看到这套日志搜索方案时,其实有点疑惑:AWS 已经有 CloudWatch Logs Insights,为什么还要自己搭一条 Firehose -> S3 -> EC2 -> ripgrep 的链路?

后来把代码和部署手册看完,答案反而很朴素。我们不是想重新做一个日志平台,只是想解决一个更窄的问题:开发排查线上问题时,能不能低成本地搜最近几天的历史日志,而且不要每次手一抖就把账单扫上去。

阅读全文 »

1. 这篇其实不是讲 CLI 命令

前两篇讲了 Runway 怎么把 YAML 变成 RDS,又怎么把 Terraform 模块塞进 Go server。这一篇看起来是在讲 CLI,其实更像是在讲:一个部署工具怎么把很多“不想让业务方操心”的事情藏起来。

比如本地连数据库,不让开发者配 AWS 凭证;比如 ECS 说部署完成以后,不马上报成功;比如危险操作不能靠前端弹窗确认;比如 admin console 打开 SQL 浏览器时,不要每次都重新启动一个 pgweb。

这些都不是大架构,但很影响工具好不好用。

阅读全文 »

1. 为什么我说 Runway 是把 Terraform 当库用

我以前对 Terraform 的默认理解很简单:业务仓库里放 .tf 文件,CI 跑 terraform plan,合并后再跑 terraform apply。Atlantis、Terraform Cloud、Spacelift 大多都是这个路子。

Runway 不是这么做的。

业务方只写 runway.yaml。Terraform 模块跟 Go server 一起被编进二进制里。部署时,server 解压模块、拼出一份临时的根 .tf,再启动一个 Terraform 子进程去 apply

所以这里说“把 Terraform 当库用”,不是说 Go 代码里真的 import terraform。它的意思是:Terraform 仍然是执行引擎,但模块版本、调用时机、输入生成、日志输出,都被平台代码接管了。

这篇记录我读这套设计时真正卡住的几个点:模块怎么塞进二进制,为什么解压目录不能随机,为什么 HCL 只是字符串模板,跨 stack 引用为什么一会儿走 Terraform、一会儿走 Go 代码。

阅读全文 »

1. 以为是 5 分钟升级,最后花了半天

这次升级 Gitea,我原本只想顺手把自建 runner 改成 ephemeral 模式。

预期很简单:Gitea 升到 1.26,act_runner register 加一个 --ephemeral,runner 跑完一个 job 自动退出。这样就不用再让 Fargate task 空等 30 分钟。

实际不是。

先是 runner 启不来,接着 job 卡在 queued,再接着发现 webhook 没发对事件,最后翻出两百多个 offline runner。问题单个看都不大,串起来就吃掉半天。

阅读全文 »

1. 一行 YAML 为什么没有变成 DATABASE_URL

我一开始把 db: postgresql 理解成一个动作:写进 runway.yaml,再跑一次 runway deploy,平台就会帮我建库,并把连接串注入到容器里。

结果不是这样。

部署成功了,容器也起来了,但应用里读 os.Getenv("DATABASE_URL"),拿到的是空字符串。这个问题最烦的地方是:它不是报错,而是“看起来都正常,只是少了一个变量”。

顺着代码查下去才发现,那行 YAML 只是被解析了,并没有参与部署请求。换句话说,它像一句写在配置里的备注:人看得懂,程序不一定理它。

这篇先不写成“Runway PaaS 机制大全”。我只记录一件事:一个应用最终拿到 DATABASE_URL,中间到底经过了哪些地方;又有哪些地方我一开始想错了。

阅读全文 »

1. 为什么我不想再手工维护 Postman

Postman collection 手工维护到最后,最麻烦的不是点几下 UI,而是你不知道哪一份才是真的。

代码里有一份路由,docs/openapi.json 里有一份接口契约,Postman 里又有一份 collection。接口改过几轮以后,这三份东西很容易开始互相打架。路径少一个前缀、body 字段改了名字、认证方式忘了更新,这些问题单独看都不大,但排查时很烦。

阅读全文 »

Agent 看起来很神秘:它能读代码、改文件、跑命令,遇到错误还能换一种方式重试。但把最小实现拆开,核心并不复杂——一个能工作的 Agent,最小内核只有三件事:模型、循环、工具。

这篇文章不停留在概念层面。读完以后,你应该能看懂任何 Agent 框架的核心循环,能判断一个 SDK 帮你封装了什么、省掉了什么、藏了什么坑。

阅读全文 »

每次打开 git log --graph,看到那些七拐八绕的线,第一反应往往是:换个工具会不会清楚点?

通常不会。图乱不是渲染器的问题,而是历史本身的问题。要看清这一点,先要回答两件事:Git 底层到底存了什么,以及 “ 分支 “” 提交 “” 合并 “ 这些词真正指向哪些对象。

阅读全文 »
0%