1.Compose.yaml
services:
sub-store:
image: xream/sub-store:latest
container_name: sub-store
restart: always
volumes:
- ./data:/opt/app/data
environment:
- SUB_STORE_FRONTEND_BACKEND_PATH=/oSa0gNr9tMYtdcaqteo8
ports:
- 3001:3001
stdin_open: true
tty: true
2.节点清洗
function operator(proxies = [], targetPlatform, context) {
// === 第一阶段:建立 "IP -> 域名" 的映射字典 ===
const ipToDomainMap = {};
proxies.forEach(p => {
if (p.server && p.servername) {
ipToDomainMap[p.server] = p.servername;
}
});
// === 第二阶段:遍历处理并返回全新排序的节点数组 ===
return proxies.map(p => {
// 1. 基础修正
p['skip-cert-verify'] = true;
p.udp = true;
// 清理冗余字段
delete p._subName;
delete p._subDisplayName;
delete p._collectionName;
delete p._collectionDisplayName;
// 2. 协议专项定制
if (p.type === 'tuic') {
p.version = 5;
p.tls = true;
delete p['udp-relay-mode'];
// TUIC 依赖 QUIC,优先 h3
p.alpn = ['h3', 'h2', 'http/1.1'];
}
if (p.type === 'hysteria2') {
p.tls = true;
// Hy2 依赖 QUIC,优先 h3
p.alpn = ['h3', 'h2', 'http/1.1'];
}
if (p.type === 'trojan') {
p.tls = true;
if (p.network === 'ws') {
const domain = ipToDomainMap[p.server];
if (domain) {
p.port = 443;
p.servername = domain;
p.sni = domain;
// 核心修改:WebSocket 强绑 http/1.1
p.alpn = ['http/1.1'];
if (!p['ws-opts']) p['ws-opts'] = {};
if (!p['ws-opts'].headers) p['ws-opts'].headers = {};
p['ws-opts'].headers['Host'] = domain;
}
}
}
// 3. 自动加国旗
const match = p.name.match(/^([a-zA-Z0-9]+)-([a-zA-Z]{2})-(.+)$/);
if (match) {
const countryCode = match[2];
const flag = ProxyUtils.getFlag(countryCode) || '🌍';
p.name = `${flag} ${p.name}`;
}
// === 第三阶段:终极格式化 (拯救强迫症) ===
const orderedProxy = {};
// 定义你期望的完美 YAML 字段输出顺序
const perfectOrder = [
'name', 'type', 'server', 'port', 'password', 'uuid',
'network', 'tls', 'servername', 'sni', 'alpn',
'skip-cert-verify', 'udp', 'version', 'up', 'down', 'congestion-controller',
'ws-opts'
];
// 按照完美顺序挑拣属性
perfectOrder.forEach(key => {
if (p[key] !== undefined) {
orderedProxy[key] = p[key];
}
});
// 把其他没考虑到的生僻属性补在最后面,防止丢数据
Object.keys(p).forEach(key => {
if (orderedProxy[key] === undefined) {
orderedProxy[key] = p[key];
}
});
return orderedProxy;
});
}
3. 基本配置调用
# ================= 基础设置 =================
mixed-port: 7890
allow-lan: true
bind-address: '*'
mode: rule
log-level: info
ipv6: false
external-controller: '0.0.0.0:9090'
# ================= DNS 配置 =================
dns:
enable: true
listen: 0.0.0.0:1053
ipv6: false
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
default-nameserver:
- 114.114.114.114
- 119.29.29.29
- 223.5.5.5
nameserver:
- https://doh.pub/dns-query
- https://dns.alidns.com/dns-query
- 223.5.5.5
- 119.29.29.29
fallback:
- https://dns.google/dns-query
- https://cloudflare-dns.com/dns-query
fallback-filter:
geoip: true
geoip-code: CN
ipcidr:
- 240.0.0.0/4
# ================= 节点配置 =================
proxies: []
# ================= 策略组配置 =================
proxy-groups:
# 1. 主干选择
- name: "🚀 节点选择"
type: select
proxies:
- "⚡ 全局最低延迟"
# (我们会在最终脚本里,把生成的地区组动态插在这里)
- "🎯 全球直连"
# 2. 全局自动优选框架
- name: "⚡ 全局最低延迟"
type: url-test
url: http://cp.cloudflare.com/generate_204
interval: 300
tolerance: 50
proxies: [] # (最终脚本会自动把所有节点填入这里)
# 4. 辅助兜底组
- name: "🎯 全球直连"
type: select
proxies:
- DIRECT
- name: "🐟 漏网之鱼"
type: select
proxies:
- "🚀 节点选择"
- "🎯 全球直连"
# ================= 路由规则 =================
rules:
- DOMAIN-SUFFIX,local,🎯 全球直连
- DOMAIN-SUFFIX,localhost,🎯 全球直连
- IP-CIDR,127.0.0.0/8,🎯 全球直连,no-resolve
- IP-CIDR,192.168.0.0/16,🎯 全球直连,no-resolve
- IP-CIDR,10.0.0.0/8,🎯 全球直连,no-resolve
- IP-CIDR,172.16.0.0/12,🎯 全球直连,no-resolve
- IP-CIDR,100.64.0.0/10,🎯 全球直连,no-resolve
- IP-CIDR6,::1/128,🎯 全球直连,no-resolve
- IP-CIDR6,fc00::/7,🎯 全球直连,no-resolve
- IP-CIDR6,fe80::/10,🎯 全球直连,no-resolve
- DOMAIN-KEYWORD,gemini,🚀 节点选择
- DOMAIN-SUFFIX,generativelanguage.googleapis.com,🚀 节点选择
- DOMAIN-SUFFIX,googleapis.com,🚀 节点选择
- GEOSITE,google,🚀 节点选择
- GEOSITE,openai,🚀 节点选择
- DOMAIN-SUFFIX,ai.com,🚀 节点选择
- GEOSITE,apple,🎯 全球直连
- DOMAIN-SUFFIX,apple.com,🎯 全球直连
- DOMAIN-SUFFIX,icloud.com,🎯 全球直连
- DOMAIN-SUFFIX,mzstatic.com,🎯 全球直连
- GEOSITE,cn,🎯 全球直连
- GEOIP,CN,🎯 全球直连
- MATCH,🐟 漏网之鱼
4. 节点组合
// 1. 读取基础配置
const baseStr = await produceArtifact({ type: 'file', name: '第 2 步节点清洗文件名称' });
const yamlObj = ProxyUtils.yaml.safeLoad(baseStr);
// 2. 获取清洗完毕的节点
let clashMetaProxies = await produceArtifact({
type: 'collection',
name: '组合订阅文件名称',
platform: 'ClashMeta',
produceType: 'internal'
});
// 写入节点列表
yamlObj.proxies = clashMetaProxies;
const allNodeNames = clashMetaProxies.map(p => p.name);
// 3. 实时解析节点名称并进行动态分组 (完美替代原有的黑科技)
const regionGroupsMap = {};
clashMetaProxies.forEach(p => {
// 此时的 p.name 已经是加过国旗的,例如 "🇯🇵 AWS-JP-VLESS"
// 我们用正则拆解出:Flag="🇯🇵", Provider="AWS", CountryCode="JP"
const match = p.name.match(/^(.*?)\s+([a-zA-Z0-9]+)-([a-zA-Z]{2})-(.+)$/);
if (match) {
const flag = match[1];
const provider = match[2];
const countryCode = match[3];
// 实时拼装出组名
const groupName = `${flag} ${provider}-${countryCode}优选`;
// 归类到对象中
if (!regionGroupsMap[groupName]) {
regionGroupsMap[groupName] = [];
}
regionGroupsMap[groupName].push(p.name);
}
});
// 4. 将分组和节点注入到策略组中
const selectGroup = yamlObj['proxy-groups'].find(g => g.name === '🚀 节点选择');
const urlTestGroup = yamlObj['proxy-groups'].find(g => g.name === '⚡ 全局最低延迟');
// A. 填充“全局最低延迟”
urlTestGroup.proxies = allNodeNames;
// B. 动态生成地区优选组,并插入配置中
const regionGroupNames = Object.keys(regionGroupsMap);
regionGroupNames.forEach(groupName => {
yamlObj['proxy-groups'].push({
name: groupName,
type: 'url-test',
url: 'http://cp.cloudflare.com/generate_204',
interval: 300,
tolerance: 50,
proxies: regionGroupsMap[groupName]
});
// 插入到 "🚀 节点选择" 中 (保持在 '🎯 全球直连' 前面)
const directIndex = selectGroup.proxies.indexOf('🎯 全球直连');
if (directIndex !== -1) {
selectGroup.proxies.splice(directIndex, 0, groupName);
} else {
selectGroup.proxies.push(groupName);
}
});
// 5. 导出干净的最终配置!
$content = ProxyUtils.yaml.dump(yamlObj);