docs: 联网搜索服务架构方案全套(plan-final/design-arch/选型决策/整合导览/MCP文档/部署预设/联调手册) bench: 5 方案 + 代理 + 站点矩阵本机实测工程(无密钥) 部署目标:primary mgr1 先行测试(待批准后执行)
148 lines
4.9 KiB
Python
Executable file
148 lines
4.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Sanitized Clash-sub helpers. Never print or persist credentials."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import collections
|
|
import json
|
|
import os
|
|
import re
|
|
import urllib.parse
|
|
from typing import Any
|
|
|
|
INFO_NAME_RE = re.compile(r"^(剩余流量|套餐到期|到期|流量)")
|
|
|
|
REGION_RULES: list[tuple[str, list[str]]] = [
|
|
("香港", ["香港", "HK", "🇭🇰"]),
|
|
("台湾", ["台湾", "臺灣", "🇹🇼"]),
|
|
("日本", ["日本", "东京", "大阪", "🇯🇵"]),
|
|
("韩国", ["韩国", "韓國", "首尔", "🇰🇷"]),
|
|
("新加坡", ["新加坡", "狮城", "🇸🇬"]),
|
|
("美国", ["美国", "美國", "圣何塞", "洛杉矶", "硅谷", "西雅图", "芝加哥", "纽约", "🇺🇸"]),
|
|
("英国", ["英国", "英國", "伦敦", "🇬🇧"]),
|
|
("德国", ["德国", "德國", "法兰克福", "🇩🇪"]),
|
|
("法国", ["法国", "法國", "巴黎", "🇫🇷"]),
|
|
("加拿大", ["加拿大", "🇨🇦"]),
|
|
("澳大利亚", ["澳大利亚", "澳洲", "🇦🇺"]),
|
|
("澳门", ["澳门", "澳門", "🇲🇴"]),
|
|
("中国", ["中国", "回国"]),
|
|
]
|
|
|
|
|
|
def infer_region(name: str) -> str:
|
|
for label, keys in REGION_RULES:
|
|
for key in keys:
|
|
if key in name:
|
|
return label
|
|
return "其他/未知"
|
|
|
|
|
|
def is_info_node(name: str) -> bool:
|
|
return bool(INFO_NAME_RE.search(name))
|
|
|
|
|
|
def parse_flow_proxies(text: str) -> list[dict[str, str]]:
|
|
proxies: list[dict[str, str]] = []
|
|
for line in text.splitlines():
|
|
if "name:" not in line or "type:" not in line or "server:" not in line:
|
|
continue
|
|
name_m = re.search(r"name:\s*'([^']+)'|name:\s*([^,}]+)", line)
|
|
type_m = re.search(r"type:\s*([a-zA-Z0-9-]+)", line)
|
|
if not name_m or not type_m:
|
|
continue
|
|
name = (name_m.group(1) or name_m.group(2)).strip()
|
|
proxies.append(
|
|
{
|
|
"name": name,
|
|
"type": type_m.group(1),
|
|
"region": infer_region(name),
|
|
}
|
|
)
|
|
return proxies
|
|
|
|
|
|
def detect_format(text: str) -> str:
|
|
head = text.lstrip()[:400]
|
|
if "proxies:" in text[:4000] and (
|
|
"mixed-port:" in head or "port:" in head or head.startswith("proxies:")
|
|
):
|
|
return "clash-yaml"
|
|
if any(head.startswith(s) for s in ("ss://", "vmess://", "trojan://", "vless://")):
|
|
return "uri-list"
|
|
compact = re.sub(r"\s+", "", text)
|
|
if compact and re.fullmatch(r"[A-Za-z0-9+/=_-]+", compact[:80] or ""):
|
|
return "base64-maybe"
|
|
return "other"
|
|
|
|
|
|
def rewrite_runtime_config(src: str, mixed_port: int, controller: str) -> str:
|
|
"""Rewrite listen ports / controller; strip original rules to MATCH selector."""
|
|
lines = src.splitlines()
|
|
out: list[str] = []
|
|
skipping_rules = False
|
|
selector = "🚀节点选择"
|
|
for line in lines:
|
|
if line.startswith("proxy-groups:"):
|
|
skipping_rules = False
|
|
if line.startswith("rules:"):
|
|
skipping_rules = True
|
|
continue
|
|
if skipping_rules:
|
|
continue
|
|
if line.startswith("mixed-port:"):
|
|
out.append(f"mixed-port: {mixed_port}")
|
|
continue
|
|
if line.startswith("allow-lan:"):
|
|
out.append("allow-lan: true")
|
|
continue
|
|
if line.startswith("external-controller:"):
|
|
out.append(f"external-controller: '{controller}'")
|
|
continue
|
|
if line.startswith("bind-address:"):
|
|
out.append("bind-address: '*'")
|
|
continue
|
|
out.append(line)
|
|
out.append("rules:")
|
|
out.append(f" - MATCH,{selector}")
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
def mask_secret(value: str, keep: int = 4) -> str:
|
|
if not value:
|
|
return ""
|
|
return value[:keep] + "***"
|
|
|
|
|
|
def summarize_subscription(text: str) -> dict[str, Any]:
|
|
fmt = detect_format(text)
|
|
proxies = parse_flow_proxies(text)
|
|
real = [p for p in proxies if not is_info_node(p["name"])]
|
|
info = [p for p in proxies if is_info_node(p["name"])]
|
|
return {
|
|
"format": fmt,
|
|
"proxy_count_raw": len(proxies),
|
|
"proxy_count_real": len(real),
|
|
"info_nodes": [p["name"] for p in info],
|
|
"types": dict(collections.Counter(p["type"] for p in real)),
|
|
"regions": dict(collections.Counter(p["region"] for p in real)),
|
|
"proxies": real,
|
|
}
|
|
|
|
|
|
def api_quote(name: str) -> str:
|
|
return urllib.parse.quote(name, safe="")
|
|
|
|
|
|
def require_sub_url() -> str:
|
|
url = os.environ.get("PROXY_SUB_URL", "").strip()
|
|
if not url:
|
|
raise SystemExit("PROXY_SUB_URL is required (do not hardcode the URL)")
|
|
if not url.startswith(("http://", "https://")):
|
|
raise SystemExit("PROXY_SUB_URL must be an http(s) URL")
|
|
return url
|
|
|
|
|
|
def dump_json(path: str, obj: Any) -> None:
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
json.dump(obj, fh, ensure_ascii=False, indent=2)
|
|
fh.write("\n")
|