Compare commits

..

No commits in common. "ededd6f138c8ee6444bacdccf4c2ce9838c46b0a" and "ac3e2a71f137fa67b888e71b02e00e680e1e5c7f" have entirely different histories.

3 changed files with 809 additions and 803 deletions

View File

@ -49,4 +49,3 @@
[2026-06-24 20:53:59] [WARN] 未能获取新的更新内容
[2026-06-25 09:01:50] [WARN] 未能获取新的更新内容
[2026-06-25 20:55:17] [WARN] 未能获取新的更新内容
[2026-06-26 09:04:28] [WARN] 未能获取新的更新内容

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

145
main.py
View File

@ -1,4 +1,3 @@
import base64
import os
import re
import smtplib
@ -42,36 +41,28 @@ def _extract_urls(summary):
return urls, decoded
_NODE_SCHEME_RE = re.compile(r"(?:vmess|vless|trojan|ss|ssr|hysteria2?|tuic)://")
def _pick_url(urls, mode):
if mode == "v2ray":
for suffix in (".txt", ".json"):
for url in urls:
if url.lower().endswith(suffix):
return url
if mode == "clash":
for suffix in (".yaml", ".yml"):
for url in urls:
if url.lower().endswith(suffix):
return url
return ""
def _b64decode(text):
compact = re.sub(r"\s+", "", text)
if not compact:
return ""
try:
# binascii.Error 是 ValueError 的子类,统一捕获即可
raw = base64.b64decode(compact + "=" * (-len(compact) % 4))
except ValueError:
return ""
return raw.decode("utf-8", "ignore")
def _detect_kind(text):
"""根据下载内容判断订阅类型:'clash' / 'v2ray',无法识别返回 None。"""
sample = text.strip()
if not sample:
return None
# clash 配置为 YAML包含 proxies/proxy-groups 字段
if re.search(r"^(?:proxies|proxy-groups)\s*:", sample, re.MULTILINE):
return "clash"
# v2ray 订阅为节点 URI 列表,可能是明文或 base64 编码
if _NODE_SCHEME_RE.search(sample):
return "v2ray"
decoded = _b64decode(sample)
if decoded and _NODE_SCHEME_RE.search(decoded):
return "v2ray"
return None
def _pick_urls(urls, mode):
matched = []
suffixes = (".txt", ".json") if mode == "v2ray" else (".yaml", ".yml")
for suffix in suffixes:
for url in urls:
if url.lower().endswith(suffix) and url not in matched:
matched.append(url)
return matched
def _download_with_retry(urls):
@ -117,25 +108,19 @@ def _build_session():
return session
def _classify_subscriptions(session, urls):
"""逐个下载候选链接并按内容判断类型,返回 {'v2ray': (req, url), 'clash': (req, url)}。"""
found = {}
def _download_candidates(session, urls):
if not urls:
return None, None
for url in urls:
if "v2ray" in found and "clash" in found:
break
try:
req = session.get(url, verify=False, timeout=20)
except requests.RequestException as e:
write_log(f"请求失败:{url} - {e}", "WARN")
continue
if req.status_code not in ok_code:
write_log(f"请求失败:{url} - {req.status_code}", "WARN")
continue
kind = _detect_kind(req.text)
if kind and kind not in found:
found[kind] = (req, url)
write_log(f"识别到 {kind} 订阅:{url}", "INFO")
return found
if req.status_code in ok_code:
return req, url
write_log(f"请求失败:{url} - {req.status_code}", "WARN")
return None, urls[0]
def get_subscribe_url():
dirs = './subscribe'
@ -176,40 +161,62 @@ def get_subscribe_url():
write_log("暂时没有可用的订阅更新", "WARN")
return
urls, _ = _extract_urls(summary)
urls, decoded_summary = _extract_urls(summary)
# 链接已无固定后缀,需下载内容后再判断是 v2ray 还是 clash
classified = _classify_subscriptions(session, urls)
v2ray_url = _pick_url(urls, "v2ray")
clash_url = _pick_url(urls, "clash")
v2ray_candidates = _pick_urls(urls, "v2ray")
clash_candidates = _pick_urls(urls, "clash")
# 兼容旧页面结构,通用提取失败时再尝试历史规则
if not v2ray_url:
v2ray_list = re.findall(r">V2Ray/XRay -&gt; (.*?)</span>", summary)
if not v2ray_list:
v2ray_list = re.findall(r">V2Ray/XRay -> (.*?)</span>", decoded_summary)
if any(v2ray_list):
v2ray_url = v2ray_list[-1].replace('amp;', '')
if v2ray_url not in v2ray_candidates:
v2ray_candidates.append(v2ray_url)
if not clash_url:
clash_list = re.findall(r">clash -&gt; (.*?)</span>", summary)
if not clash_list:
clash_list = re.findall(r">clash -> (.*?)</span>", decoded_summary)
if any(clash_list) and not clash_list[-1].startswith("订阅地址生成失败"):
clash_url = clash_list[-1].replace('amp;', '')
if clash_url not in clash_candidates:
clash_candidates.append(clash_url)
# 获取普通订阅链接
v2ray_entry = classified.get("v2ray")
if v2ray_entry:
v2ray_req, _ = v2ray_entry
update_list.append(f"v2ray: {v2ray_req.status_code}")
with open(dirs + '/v2ray.txt', 'w', encoding="utf-8") as f:
f.write(v2ray_req.text)
else:
cache_file = dirs + '/v2ray.txt'
if os.path.exists(cache_file) and os.path.getsize(cache_file) > 0:
update_list.append("v2ray: cache")
write_log("未获取到 v2ray 订阅,已保留本地缓存", "WARN")
if v2ray_url:
v2ray_req, used_v2ray_url = _download_candidates(session, v2ray_candidates)
if not v2ray_req:
cache_file = dirs + '/v2ray.txt'
if os.path.exists(cache_file) and os.path.getsize(cache_file) > 0:
update_list.append("v2ray: cache")
write_log(f"获取 v2ray 订阅失败,已保留本地缓存:{used_v2ray_url}", "WARN")
else:
write_log(f"获取 v2ray 订阅失败:{used_v2ray_url}", "WARN")
else:
write_log("未获取到 v2ray 订阅", "WARN")
update_list.append(f"v2ray: {v2ray_req.status_code}")
with open(dirs + '/v2ray.txt', 'w', encoding="utf-8") as f:
f.write(v2ray_req.text)
# 获取clash订阅链接
clash_entry = classified.get("clash")
if clash_entry:
clash_req, _ = clash_entry
update_list.append(f"clash: {clash_req.status_code}")
with open(dirs + '/clash.yml', 'w', encoding="utf-8") as f:
f.write(clash_req.content.decode("utf-8"))
else:
cache_file = dirs + '/clash.yml'
if os.path.exists(cache_file) and os.path.getsize(cache_file) > 0:
update_list.append("clash: cache")
write_log("未获取到 clash 订阅,已保留本地缓存", "WARN")
if clash_url and not clash_url.startswith("订阅地址生成失败"):
clash_req, used_clash_url = _download_candidates(session, clash_candidates)
if not clash_req:
cache_file = dirs + '/clash.yml'
if os.path.exists(cache_file) and os.path.getsize(cache_file) > 0:
update_list.append("clash: cache")
write_log(f"获取 clash 订阅失败,已保留本地缓存:{used_clash_url}", "WARN")
else:
write_log(f"获取 clash 订阅失败:{used_clash_url}", "WARN")
else:
write_log("未获取到 clash 订阅", "WARN")
update_list.append(f"clash: {clash_req.status_code}")
with open(dirs + '/clash.yml', 'w', encoding="utf-8") as f:
clash_content = clash_req.content.decode("utf-8")
f.write(clash_content)
if update_list:
file_pat = re.compile(r"v2ray\.txt|clash\.yml")
if file_pat.search(os.popen("git status").read()):