实现独立加密设备身份、网关 agent、持久生命周期任务和 Surge/FlClash 专属订阅凭证。 完善 Shadowrocket 规则转换与 Clash AI DNS,增加 Surge FFSB 适配、规则快照、运维工具及验收文档。
65 lines
2.8 KiB
Python
65 lines
2.8 KiB
Python
#!/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()
|