#!/usr/bin/env python3 import argparse import base64 import json import os import re import sys def b64_decode_flexible(s): t = "".join(s.strip().split()) pad = (-len(t)) % 4 t = t + ("=" * pad) try: return base64.b64decode(t).decode("utf-8", errors="replace") except Exception: return base64.urlsafe_b64decode(t).decode("utf-8", errors="replace") def b64_encode_std(s): return base64.b64encode(s.encode("utf-8")).decode("ascii") def normalize_vmess_payload(payload): clean = "".join(payload.strip().split()) pad = (-len(clean)) % 4 return clean + ("=" * pad) def update_vmess_line(line, domain, name_rx=None): stripped = line.strip() if not stripped.startswith("vmess://"): return line, False, None payload = stripped[len("vmess://") :] try: body = b64_decode_flexible(normalize_vmess_payload(payload)) obj = json.loads(body) except Exception as e: return line, False, f"decode_error: {e}" ps = str(obj.get("ps", "")) if name_rx is not None and not name_rx.search(ps): return line, False, None if str(obj.get("add", "")).strip() == domain: return line, False, None obj["add"] = domain encoded = b64_encode_std(json.dumps(obj, ensure_ascii=False, separators=(",", ":"))) return f"vmess://{encoded}", True, None def read_domain(args): if args.domain: return args.domain.strip() if not args.domain_file: raise ValueError("must provide --domain or --domain-file") with open(args.domain_file, "r", encoding="utf-8") as f: return f.read().strip() def main(): ap = argparse.ArgumentParser(description="Update vmess:// links by replacing add field") ap.add_argument("--input", required=True, help="Input subscription file") ap.add_argument("--output", required=True, help="Output subscription file") ap.add_argument("--domain", default="", help="Target domain to set as add") ap.add_argument("--domain-file", default="./runtime/current_domain.txt", help="Domain file path") ap.add_argument("--name-regex", default="", help="Only update nodes whose ps matches regex") ap.add_argument( "--subscription-base64", action="store_true", help="Input/output file is base64-encoded full subscription content", ) args = ap.parse_args() domain = read_domain(args) if not domain: print(json.dumps({"status": "error", "error": "empty domain"}, ensure_ascii=True), file=sys.stderr) sys.exit(1) name_rx = re.compile(args.name_regex) if args.name_regex else None with open(args.input, "r", encoding="utf-8") as f: raw_input = f.read() content = raw_input if args.subscription_base64: content = b64_decode_flexible(raw_input) lines = content.splitlines() out_lines = [] total_vmess = 0 updated = 0 errors = 0 for line in lines: if line.strip().startswith("vmess://"): total_vmess += 1 new_line, changed, err = update_vmess_line(line, domain, name_rx=name_rx) if changed: updated += 1 if err: errors += 1 out_lines.append(new_line) out_text = "\n".join(out_lines) if content.endswith("\n"): out_text += "\n" write_text = out_text if args.subscription_base64: write_text = b64_encode_std(out_text) os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True) with open(args.output, "w", encoding="utf-8") as f: f.write(write_text) result = { "status": "ok", "domain": domain, "total_vmess": total_vmess, "updated": updated, "errors": errors, "output": args.output, } print(json.dumps(result, ensure_ascii=True)) if __name__ == "__main__": main()