feat: 添加 WireGuard 设备管理并完善客户端分流

实现独立加密设备身份、网关 agent、持久生命周期任务和 Surge/FlClash 专属订阅凭证。

完善 Shadowrocket 规则转换与 Clash AI DNS,增加 Surge FFSB 适配、规则快照、运维工具及验收文档。
This commit is contained in:
YANG JIANKUAN
2026-11-02 22:44:23 +08:00
parent f741aeeccd
commit a5d980a5e9
80 changed files with 4777 additions and 51 deletions

64
scripts/ffsb/overlay.py Normal file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Build a device-local Surge profile. Never modify the managed source profile."""
import argparse
import json
import re
from pathlib import Path
def sections(text):
result = {}
current = None
for line in text.splitlines():
match = re.fullmatch(r'\[([^]]+)\]', line.strip())
if match:
current = match[1]
result[current] = []
elif current is not None:
result[current].append(line)
return result
def build(base, domains, enabled):
if len(domains) != 25 or len(set(domains)) != 25:
raise ValueError('FFSB scope changed; re-audit before applying')
s = sections(base)
for key in ['General', 'Proxy', 'Proxy Group', 'Rule', 'Host']:
if key not in s:
raise ValueError('Missing section: ' + key)
groups = s['Proxy Group']
original = [x for x in groups if re.match(r'^(?:Claude|"?🦀 Claude"?)\s*=', x)]
if len(original) != 1:
raise ValueError('Expected one production Claude group')
if original[0].startswith('Claude'):
groups[groups.index(original[0])] = original[0].replace('Claude', '"🦀 Claude"', 1)
groups.insert(0, 'Claude = fallback, ' + ('FFSB, ' if enabled else '') + '"🦀 Claude", interval=5')
s['Proxy'].insert(0, 'FFSB = direct, interface=utun100, allow-other-interface=false, dns-follow-interface=true, test-url=https://api.ipify.org')
# The domain union goes to the Surge fallback unless overridden by the exact FFSB scope.
s['Rule'] = [re.sub(r',"?Claude"?(?=,|$)', ',"🦀 Claude"', line) for line in s['Rule']]
scope = [('DOMAIN-WILDCARD' if d.startswith('*.') else 'DOMAIN') + ',' + d + ',Claude' for d in domains]
outer = 'PROCESS-NAME,/Applications/FFSB.app/Contents/MacOS/ffsb-service,DIRECT'
s['Rule'] = [outer] + scope + [line for line in s['Rule'] if line != outer]
if enabled:
s['Host'] = [d + ' = server:100.70.255.254' for d in domains] + s['Host']
for i, line in enumerate(s['General']):
if re.match(r'^always-real-ip\s*=', line):
s['General'][i] = line + ', ' + ', '.join(domains)
break
else:
s['General'].append('always-real-ip = ' + ', '.join(domains))
return '# Local Claude overlay; source remains managed. Mode: ' + ('FFSB' if enabled else 'Surge') + '\n\n' + '\n\n'.join('['+key+']\n'+'\n'.join(lines) for key, lines in s.items()) + '\n'
def main():
parser = argparse.ArgumentParser()
parser.add_argument('base', type=Path)
parser.add_argument('domains', type=Path)
parser.add_argument('output', type=Path)
parser.add_argument('--ffsb', action='store_true')
args = parser.parse_args()
args.output.write_text(build(args.base.read_text(), json.loads(args.domains.read_text()), args.ffsb))
if __name__ == '__main__':
main()