update
This commit is contained in:
298
.codex_tmp_mihomo_config.yaml
Normal file
298
.codex_tmp_mihomo_config.yaml
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
allow-lan: true
|
||||||
|
mode: rule
|
||||||
|
bind-address: "0.0.0.0"
|
||||||
|
mixed-port: 7891
|
||||||
|
socks-port: 7890
|
||||||
|
ipv6: true
|
||||||
|
unified-delay: true
|
||||||
|
log-level: warning
|
||||||
|
find-process-mode: strict
|
||||||
|
tcp-concurrent: true
|
||||||
|
keep-alive-idle: 600
|
||||||
|
keep-alive-interval: 15
|
||||||
|
|
||||||
|
profile:
|
||||||
|
store-selected: true
|
||||||
|
store-fake-ip: true
|
||||||
|
|
||||||
|
external-controller: 0.0.0.0:9090
|
||||||
|
|
||||||
|
tun:
|
||||||
|
enable: true
|
||||||
|
stack: mixed
|
||||||
|
auto-route: true
|
||||||
|
auto-detect-interface: true
|
||||||
|
dns-hijack:
|
||||||
|
- any:53
|
||||||
|
|
||||||
|
NodeParam: &NodeParam
|
||||||
|
type: http
|
||||||
|
interval: 86400
|
||||||
|
health-check:
|
||||||
|
enable: true
|
||||||
|
url: https://www.gstatic.com/generate_204
|
||||||
|
interval: 300
|
||||||
|
|
||||||
|
proxy-providers:
|
||||||
|
airport:
|
||||||
|
<<: *NodeParam
|
||||||
|
url: "http://172.30.0.2:25500/sub?target=clash&new_name=true&url=https%3A%2F%2Fdash.pqjc.site%2Fapi%2Fv1%2Fpq%2Fdce38d7c4a39d9b8fb6818a658e2f554"
|
||||||
|
path: ./providers/airport.yaml
|
||||||
|
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
cache-algorithm: arc
|
||||||
|
listen: 0.0.0.0:1053
|
||||||
|
ipv6: true
|
||||||
|
respect-rules: true
|
||||||
|
use-hosts: true
|
||||||
|
use-system-hosts: true
|
||||||
|
enhanced-mode: fake-ip
|
||||||
|
fake-ip-range: 198.18.0.1/16
|
||||||
|
fake-ip-filter-mode: blacklist
|
||||||
|
fake-ip-filter:
|
||||||
|
- rule-set:fakeipfilter
|
||||||
|
nameserver:
|
||||||
|
- https://223.5.5.5/dns-query
|
||||||
|
- https://doh.pub/dns-query
|
||||||
|
proxy-server-nameserver:
|
||||||
|
- https://223.5.5.5/dns-query
|
||||||
|
- https://doh.pub/dns-query
|
||||||
|
|
||||||
|
sniffer:
|
||||||
|
enable: true
|
||||||
|
parse-pure-ip: true
|
||||||
|
sniff:
|
||||||
|
HTTP:
|
||||||
|
ports: [80, 8080-8880]
|
||||||
|
override-destination: true
|
||||||
|
TLS:
|
||||||
|
ports: [443, 8443]
|
||||||
|
QUIC:
|
||||||
|
ports: [443, 8443]
|
||||||
|
skip-domain:
|
||||||
|
- "Mijia Cloud"
|
||||||
|
- "+.push.apple.com"
|
||||||
|
|
||||||
|
FilterUS: &FilterUS '^(?=.*?(?i)(United States|America|\bUS\b|硅谷|家宽|洛杉矶|西雅图|纽约|芝加哥|拉斯维加斯|美帝|美国)).*(?<!(?i)(回国|过期|剩余|官网|网址|节点|订阅|机场|客服|校园|本土)).*$'
|
||||||
|
FilterJP: &FilterJP '^(?=.*?(?i)(Japan|\bJP\b|东京|大阪|埼玉|川日|泉日|日区|日本)).*(?<!(?i)(回国|过期|剩余|官网|网址|节点|订阅|机场|客服|校园|本土)).*$'
|
||||||
|
FilterSG: &FilterSG '^(?=.*?(?i)(Singapore|\bSG\b|新加坡|狮城|狮)).*(?<!(?i)(回国|过期|剩余|官网|网址|节点|订阅|机场|客服|校园|本土)).*$'
|
||||||
|
FilterTW: &FilterTW '^(?=.*?(?i)(Taiwan|\bTW\b|\bTai(?:wan)?\b|台湾|台北|台中|台南|高雄|新竹)).*(?<!(?i)(回国|过期|剩余|官网|网址|节点|订阅|机场|客服|校园|本土)).*$'
|
||||||
|
FilterHK: &FilterHK '^(?=.*?(?i)(\bHK\b|Hong.?Kong|香港|深港|广港|港区)).*(?<!(?i)(回国|过期|剩余|官网|网址|节点|订阅|机场|客服|校园|本土)).*$'
|
||||||
|
FilterAll: &FilterAll '^(?!.*?(?i)(GB\s*[\|丨]\s*GB|GB|Traffic|Reset|Days|Left|Expire|Date|Direct|直连|套餐|流量|重置|剩余|到期|时间|频道)).*$'
|
||||||
|
|
||||||
|
Select: &Select
|
||||||
|
type: select
|
||||||
|
url: https://www.gstatic.com/generate_204
|
||||||
|
disable-udp: false
|
||||||
|
hidden: false
|
||||||
|
include-all: true
|
||||||
|
|
||||||
|
UrlTest: &UrlTest
|
||||||
|
type: url-test
|
||||||
|
url: https://www.gstatic.com/generate_204
|
||||||
|
disable-udp: false
|
||||||
|
hidden: true
|
||||||
|
include-all: true
|
||||||
|
interval: 300
|
||||||
|
lazy: true
|
||||||
|
tolerance: 50
|
||||||
|
timeout: 2000
|
||||||
|
max-failed-times: 3
|
||||||
|
|
||||||
|
FallBack: &FallBack
|
||||||
|
type: fallback
|
||||||
|
url: https://www.gstatic.com/generate_204
|
||||||
|
disable-udp: false
|
||||||
|
hidden: true
|
||||||
|
include-all: true
|
||||||
|
interval: 300
|
||||||
|
lazy: true
|
||||||
|
timeout: 2000
|
||||||
|
max-failed-times: 3
|
||||||
|
|
||||||
|
pg_FallBack_US: &pg_FallBack_US [US_FALLBACK, JP_FALLBACK, SG_FALLBACK, TW_FALLBACK, HK_FALLBACK]
|
||||||
|
pg_Proxy: &pg_Proxy [AUTO, ALL_NODES, ROTATE_RR, ROTATE_AUTO, ROTATE_NODES, DIRECT, US_AUTO, JP_AUTO, SG_AUTO, TW_AUTO, HK_AUTO, US_FALLBACK, JP_FALLBACK, SG_FALLBACK, TW_FALLBACK, HK_FALLBACK, US_NODES, JP_NODES, SG_NODES, TW_NODES, HK_NODES]
|
||||||
|
pg_Direct: &pg_Direct [DIRECT, AUTO, ALL_NODES, ROTATE_RR, ROTATE_AUTO, ROTATE_NODES, US_AUTO, JP_AUTO, SG_AUTO, TW_AUTO, HK_AUTO, US_FALLBACK, JP_FALLBACK, SG_FALLBACK, TW_FALLBACK, HK_FALLBACK, US_NODES, JP_NODES, SG_NODES, TW_NODES, HK_NODES]
|
||||||
|
|
||||||
|
proxy-groups:
|
||||||
|
- {name: AUTO, <<: *UrlTest, filter: *FilterAll}
|
||||||
|
- {name: ALL_NODES, <<: *Select, filter: *FilterAll}
|
||||||
|
- {name: ROTATE_NODES, type: select, use: [airport], filter: '^(美国01-0\.1x \| 电信联通移动推荐|美国08-0\.1x \| 电信联通移动推荐|香港01 \| 移动联通推荐|韩国 \| 高速专线-hy2|迪拜 \| 高速专线-hy2|新加坡4 \| 高速专线-hy2|日本东京3 \| 移动联通推荐-hy2)$'}
|
||||||
|
- {name: ROTATE_AUTO, type: url-test, use: [airport], filter: '^(美国01-0\.1x \| 电信联通移动推荐|美国08-0\.1x \| 电信联通移动推荐|香港01 \| 移动联通推荐|韩国 \| 高速专线-hy2|迪拜 \| 高速专线-hy2|新加坡4 \| 高速专线-hy2|日本东京3 \| 移动联通推荐-hy2)$', url: https://www.gstatic.com/generate_204, interval: 300, tolerance: 50, lazy: true, timeout: 2000, max-failed-times: 3}
|
||||||
|
- {name: ROTATE_RR, type: load-balance, strategy: round-robin, use: [airport], filter: '^(美国01-0\.1x \| 电信联通移动推荐|美国08-0\.1x \| 电信联通移动推荐|香港01 \| 移动联通推荐|韩国 \| 高速专线-hy2|迪拜 \| 高速专线-hy2|新加坡4 \| 高速专线-hy2|日本东京3 \| 移动联通推荐-hy2)$', url: https://www.gstatic.com/generate_204, interval: 300, lazy: true, timeout: 2000, max-failed-times: 3}
|
||||||
|
- {name: DEFAULT_PROXY, type: select, proxies: *pg_Proxy}
|
||||||
|
- {name: AIGC, type: select, proxies: *pg_FallBack_US}
|
||||||
|
- {name: STEAM, type: select, proxies: *pg_Direct}
|
||||||
|
- {name: ONEDRIVE, type: select, proxies: *pg_Direct}
|
||||||
|
- {name: MICROSOFT, type: select, proxies: *pg_Direct}
|
||||||
|
- {name: GITHUB, type: select, proxies: *pg_Proxy}
|
||||||
|
- {name: X, type: select, proxies: *pg_Proxy}
|
||||||
|
- {name: SONY, type: select, proxies: *pg_Proxy}
|
||||||
|
- {name: TELEGRAM, type: select, proxies: *pg_Proxy}
|
||||||
|
- {name: GOOGLE, type: select, proxies: *pg_FallBack_US}
|
||||||
|
- {name: YOUTUBE, type: select, proxies: *pg_Proxy}
|
||||||
|
- {name: FINAL, type: select, proxies: *pg_Proxy}
|
||||||
|
- {name: US_NODES, <<: *Select, filter: *FilterUS}
|
||||||
|
- {name: JP_NODES, <<: *Select, filter: *FilterJP}
|
||||||
|
- {name: SG_NODES, <<: *Select, filter: *FilterSG}
|
||||||
|
- {name: TW_NODES, <<: *Select, filter: *FilterTW}
|
||||||
|
- {name: HK_NODES, <<: *Select, filter: *FilterHK}
|
||||||
|
- {name: US_FALLBACK, <<: *FallBack, filter: *FilterUS}
|
||||||
|
- {name: JP_FALLBACK, <<: *FallBack, filter: *FilterJP}
|
||||||
|
- {name: SG_FALLBACK, <<: *FallBack, filter: *FilterSG}
|
||||||
|
- {name: TW_FALLBACK, <<: *FallBack, filter: *FilterTW}
|
||||||
|
- {name: HK_FALLBACK, <<: *FallBack, filter: *FilterHK}
|
||||||
|
- {name: US_AUTO, <<: *UrlTest, filter: *FilterUS}
|
||||||
|
- {name: JP_AUTO, <<: *UrlTest, filter: *FilterJP}
|
||||||
|
- {name: SG_AUTO, <<: *UrlTest, filter: *FilterSG}
|
||||||
|
- {name: TW_AUTO, <<: *UrlTest, filter: *FilterTW}
|
||||||
|
- {name: HK_AUTO, <<: *UrlTest, filter: *FilterHK}
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- IP-CIDR,169.254.169.254/32,REJECT,no-resolve
|
||||||
|
- DST-PORT,11434,REJECT
|
||||||
|
- DOMAIN,t-ring-fdv2.msedge.net,REJECT,no-resolve
|
||||||
|
- IP-CIDR,223.5.5.5/32,DIRECT,no-resolve
|
||||||
|
- IP-CIDR,223.6.6.6/32,DIRECT,no-resolve
|
||||||
|
- RULE-SET,custom_proxy,DEFAULT_PROXY
|
||||||
|
- RULE-SET,custom_direct,DIRECT
|
||||||
|
- RULE-SET,private_ip,DIRECT,no-resolve
|
||||||
|
- RULE-SET,private_domain,DIRECT
|
||||||
|
- RULE-SET,speedtest_domain,DIRECT
|
||||||
|
- RULE-SET,apple_domain,DIRECT
|
||||||
|
- RULE-SET,apple_ip,DIRECT,no-resolve
|
||||||
|
- RULE-SET,ai_domain,AIGC
|
||||||
|
- RULE-SET,onedrive_domain,ONEDRIVE
|
||||||
|
- RULE-SET,steam_domain,STEAM
|
||||||
|
- RULE-SET,github_domain,GITHUB
|
||||||
|
- RULE-SET,x_domain,X
|
||||||
|
- RULE-SET,telegram_ip,TELEGRAM,no-resolve
|
||||||
|
- RULE-SET,telegram_domain,TELEGRAM
|
||||||
|
- RULE-SET,sony_domain,SONY
|
||||||
|
- RULE-SET,microsoft_domain,MICROSOFT
|
||||||
|
- RULE-SET,youtube_domain,YOUTUBE
|
||||||
|
- RULE-SET,google_domain,GOOGLE
|
||||||
|
- RULE-SET,google_ip,GOOGLE,no-resolve
|
||||||
|
- RULE-SET,cn_domain,DIRECT
|
||||||
|
- RULE-SET,cn_ip,DIRECT,no-resolve
|
||||||
|
- RULE-SET,geolocation-!cn_domain,DEFAULT_PROXY
|
||||||
|
- MATCH,FINAL
|
||||||
|
|
||||||
|
RuleSet_classical_text: &RuleSet_classical_text
|
||||||
|
type: http
|
||||||
|
behavior: classical
|
||||||
|
interval: 43200
|
||||||
|
format: text
|
||||||
|
|
||||||
|
RuleSet_domain_mrs: &RuleSet_domain_mrs
|
||||||
|
type: http
|
||||||
|
behavior: domain
|
||||||
|
interval: 43200
|
||||||
|
format: mrs
|
||||||
|
|
||||||
|
RuleSet_domain_text: &RuleSet_domain_text
|
||||||
|
type: http
|
||||||
|
behavior: domain
|
||||||
|
interval: 43200
|
||||||
|
format: text
|
||||||
|
|
||||||
|
RuleSet_ipcidr_mrs: &RuleSet_ipcidr_mrs
|
||||||
|
type: http
|
||||||
|
behavior: ipcidr
|
||||||
|
interval: 43200
|
||||||
|
format: mrs
|
||||||
|
|
||||||
|
rule-providers:
|
||||||
|
custom_proxy:
|
||||||
|
<<: *RuleSet_classical_text
|
||||||
|
url: "https://raw.githubusercontent.com/lvbibir/clash/refs/heads/master/Ruleset/Proxy.list"
|
||||||
|
path: ./rule-providers/custom_proxy.list
|
||||||
|
custom_direct:
|
||||||
|
<<: *RuleSet_classical_text
|
||||||
|
url: "https://raw.githubusercontent.com/lvbibir/clash/refs/heads/master/Ruleset/Direct.list"
|
||||||
|
path: ./rule-providers/custom_direct.list
|
||||||
|
fakeipfilter:
|
||||||
|
<<: *RuleSet_domain_text
|
||||||
|
url: "https://raw.githubusercontent.com/juewuy/ShellCrash/refs/heads/dev/public/fake_ip_filter.list"
|
||||||
|
path: ./rule-providers/fakeipfilter.list
|
||||||
|
private_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/private.mrs"
|
||||||
|
path: ./rule-providers/private_domain.mrs
|
||||||
|
ai_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/category-ai-!cn.mrs"
|
||||||
|
path: ./rule-providers/ai_domain.mrs
|
||||||
|
x_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/x.mrs"
|
||||||
|
path: ./rule-providers/x_domain.mrs
|
||||||
|
youtube_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/youtube.mrs"
|
||||||
|
path: ./rule-providers/youtube_domain.mrs
|
||||||
|
google_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/google.mrs"
|
||||||
|
path: ./rule-providers/google_domain.mrs
|
||||||
|
github_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/github.mrs"
|
||||||
|
path: ./rule-providers/github_domain.mrs
|
||||||
|
steam_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/steam.mrs"
|
||||||
|
path: ./rule-providers/steam_domain.mrs
|
||||||
|
sony_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/sony.mrs"
|
||||||
|
path: ./rule-providers/sony_domain.mrs
|
||||||
|
telegram_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/telegram.mrs"
|
||||||
|
path: ./rule-providers/telegram_domain.mrs
|
||||||
|
onedrive_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/onedrive.mrs"
|
||||||
|
path: ./rule-providers/onedrive_domain.mrs
|
||||||
|
microsoft_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/microsoft.mrs"
|
||||||
|
path: ./rule-providers/microsoft_domain.mrs
|
||||||
|
apple_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/apple.mrs"
|
||||||
|
path: ./rule-providers/apple_domain.mrs
|
||||||
|
speedtest_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/ookla-speedtest.mrs"
|
||||||
|
path: ./rule-providers/speedtest_domain.mrs
|
||||||
|
geolocation-!cn_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/geolocation-!cn.mrs"
|
||||||
|
path: ./rule-providers/geolocation-!cn_domain.mrs
|
||||||
|
cn_domain:
|
||||||
|
<<: *RuleSet_domain_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/cn.mrs"
|
||||||
|
path: ./rule-providers/cn_domain.mrs
|
||||||
|
private_ip:
|
||||||
|
<<: *RuleSet_ipcidr_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/private.mrs"
|
||||||
|
path: ./rule-providers/private_ip.mrs
|
||||||
|
cn_ip:
|
||||||
|
<<: *RuleSet_ipcidr_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/cn.mrs"
|
||||||
|
path: ./rule-providers/cn_ip.mrs
|
||||||
|
google_ip:
|
||||||
|
<<: *RuleSet_ipcidr_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/google.mrs"
|
||||||
|
path: ./rule-providers/google_ip.mrs
|
||||||
|
apple_ip:
|
||||||
|
<<: *RuleSet_ipcidr_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo-lite/geoip/apple.mrs"
|
||||||
|
path: ./rule-providers/apple_ip.mrs
|
||||||
|
telegram_ip:
|
||||||
|
<<: *RuleSet_ipcidr_mrs
|
||||||
|
url: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo-lite/geoip/telegram.mrs"
|
||||||
|
path: ./rule-providers/telegram_ip.mrs
|
||||||
184
gui.py
184
gui.py
@@ -1,9 +1,11 @@
|
|||||||
|
from html import unescape
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from PySide6.QtCore import QTimer, Qt
|
from PySide6.QtCore import QTimer, Qt
|
||||||
from PySide6.QtGui import QFont
|
from PySide6.QtGui import QFont, QTextDocumentFragment
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
@@ -65,6 +67,55 @@ def _email_text_from_detail(detail):
|
|||||||
return str(detail.get("raw") or "")
|
return str(detail.get("raw") or "")
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_html(value):
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
if "<html" in text.lower() or "<body" in text.lower():
|
||||||
|
return True
|
||||||
|
return bool(re.search(r"<\s*[a-zA-Z][^>]*>", text))
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_email_body(body):
|
||||||
|
if isinstance(body, list):
|
||||||
|
return "\n\n".join(str(item) for item in body if item is not None)
|
||||||
|
if body is None:
|
||||||
|
return ""
|
||||||
|
return str(body)
|
||||||
|
|
||||||
|
|
||||||
|
def _html_to_plain_text(html_text):
|
||||||
|
text = QTextDocumentFragment.fromHtml(html_text).toPlainText().strip()
|
||||||
|
if text:
|
||||||
|
return text
|
||||||
|
return unescape(re.sub(r"<[^>]+>", " ", html_text)).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_verification_code(text):
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
match = re.search(r"(?<!\d)(\d{6})(?!\d)", text)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _highlight_verification_code_html(html_text, code):
|
||||||
|
if not html_text or not code:
|
||||||
|
return html_text
|
||||||
|
pattern = re.compile(rf"(?<!\d){re.escape(code)}(?!\d)")
|
||||||
|
return pattern.sub(
|
||||||
|
(
|
||||||
|
'<span style="background:#fff1bf;color:#8a3b12;'
|
||||||
|
"font-weight:700;padding:2px 6px;border-radius:6px;"
|
||||||
|
'letter-spacing:1px;">'
|
||||||
|
f"{code}</span>"
|
||||||
|
),
|
||||||
|
html_text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _generate_password(length=14):
|
def _generate_password(length=14):
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
@@ -183,10 +234,13 @@ class MainWindow(QMainWindow):
|
|||||||
self.reload_accounts_btn.clicked.connect(self._on_reload_accounts)
|
self.reload_accounts_btn.clicked.connect(self._on_reload_accounts)
|
||||||
self.search_input.textChanged.connect(self._apply_account_filter)
|
self.search_input.textChanged.connect(self._apply_account_filter)
|
||||||
self.show_passwords_check.toggled.connect(self._on_toggle_password_visibility)
|
self.show_passwords_check.toggled.connect(self._on_toggle_password_visibility)
|
||||||
self.backend_type_input.currentTextChanged.connect(self._on_backend_type_changed)
|
self.backend_type_input.currentTextChanged.connect(
|
||||||
|
self._on_backend_type_changed
|
||||||
|
)
|
||||||
self.db_type_input.currentTextChanged.connect(self._on_db_type_changed)
|
self.db_type_input.currentTextChanged.connect(self._on_db_type_changed)
|
||||||
self.test_db_btn.clicked.connect(self._on_test_db_connection)
|
self.test_db_btn.clicked.connect(self._on_test_db_connection)
|
||||||
self.init_db_btn.clicked.connect(self._on_init_db)
|
self.init_db_btn.clicked.connect(self._on_init_db)
|
||||||
|
self.copy_verification_code_btn.clicked.connect(self._copy_verification_code)
|
||||||
|
|
||||||
def _migrate_legacy_config_if_needed(self):
|
def _migrate_legacy_config_if_needed(self):
|
||||||
if CONFIG_PATH.exists():
|
if CONFIG_PATH.exists():
|
||||||
@@ -227,13 +281,13 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
details_group = QGroupBox("账号详情")
|
details_group = QGroupBox("账号详情")
|
||||||
details_layout = QFormLayout(details_group)
|
details_layout = QFormLayout(details_group)
|
||||||
self.detail_email = QLabel("-")
|
self.detail_email = self._create_copyable_label("-")
|
||||||
self.detail_mail_password = QLabel("-")
|
self.detail_mail_password = self._create_copyable_label("-")
|
||||||
self.detail_chatgpt_password = QLabel("-")
|
self.detail_chatgpt_password = self._create_copyable_label("-")
|
||||||
self.detail_name = QLabel("-")
|
self.detail_name = self._create_copyable_label("-")
|
||||||
self.detail_birthdate = QLabel("-")
|
self.detail_birthdate = self._create_copyable_label("-")
|
||||||
self.detail_mail_token = QLabel("-")
|
self.detail_mail_token = self._create_copyable_label("-")
|
||||||
self.detail_source = QLabel("postgres")
|
self.detail_source = self._create_copyable_label("postgres")
|
||||||
details_layout.addRow("邮箱", self.detail_email)
|
details_layout.addRow("邮箱", self.detail_email)
|
||||||
details_layout.addRow("邮箱密码", self.detail_mail_password)
|
details_layout.addRow("邮箱密码", self.detail_mail_password)
|
||||||
details_layout.addRow("Mail Token", self.detail_mail_token)
|
details_layout.addRow("Mail Token", self.detail_mail_token)
|
||||||
@@ -248,6 +302,18 @@ class MainWindow(QMainWindow):
|
|||||||
right_layout.addWidget(QLabel("邮件列表"))
|
right_layout.addWidget(QLabel("邮件列表"))
|
||||||
self.emails_list = QListWidget()
|
self.emails_list = QListWidget()
|
||||||
right_layout.addWidget(self.emails_list)
|
right_layout.addWidget(self.emails_list)
|
||||||
|
code_layout = QHBoxLayout()
|
||||||
|
self.verification_code_label = QLabel("验证码")
|
||||||
|
self.verification_code_value = QLineEdit()
|
||||||
|
self.verification_code_value.setReadOnly(True)
|
||||||
|
self.verification_code_value.setPlaceholderText("自动提取 6 位验证码")
|
||||||
|
self.verification_code_value.setFocusPolicy(Qt.StrongFocus)
|
||||||
|
self.copy_verification_code_btn = QPushButton("复制")
|
||||||
|
self.copy_verification_code_btn.setEnabled(False)
|
||||||
|
code_layout.addWidget(self.verification_code_label)
|
||||||
|
code_layout.addWidget(self.verification_code_value)
|
||||||
|
code_layout.addWidget(self.copy_verification_code_btn)
|
||||||
|
right_layout.addLayout(code_layout)
|
||||||
right_layout.addWidget(QLabel("邮件内容"))
|
right_layout.addWidget(QLabel("邮件内容"))
|
||||||
self.email_content = QTextEdit()
|
self.email_content = QTextEdit()
|
||||||
self.email_content.setReadOnly(True)
|
self.email_content.setReadOnly(True)
|
||||||
@@ -360,11 +426,27 @@ class MainWindow(QMainWindow):
|
|||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
def _create_copyable_label(self, text=""):
|
||||||
|
label = QLabel(text)
|
||||||
|
label.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||||
|
label.setCursor(Qt.IBeamCursor)
|
||||||
|
label.setWordWrap(True)
|
||||||
|
return label
|
||||||
|
|
||||||
def _setup_timer(self):
|
def _setup_timer(self):
|
||||||
self.timer = QTimer(self)
|
self.timer = QTimer(self)
|
||||||
self.timer.setInterval(self.refresh_interval.value() * 1000)
|
self.timer.setInterval(self.refresh_interval.value() * 1000)
|
||||||
self.timer.timeout.connect(self._auto_refresh)
|
self.timer.timeout.connect(self._auto_refresh)
|
||||||
|
|
||||||
|
def _copy_verification_code(self):
|
||||||
|
code = self.verification_code_value.text().strip()
|
||||||
|
if not code:
|
||||||
|
return
|
||||||
|
QApplication.clipboard().setText(code)
|
||||||
|
self.verification_code_value.setFocus()
|
||||||
|
self.verification_code_value.selectAll()
|
||||||
|
self._log(f"验证码已复制: {code}")
|
||||||
|
|
||||||
def _log(self, message):
|
def _log(self, message):
|
||||||
ts = time.strftime("%H:%M:%S")
|
ts = time.strftime("%H:%M:%S")
|
||||||
self.log_box.append(f"[{ts}] {message}")
|
self.log_box.append(f"[{ts}] {message}")
|
||||||
@@ -408,9 +490,14 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _load_config(self):
|
def _load_config(self):
|
||||||
data = load_config(CONFIG_PATH)
|
data = load_config(CONFIG_PATH)
|
||||||
self.api_base_input.setText(data.get("api_base", "https://temp-email-api.example.com"))
|
self.api_base_input.setText(
|
||||||
|
data.get("api_base", "https://temp-email-api.example.com")
|
||||||
|
)
|
||||||
self.domain_input.setText(data.get("domain", "example.com"))
|
self.domain_input.setText(data.get("domain", "example.com"))
|
||||||
backend_type = str(data.get("backend_type", "cloudflare_temp_email")).strip().lower() or "cloudflare_temp_email"
|
backend_type = (
|
||||||
|
str(data.get("backend_type", "cloudflare_temp_email")).strip().lower()
|
||||||
|
or "cloudflare_temp_email"
|
||||||
|
)
|
||||||
backend_index = self.backend_type_input.findText(backend_type)
|
backend_index = self.backend_type_input.findText(backend_type)
|
||||||
if backend_index >= 0:
|
if backend_index >= 0:
|
||||||
self.backend_type_input.setCurrentIndex(backend_index)
|
self.backend_type_input.setCurrentIndex(backend_index)
|
||||||
@@ -418,18 +505,33 @@ class MainWindow(QMainWindow):
|
|||||||
self.proxy_input.setText(data.get("proxy", ""))
|
self.proxy_input.setText(data.get("proxy", ""))
|
||||||
self.show_passwords_check.setChecked(bool(data.get("show_passwords", False)))
|
self.show_passwords_check.setChecked(bool(data.get("show_passwords", False)))
|
||||||
self.auto_refresh_check.setChecked(bool(data.get("auto_refresh", False)))
|
self.auto_refresh_check.setChecked(bool(data.get("auto_refresh", False)))
|
||||||
self.refresh_interval.setValue(max(10, min(300, int(data.get("refresh_interval", 30) or 30))))
|
self.refresh_interval.setValue(
|
||||||
self.pg_enabled_check.setChecked(bool(data.get("db_enabled", data.get("pg_enabled", True))))
|
max(10, min(300, int(data.get("refresh_interval", 30) or 30)))
|
||||||
|
)
|
||||||
|
self.pg_enabled_check.setChecked(
|
||||||
|
bool(data.get("db_enabled", data.get("pg_enabled", True)))
|
||||||
|
)
|
||||||
db_type = str(data.get("db_type", "postgresql")).strip().lower() or "postgresql"
|
db_type = str(data.get("db_type", "postgresql")).strip().lower() or "postgresql"
|
||||||
index = self.db_type_input.findText(db_type)
|
index = self.db_type_input.findText(db_type)
|
||||||
if index >= 0:
|
if index >= 0:
|
||||||
self.db_type_input.setCurrentIndex(index)
|
self.db_type_input.setCurrentIndex(index)
|
||||||
self.pg_host_input.setText(data.get("db_host", data.get("pg_host", "")))
|
self.pg_host_input.setText(data.get("db_host", data.get("pg_host", "")))
|
||||||
self.pg_port_input.setValue(int(data.get("db_port", data.get("pg_port", 5432 if db_type != "mysql" else 3306)) or (3306 if db_type == "mysql" else 5432)))
|
self.pg_port_input.setValue(
|
||||||
self.pg_db_input.setText(data.get("db_name", data.get("pg_db", "mail_accounts_db")))
|
int(
|
||||||
|
data.get(
|
||||||
|
"db_port", data.get("pg_port", 5432 if db_type != "mysql" else 3306)
|
||||||
|
)
|
||||||
|
or (3306 if db_type == "mysql" else 5432)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.pg_db_input.setText(
|
||||||
|
data.get("db_name", data.get("pg_db", "mail_accounts_db"))
|
||||||
|
)
|
||||||
self.db_table_input.setText(data.get("db_table", "registered_accounts"))
|
self.db_table_input.setText(data.get("db_table", "registered_accounts"))
|
||||||
self.pg_user_input.setText(data.get("db_user", data.get("pg_user", "")))
|
self.pg_user_input.setText(data.get("db_user", data.get("pg_user", "")))
|
||||||
self.pg_password_input.setText(data.get("db_password", data.get("pg_password", "")))
|
self.pg_password_input.setText(
|
||||||
|
data.get("db_password", data.get("pg_password", ""))
|
||||||
|
)
|
||||||
self.db_auto_create_check.setChecked(bool(data.get("db_auto_create", False)))
|
self.db_auto_create_check.setChecked(bool(data.get("db_auto_create", False)))
|
||||||
self._on_toggle_password_visibility(self.show_passwords_check.isChecked())
|
self._on_toggle_password_visibility(self.show_passwords_check.isChecked())
|
||||||
|
|
||||||
@@ -522,7 +624,11 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _apply_account_filter(self):
|
def _apply_account_filter(self):
|
||||||
keyword = self.search_input.text().strip().lower()
|
keyword = self.search_input.text().strip().lower()
|
||||||
filtered = [item for item in self.all_accounts if self._account_matches_search(item, keyword)]
|
filtered = [
|
||||||
|
item
|
||||||
|
for item in self.all_accounts
|
||||||
|
if self._account_matches_search(item, keyword)
|
||||||
|
]
|
||||||
self._render_accounts_list(filtered)
|
self._render_accounts_list(filtered)
|
||||||
if not filtered:
|
if not filtered:
|
||||||
self.current_mail_token = ""
|
self.current_mail_token = ""
|
||||||
@@ -538,7 +644,9 @@ class MainWindow(QMainWindow):
|
|||||||
try:
|
try:
|
||||||
self.all_accounts = load_accounts(config)
|
self.all_accounts = load_accounts(config)
|
||||||
self._apply_account_filter()
|
self._apply_account_filter()
|
||||||
self._log(f"已加载 {config.get('db_type', 'postgresql')} 账号: {len(self.all_accounts)} 个")
|
self._log(
|
||||||
|
f"已加载 {config.get('db_type', 'postgresql')} 账号: {len(self.all_accounts)} 个"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.all_accounts = []
|
self.all_accounts = []
|
||||||
self._apply_account_filter()
|
self._apply_account_filter()
|
||||||
@@ -635,9 +743,13 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
token_status = "已存在" if account.get("mail_token", "") else "缺失"
|
token_status = "已存在" if account.get("mail_token", "") else "缺失"
|
||||||
self.detail_email.setText(account.get("email", ""))
|
self.detail_email.setText(account.get("email", ""))
|
||||||
self.detail_mail_password.setText(self._mask_secret(account.get("mail_password", ""), "(管理员模式下可留空)"))
|
self.detail_mail_password.setText(
|
||||||
|
self._mask_secret(account.get("mail_password", ""), "(管理员模式下可留空)")
|
||||||
|
)
|
||||||
self.detail_mail_token.setText(token_status)
|
self.detail_mail_token.setText(token_status)
|
||||||
self.detail_chatgpt_password.setText(self._mask_secret(account.get("chatgpt_password", "")))
|
self.detail_chatgpt_password.setText(
|
||||||
|
self._mask_secret(account.get("chatgpt_password", ""))
|
||||||
|
)
|
||||||
self.detail_name.setText(account.get("name", ""))
|
self.detail_name.setText(account.get("name", ""))
|
||||||
self.detail_birthdate.setText(account.get("birthdate", ""))
|
self.detail_birthdate.setText(account.get("birthdate", ""))
|
||||||
self.detail_source.setText(account.get("source", "postgres"))
|
self.detail_source.setText(account.get("source", "postgres"))
|
||||||
@@ -710,7 +822,9 @@ class MainWindow(QMainWindow):
|
|||||||
if not items:
|
if not items:
|
||||||
return
|
return
|
||||||
email_item = items[0].data(Qt.UserRole)
|
email_item = items[0].data(Qt.UserRole)
|
||||||
msg_id = email_item.get("id") or email_item.get("@id") or email_item.get("messageId")
|
msg_id = (
|
||||||
|
email_item.get("id") or email_item.get("@id") or email_item.get("messageId")
|
||||||
|
)
|
||||||
body = (
|
body = (
|
||||||
email_item.get("text")
|
email_item.get("text")
|
||||||
or email_item.get("message")
|
or email_item.get("message")
|
||||||
@@ -725,7 +839,33 @@ class MainWindow(QMainWindow):
|
|||||||
body = _email_text_from_detail(detail)
|
body = _email_text_from_detail(detail)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
body = f"(获取邮件详情失败: {e})"
|
body = f"(获取邮件详情失败: {e})"
|
||||||
self.email_content.setPlainText(str(body or "(无正文)"))
|
content = _normalize_email_body(body)
|
||||||
|
if not content:
|
||||||
|
self.verification_code_value.clear()
|
||||||
|
self.copy_verification_code_btn.setEnabled(False)
|
||||||
|
self.verification_code_label.setText("验证码")
|
||||||
|
self.email_content.setToolTip("")
|
||||||
|
self.email_content.setPlainText("(无正文)")
|
||||||
|
return
|
||||||
|
plain_text = (
|
||||||
|
_html_to_plain_text(content) if _looks_like_html(content) else content
|
||||||
|
)
|
||||||
|
verification_code = _extract_verification_code(plain_text)
|
||||||
|
self.verification_code_value.setText(verification_code)
|
||||||
|
self.copy_verification_code_btn.setEnabled(bool(verification_code))
|
||||||
|
self.verification_code_label.setText(
|
||||||
|
"验证码 (已提取)" if verification_code else "验证码"
|
||||||
|
)
|
||||||
|
display_text = plain_text
|
||||||
|
if verification_code:
|
||||||
|
display_text = re.sub(
|
||||||
|
rf"(?<!\d)({re.escape(verification_code)})(?!\d)",
|
||||||
|
r"[验证码: \1]",
|
||||||
|
plain_text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
self.email_content.setToolTip("")
|
||||||
|
self.email_content.setPlainText(display_text)
|
||||||
|
|
||||||
def _on_auto_refresh_toggle(self, checked):
|
def _on_auto_refresh_toggle(self, checked):
|
||||||
if checked:
|
if checked:
|
||||||
|
|||||||
Reference in New Issue
Block a user