模板示例
Thurgio 的核心扩展面是 Tera 模板:每个 provider / snippet 通过 template 字段指定一个模板,渲染上下文携带代理列表(l.proxies)、配置(config)、参数(p)等数据,配合内置过滤器即可产出任意格式的订阅(Clash / sing-box / V2RayN / Surge / 纯文本 / Base64…)。
本页索引 fixtures/examples/tpls/ 中的示例模板,并给出过滤器快速参考。按输出格式分类的精选索引(用途 / 依赖过滤器 / 适用客户端)见 fixtures/examples/README.md。模板语法详见配置指南。
目录结构
fixtures/examples/
├── README.md # 精选模板索引(按输出格式分类)
├── thurgio.yaml # 主配置(setting)
├── provider.yaml # provider 定义(模板引用在这里)
├── snippet.yaml # snippet 定义
├── filter.yaml # 过滤规则
├── rename.yaml # 改名规则
├── params.yaml # 渲染参数
├── tpls/ # 模板目录(THURGIO_TPL_DIR,默认 "tpls")
├── prov/ # 订阅源(本地文件)
├── rules/ # 规则集(snippet 源)
└── prod/ # build 输出
模板清单
以下模板全部默认接线:
provider.yaml的outputs与snippet.yaml条目已引用它们,thurgio build一次产出所有格式。
| 模板 | 适用 | 输出格式 | 说明 |
|---|---|---|---|
base.tpl | provider | Clash YAML | 最小模板(thurgio init 生成),json_str 逐节点输出 |
clash.tpl | provider | Clash YAML | 紧凑 JSON 节点 + test 块 + rule-providers(遍历 config.snippet 的 p.clash_url) |
clash_groups.tpl | provider | Clash YAML | 按 p.groups 规则名动态生成 proxy-groups(filter(rules=...) 实时筛选成员) |
clash_list.tpl | provider | Clash YAML | clash.tpl 变体:头部注释打印 l.rename / l.filter |
surge.tpl | provider | Surge 配置 | 手写 [Proxy] 段(for 循环 + 字段展开,教学向) |
surge_proxy.tpl | provider | Surge 配置 | surge_proxy 过滤器版 [Proxy] 段({{ l.proxies | surge_proxy() }}) |
surge_full.tpl | provider | Surge 完整配置 | [General] / [Proxy] / [Proxy Group] / [Rule] 四段齐全,可直接订阅导入(surge_proxy 节点行 + select/url-test 组 + p.rules 规则聚合,首行 #!MANAGED-CONFIG) |
v2rayn.tpl | provider | 纯文本 | 每行一个 vmess:// 分享链接(v2rayn 过滤器) |
sip002.tpl | provider | 纯文本 | 每行一个 ss:// 分享链接(sip002 过滤器) |
sip008.tpl | provider | JSON | SS Android 官方订阅:version + servers 数组(sip008 过滤器,仅 ss 节点) |
ssd.tpl | provider | 纯文本 | ssd:// 订阅:UrlSafe Base64 JSON(ssd 过滤器,仅 ss 节点) |
shadowrocket.tpl | provider | 纯文本 | 每行一个分享链接(shadowrocket 过滤器,复用 sip002/v2rayn 转换,wg 跳过) |
base64_sub.tpl | provider | Base64 | 机场式订阅:v2rayn + sip002 链接整体 base64(mode="encode") 编码 |
plain_list.tpl | provider | 纯文本 | 人类可读节点清单:名称 | 类型 | 服务器:端口(for 循环 + 字段访问) |
singbox.tpl | provider | sing-box JSON | 完整 sing-box 配置:sibox 转 outbounds + direct/block |
singbox_rules.tpl | snippet | sing-box JSON | clash 规则文本 → sing-box rule 数组(clashbox 过滤器),与 singbox.tpl 组合成完整配置 |
qx.tpl | provider | QX 节点行 | 每行一个 QX 节点(qx 过滤器,ss/ssr/vmess/vless/trojan/http/socks5) |
qx_config.tpl | provider | QX 完整配置 | 完整 Quantumult X 配置:[general]/[dns]/[server_local]/[policy](qx_config 过滤器) |
loon.tpl | provider | Loon 节点行 | 每行一个 Loon 节点(loon 过滤器,10 种类型) |
loon_config.tpl | provider | Loon 完整配置 | 完整 Loon 配置:[General]/[Proxy]/[Proxy Group]/[Rule](loon_config 过滤器) |
mellow.tpl | provider | Mellow JSON | Mellow/V2Ray-core JSON:socks inbound + per-proxy outbounds(mellow 过滤器,ss/vmess/vless/trojan/http/socks5 + TCP/WS/TLS) |
mixed.tpl | provider | 混合分享链接 | 混合单链接列表:逐节点 V2RayN 优先、SIP002 回退,mixed 过滤器 |
rules_aggregate.tpl | snippet | Quantumult X 规则集 | 多规则集聚合:拼接 config.snippet 中各规则集内容 → uniq 去重 → sort_domain_ip 排序 → quantumult 输出(s.p.rules 指定要聚合的 snippet 名,s.p.policy 指定策略);换 clash / surge 过滤器即得对应格式规则列表 |
snippet.tpl | snippet | 原样 | 原样输出 snippet 内容(s.response.content) |
rules_pipeline.tpl | provider | 教学 | 演示模板内 filter → rename → v2rayn/sip002/cla 过滤器链 |
date.tpl | 片段 | 文本 | {% include %} 引入的生成时间注释 |
ui.tpl | — | HTML | Dashboard 页面(服务器自带,勿删除) |
auto/*.tpl | provider | UA 自适应 | tpl=auto 时按 User-Agent 选择的模板(auto/cla.tpl/surge.tpl/qx.tpl/loon.tpl/sibox.tpl/shadowrocket.tpl/v2rayn.tpl,见 examples/templates-auto/,复制到 tpls/auto/ 即用) |
配置引用方式:
provider.yaml中template: clash.tpl即相对模板目录的模板名;outputs下每个具名输出可指定不同模板,同一份代理列表渲染多种格式:
provider:
example:
type: clash
source: "prov/example.yaml"
template: clash_list.tpl
filename:
- prod/list/example.yaml
outputs:
base64_sub:
template: base64_sub.tpl
filename:
- prod/example_base64.txt
plain_list:
template: plain_list.tpl
filename:
- prod/example_plain.txt
surge_full.tpl — Surge 完整配置
产出可直接被 Surge 客户端订阅的完整配置([General] / [Proxy] / [Proxy Group] / [Rule] 四段齐全)。节点行来自 surge_proxy 过滤器;[Proxy Group] 含一个 select 组(Proxy)与一个 url-test 自动测速组(Auto);[Rule] 段把 p.rules 指定的规则集 snippet 聚合后经 surge 过滤器输出,末尾补 FINAL,Proxy 兜底。模板不硬编码任何服务器信息,全部来自上下文变量。
参数放在 params.yaml(渲染上下文 p 绑定来自全局参数):
# params.yaml
rules: # [Rule] 段聚合的 snippet 名(config.snippet 的键,缺省空)
- China
rule_policy: Proxy # [Rule] 默认策略名(缺省 Proxy)
test: # url-test 测速参数(缺省 http://cp.cloudflare.com/generate_204 / 300)
url: "http://cp.cloudflare.com/generate_204"
interval: 300
在 provider.yaml 的 outputs 中挂载(template / filename 字段):
provider:
example:
type: clash
source: "prov/example.yaml"
template: clash_list.tpl
filename:
- prod/list/example.yaml
outputs:
surge_full:
name: example
template: surge_full.tpl
filename:
- prod/example_surge_full.conf
首行 #!MANAGED-CONFIG 的拉取地址来自环境变量 SURGE_CONFIG_URL(未设置时输出占位 URL,需改成自己的订阅地址),Surge 客户端会按 interval=86400 定期更新。
[Proxy Group]的节点名列表与[Proxy]段保持一致:仅列出 Surge 支持的 ss/trojan/vmess/snell/http/socks5/wireguard 七种类型(与Surge::supports能力谓词同步)——vless/ssr/hysteria/hysteria2/tuic/anytls 会被surge_proxy跳过,组内引用必须匹配,避免引用不存在的节点。
跑通示例
cd fixtures/examples
thurgio -r . build # -r 是顶层选项,必须放在子命令前
或从仓库根目录:
cargo run -q -p thurgio-cli -- -r fixtures/examples build
构建成功后输出位于 fixtures/examples/prod/:
| 输出 | 模板 | 格式 |
|---|---|---|
prod/list/example.yaml | clash_list.tpl | Clash |
prod/example.yaml | clash.tpl | Clash |
prod/example_groups.yaml | clash_groups.tpl | Clash(动态 proxy-groups) |
prod/example_surge.txt | surge.tpl | Surge(手写 [Proxy] 段) |
prod/example_surge_proxy.txt | surge_proxy.tpl | Surge(过滤器版 [Proxy] 段) |
prod/list/example_v2rayn.txt / prod/example_v2rayn.txt | v2rayn.tpl | 分享链接 |
prod/list/example_sip002.txt / prod/example_sip002.txt | sip002.tpl | 分享链接 |
prod/example_sip008.json | sip008.tpl | SS Android JSON 订阅 |
prod/example_ssd.txt | ssd.tpl | ssd:// 订阅 |
prod/example_shadowrocket.txt | shadowrocket.tpl | Shadowrocket 分享链接 |
prod/example_base64.txt | base64_sub.tpl | Base64 订阅 |
prod/example_plain.txt | plain_list.tpl | 纯文本节点清单 |
prod/example_singbox.json | singbox.tpl | sing-box 完整配置 |
prod/example_singbox_rules.json | singbox_rules.tpl | sing-box 规则数组 |
prod/example_china.list | snippet.tpl | 原样规则集 |
prod/example_quantumult.txt | rules_aggregate.tpl | Quantumult X 规则集 |
挂载新模板:在 provider.yaml 的 outputs(或 snippet.yaml 条目)中按 key 复制现有条目,改 template / filename 即可。注意 clash.tpl 会遍历所有 snippet 读取 p.clash_url,新 snippet 也要带该参数。
fixtures/examples被集成测试锁定(ADR-060),修改声明文件后跑cargo test -p thurgio-app --test render_examples验证。
tpl=auto:按 User-Agent 自适应输出
服务端在 GET /app/read/l 收到 ?tpl=auto 时嗅探请求 User-Agent 选模板,按候选顺序取第一个存在于模板目录的 auto/<key>.tpl 渲染(对齐 subconverter 行为)。仅 provider 端点支持;snippet 与 /app/admin/adhoc 不支持。
工作原理:大小写不敏感子串匹配,先命中先赢;命中行的候选键按序探测文件存在性,首个存在的即为本次渲染模板。
UA 匹配表
行顺序即优先级(同时命中 clash 与 v2ray 时 clash 行先赢)。surge 行命中后会兜底追加默认顺序的剩余键;其余行仅尝试自身候选。
| UA 片段(小写) | 候选键 → 模板文件 | 说明 |
|---|---|---|
surge | surge → auto/surge.tpl,未命中则按默认顺序继续 cla / qx / loon / sibox / shadowrocket / v2rayn | Surge(含 Mac / iOS) |
quantumult | qx → auto/qx.tpl | Quantumult X |
loon | loon → auto/loon.tpl | Loon |
clash | cla → auto/cla.tpl | clash / clash-verge / clashmeta / Clash for Windows 等 |
mihomo | cla → auto/cla.tpl | mihomo |
stash | cla → auto/cla.tpl | Stash |
sing-box | sibox → auto/sibox.tpl | sing-box |
singbox | sibox → auto/sibox.tpl | singbox(无连字符) |
sfa | sibox → auto/sibox.tpl | sing-box Android 前端 |
sfi | sibox → auto/sibox.tpl | sing-box iOS 前端 |
sfm | sibox → auto/sibox.tpl | sing-box macOS 前端 |
shadowrocket | shadowrocket → auto/shadowrocket.tpl | Shadowrocket |
v2ray | v2rayn → auto/v2rayn.tpl | v2rayN / v2rayNG |
| (未知或缺失 UA) | cla → auto/cla.tpl → surge → qx → loon → sibox → shadowrocket → v2rayn | 默认顺序,依次探测 |
默认顺序见
crates/thurgio-server/src/ua.rs:16的DEFAULT_ORDER;匹配表见同文件UA_TABLE(crates/thurgio-server/src/ua.rs:44)。
部署步骤
examples/templates-auto/ 提供可直接复制的最小可用模板集(cla.tpl / surge.tpl / qx.tpl / loon.tpl / sibox.tpl / shadowrocket.tpl / v2rayn.tpl):
mkdir -p tpls/auto
cp examples/templates-auto/*.tpl tpls/auto/
# 按需删改:不需要的客户端模板可直接删除,剩余的即为该 UA 的唯一候选
模板目录由 THURGIO_TPL_DIR 决定(默认 tpls),tpl=auto 的探测根固定为该目录下的 auto/ 子目录(crates/thurgio-server/src/ua.rs:29 的 RENDERER_TPL_DIR)。
优先级与回退
- 显式
?tpl=优先:?tpl=auto才走 UA 探测;?tpl=clash.tpl等显式模板名直接透传,不做 UA 解析(crates/thurgio-server/src/ua.rs:115的apply_auto_template透传分支,见crates/thurgio-server/tests/app/auto_tpl.rs:118)。?tpl=为空时回退到 provider 配置的template。 - 候选内回退:同一 UA 的候选列表内按序探测,首个存在的
auto/<key>.tpl胜出(crates/thurgio-server/src/ua.rs:87的resolve_auto_template);仅surge行会扩展为全默认顺序,其余行仅尝试自身键(crates/thurgio-server/src/ua.rs:66的auto_template_candidates)。 - 无匹配即 400:全部候选都不存在时返回
400 Bad Request,错误信息形如no auto template exists (tried: auto/cla.tpl, auto/surge.tpl, ...)并列出本次已尝试的全部模板名(crates/thurgio-server/src/ua.rs:130的NoAutoTemplate,映射为AppError::bad_request于crates/thurgio-server/src/helpers/provider_request.rs:47)。
注意
provider.type 不能写 auto,auto 仅用于渲染侧 ?tpl=auto 的 UA 自适应选择;输入格式仍需显式指定真实类型(clash / v2rayn / sip002 等),见配置指南与配置参考。
过滤器快速参考
完整说明见配置指南,此处给出最常用的调用形式:
输出过滤器(代理数组 → 目标格式)
| 过滤器 | 参数 | 示例 |
|---|---|---|
v2rayn | — | {{ l.proxies | v2rayn() }} |
sip002 | — | {{ l.proxies | sip002() }} |
sip008 | — | {{ l.proxies | sip008() }} — SS Android JSON 订阅(仅 ss 节点) |
shadowrocket | — | {{ l.proxies | shadowrocket() }} — 每行一个分享链接(wg 跳过) |
ssd | — | {{ l.proxies | ssd() }} — ssd:// 订阅(仅 ss 节点) |
surge_proxy | — | {{ l.proxies | surge_proxy() }} — Surge [Proxy] 段行(ss/trojan/vmess/snell/http/socks5/wireguard) |
qx | — | {{ l.proxies | qx() }} — Quantumult X 节点行(7 种类型) |
qx_config | check_url=, dns=, group= | {{ l.proxies | qx_config(group="Proxy") }} — 完整 QX 配置四段 |
loon | — | {{ l.proxies | loon() }} — Loon 节点行(10 种类型) |
loon_config | dns=, group= | {{ l.proxies | loon_config(group="Proxy") }} — 完整 Loon 配置四段 |
mellow | port=1080 | {{ l.proxies | mellow(port=1080) }} — Mellow/V2Ray-core JSON(socks inbound + outbounds) |
mixed | — | {{ l.proxies | mixed() }} — 混合单链接列表(V2RayN 优先、SIP002 回退) |
stash | — | {{ l.proxies | stash() }} — Stash(Clash 兼容)节点行 |
surfboard | — | {{ l.proxies | surfboard() }} — Surfboard(Surge 兼容)节点行 |
clashr | — | {{ l.proxies | clashr() }} — ClashR 节点行 |
sibox | — | {{ l.proxies | sibox() | json_str(pretty=true) }} — sing-box outbound 对象数组 |
singbox | — | {{ l.proxies | singbox() }} — sing-box 完整 JSON(别名 sibox) |
cla | meta=true | {{ l.proxies | cla(meta=true) | json_str() }} — 过滤出 clash 兼容节点 |
proxy_suffix | suffix="..." | {{ l.proxies | proxy_suffix(suffix="HK") }} — 给节点名加后缀 |
js | src="..." | {{ l.proxies | js(src="return $input.map(p=>...)") }} — 内联 JS 变换(boa_engine) |
sort_nodes | by=name/region/latency, order=asc/desc, latencies= | {{ l.proxies | sort_nodes(by="latency", latencies=map) }} |
clash_groups | groups="a,b" | {{ l.proxies | clash_groups() }} — Clash/Mihomo 策略组 |
surge_groups | — | {{ l.proxies | surge_groups() }} — Surge 策略组 |
规则过滤器(模板内动态应用 filter.yaml / rename.yaml)
| 过滤器 | 参数 | 示例 |
|---|---|---|
filter | rules="a,b"(或数组) | {{ l.proxies | filter(rules="hk") | v2rayn() }} |
rename | rules="a,b"(或数组) | {{ l.proxies | rename(rules="rm_brackets") | v2rayn() }} |
未定义的规则名静默忽略;过滤器可链式组合,顺序即应用顺序。
规则文本过滤器(snippet 内容 / 规则集)
| 过滤器 | 参数 | 示例 |
|---|---|---|
clash | policy=..., symbol=... | {{ s.response.content | clash(policy="Proxy") }} — 规则文本 → clash 规则列表 |
surge | policy=..., symbol=... | {{ s.response.content | surge(policy="Proxy") }} |
quantumult | policy=..., symbol=... | {{ s.response.content | quantumult(policy="Proxy") }} — 规则文本 → Quantumult X 规则集(host/host-suffix/host-keyword/ip-cidr/ip6-cidr/geoip/user-agent/final;无对应类型的规则丢弃) |
clashbox | policy=..., premium=..., meta=..., pretty=true | {{ s.response.content | clashbox(policy="proxy") }} — clash 规则文本 → sing-box rule 对象 |
no_resolve | value=true/false | {{ s.response.content | no_resolve(value=true) }} |
sort_domain_ip | — | 域名/IP 规则排序 |
通用过滤器
| 过滤器 | 参数 | 示例 |
|---|---|---|
base64 | mode="encode"/"decode" | {{ lines | base64(mode="encode") }} |
json_str | pretty=false | {{ i | json_str(pretty=false) }} |
yaml_str | — | {{ l.proxies | yaml_str() }} |
indent | width=2 | {{ body | indent(width=2) }} |
uniq | — | 按行去重 |
clear | num=3, n=2 | 压缩连续空行 |
slugify / urlencode | separator="-" | 字符串处理 |
函数
| 函数 | 参数 | 示例 |
|---|---|---|
now | format="[year]-[month]-[day]...", tz="+08:00", utc=true, timestamp=true | {{ now() }}、{{ now(timestamp=true) }} |
get_env | name="VAR", default="..." | {{ get_env(name="HOME", default="") }} |
js() 过滤器:内联 JavaScript 变换
js 是代理数组的内联变换过滤器,经 thurgio-script(boa_engine 0.21)执行用户 JS 片段,适合在模板内完成 filter / rename 规则之外的临时加工与字段归一。
用法
{# 基础写法:src 内为完整 JS 函数体,$input 为本次输入 #}
{{ l.proxies | js(src="return $input.length;") }}
{# 带参数传递:模板变量可拼进 src,或经 tera 变量传入 #}
{% set script = "return $input.filter(p => p.type !== 'ss');" %}
{{ l.proxies | js(src=script) | v2rayn() }}
Tera 注册见 crates/thurgio-renderer/src/filters/js.rs:13 的 js 函数(register_all 统一注册),调用为 src="..." 关键字参数;缺失 src 时渲染报错(同文件 js:14 的校验)。
$input 结构
$input 为本次过滤器输入的 JSON 镜像(JsValue::from_json 注入,crates/thurgio-script/src/engine.rs:55),在 provider 渲染中即 l.proxies 的代理对象数组。每个元素为 ProxyTy 的序列化形态(crates/thurgio-proxy/src/proxy/mod.rs:436 的 Serialize for ProxyTy),常见顶层字段:
| 字段 | 说明 |
|---|---|
name / type / server / port | 必有基础字段(与模板中 l.proxies[].name 等一致) |
cipher / password / uuid / alterId | 协议相关字段(ss / vmess 等按类型出现) |
tls / network / ws-opts / grpc-opts | 传输与 TLS 字段(vmess / vless 等按需出现) |
udp | UDP 覆盖标志(可选) |
字段集合随代理类型不同而异,以实际序列化结果为准;模板中
{{ l.proxies | json_str(pretty=true) }}可直观查看当前数据的可用字段。
输入与输出均经 serde_json::Value 往返(crates/thurgio-script/src/engine.rs:74 的 to_json),未定义字段保留原样,返回结构可直接链给下游过滤器(如 | js(...) | v2rayn() / | cla() | json_str())。
约束
- 必须显式
return:脚本体以(function() { <src> })()形式求值(crates/thurgio-script/src/engine.rs:66的wrapped),undefined结果视为错误user script must return a value(同文件engine.rs:71),缺失return会导致渲染失败(ErrorKind::Render,见crates/thurgio-script/src/engine.rs:37与crates/thurgio-script/src/engine.rs:178的missing_return_is_err)。 - boa_engine 沙箱:每次调用新建独立
Context(crates/thurgio-script/src/engine.rs:53),$input突变不外泄、不跨调用共享(同文件engine.rs:187的input_not_shared_between_calls)。默认上下文仅提供 ECMAScript 内置对象(Array/Object/JSON/Math等),未挂载fetch/ 文件系统 / 进程等宿主能力,无网络与文件 I/O;但 v1 未提供执行超时与强沙箱隔离(crates/thurgio-script/src/lib.rs:11的信任模型),脚本来自受信模板,勿直接执行不可信第三方脚本(design/crate-notes.md:44)。 - 错误归类:语法错误、运行时
throw、输入/输出 JSON 转换失败均映射为ErrorKind::Render并以字符串透传给 Tera(crates/thurgio-script/src/engine.rs:67、crates/thurgio-renderer/src/filters/js.rs:23),渲染管线以 422/500 形式返回。
实用示例
示例 1 — 过滤 + 改名(筛掉 ss、其余加前缀):
{# tpls/filtered.tpl — 仅保留非 ss 节点并统一加前缀 #}
{{ l.proxies | js(src="
return $input
.filter(p => p.type !== 'ss')
.map(p => {
p.name = '[FILTERED] ' + p.name;
return p;
});
") | v2rayn() }}
等价于组合 filter / rename 规则的内联写法,适合一次性、客户端特定的临时逻辑,无需在 filter.yaml / rename.yaml 新增规则。
示例 2 — 字段归一(兼容历史字段):
{# 将历史数据中的 legacy_port 归一到 port,归一后走 clash 输出 #}
{{ l.proxies | js(src="
return $input.map(p => {
if (p.legacy_port) { p.port = p.legacy_port; delete p.legacy_port; }
return p;
});
") | cla() | json_str(pretty=true) | indent(width=2) }}
更复杂的变换可抽为独立 .js 片段经 include_str! 或模板变量拼装;链式组合时 js 可位于任意位置,注意后续过滤器的输入类型需匹配(如 cla 期望代理数组)。
渲染上下文速查
| 变量 | 可用范围 | 说明 |
|---|---|---|
l.proxies | provider | 代理对象数组(每个元素有 name / type / server / port 等字段) |
l.name / l.rename / l.filter | provider | 名称 / 配置层规则名 |
s.response.content | snippet | snippet 源内容(原样文本) |
p.* | 全部 | 自定义参数(params.yaml + /app/admin/p 热更新) |
hp.* | 全部 | 本次 HTTP 请求的查询参数(与 p 分离,ADR-081) |
config | 全部 | 完整配置对象(config.snippet、config.params…) |
headers / version | 全部 | 请求头 / 版本字符串 |