= 2:
35 | ip = tds[0].text().strip()
36 | port = tds[1].text().strip()
37 | if re.match(ip_regex, ip) is not None and re.match(port_regex, port) is not None:
38 | proxies.append(('http', ip, int(port)))
39 |
40 | return list(set(proxies))
41 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/_4LJ9EKD.js:
--------------------------------------------------------------------------------
1 | import{r as s,D as g,j as r}from"#entry";const p=()=>{const t=s("light"),n=s(!1),d=()=>{if(typeof window<"u"){const e=localStorage.getItem("theme-mode");e&&["light","dark"].includes(e)?t.value=e:t.value="light",l()}},l=()=>{if(typeof window<"u"){const e=t.value==="dark";n.value=e;const a=document.documentElement;e?(a.classList.add("dark"),a.classList.remove("light")):(a.classList.add("light"),a.classList.remove("dark")),u(e)}},u=e=>{if(typeof window<"u"){const a=document.querySelector(".ant-config-provider");a&&a.setAttribute("data-theme",e?"dark":"light");const o=document.documentElement;e?(o.style.setProperty("--ant-layout-bg","#1a1a1a"),o.style.setProperty("--ant-layout-sider-bg","#2d2d2d"),o.style.setProperty("--ant-layout-header-bg","#2d2d2d"),o.style.setProperty("--ant-layout-content-bg","#1a1a1a"),o.style.setProperty("--ant-layout-footer-bg","#2d2d2d")):(o.style.removeProperty("--ant-layout-bg"),o.style.removeProperty("--ant-layout-sider-bg"),o.style.removeProperty("--ant-layout-header-bg"),o.style.removeProperty("--ant-layout-content-bg"),o.style.removeProperty("--ant-layout-footer-bg"))}},i=()=>{t.value=t.value==="light"?"dark":"light"},c=e=>{t.value=e};g(t,e=>{typeof window<"u"&&(localStorage.setItem("theme-mode",e),l())});const m=()=>{},y=r(()=>t.value==="light"?"SunOutlined":"MoonOutlined"),h=r(()=>t.value==="light"?"浅色模式":"深色模式");return{themeMode:t,isDark:n,themeIcon:y,themeLabel:h,loadTheme:d,toggleTheme:i,setTheme:c,updateTheme:l,watchSystemTheme:m}};export{p as u};
2 |
--------------------------------------------------------------------------------
/frontend/src/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Node template
3 | # Logs
4 | /logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Runtime data
11 | pids
12 | *.pid
13 | *.seed
14 | *.pid.lock
15 |
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # nyc test coverage
23 | .nyc_output
24 |
25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
26 | .grunt
27 |
28 | # Bower dependency directory (https://bower.io/)
29 | bower_components
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (https://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directories
38 | node_modules/
39 | jspm_packages/
40 |
41 | # TypeScript v1 declaration files
42 | typings/
43 |
44 | # Optional npm cache directory
45 | .npm
46 |
47 | # Optional eslint cache
48 | .eslintcache
49 |
50 | # Optional REPL history
51 | .node_repl_history
52 |
53 | # Output of 'npm pack'
54 | *.tgz
55 |
56 | # Yarn Integrity file
57 | .yarn-integrity
58 |
59 | # dotenv environment variables file
60 | .env
61 |
62 | # parcel-bundler cache (https://parceljs.org/)
63 | .cache
64 |
65 | # next.js build output
66 | .next
67 |
68 | # nuxt.js build output
69 | .nuxt
70 |
71 | # Nuxt generate
72 | dist
73 |
74 | # vuepress build output
75 | .vuepress/dist
76 |
77 | # Serverless directories
78 | .serverless
79 |
80 | # IDE / Editor
81 | .idea
82 |
83 | # Service worker
84 | sw.*
85 |
86 | # macOS
87 | .DS_Store
88 |
89 | # Vim swap files
90 | *.swp
91 |
--------------------------------------------------------------------------------
/fetchers/GoubanjiaFetcher.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from .BaseFetcher import BaseFetcher
4 | import requests
5 | from pyquery import PyQuery as pq
6 | import re
7 |
8 | class GoubanjiaFetcher(BaseFetcher):
9 | """
10 | http://www.goubanjia.com/
11 | """
12 |
13 | def fetch(self):
14 | """
15 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocal是协议名称,目前主要为http
16 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)]
17 | """
18 |
19 | proxies = []
20 |
21 | headers = {'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'}
22 | html = requests.get('http://www.goubanjia.com/', headers=headers, timeout=10).text
23 | doc = pq(html)
24 | for item in doc('table tbody tr').items():
25 | ipport = item.find('td.ip').html()
26 | # 以下对ipport进行整理
27 | hide_reg = re.compile(r']*style="display:[^<>]*none;"[^<>]*>[^<>]*
')
28 | ipport = re.sub(hide_reg, '', ipport)
29 | tag_reg = re.compile(r'<[^<>]*>')
30 | ipport = re.sub(tag_reg, '', ipport)
31 |
32 | ip = ipport.split(':')[0]
33 | port = self.pde(item.find('td.ip').find('span.port').attr('class').split(' ')[1])
34 | proxies.append(('http', ip, int(port)))
35 |
36 | return list(set(proxies))
37 |
38 | def pde(self, class_key): # 解密函数,端口是加密过的
39 | """
40 | key是class内容
41 | """
42 | class_key = str(class_key)
43 | f = []
44 | for i in range(len(class_key)):
45 | f.append(str('ABCDEFGHIZ'.index(class_key[i])))
46 | return str(int(''.join(f)) >> 0x3)
47 |
--------------------------------------------------------------------------------
/db/Fetcher.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import datetime
4 |
5 | class Fetcher(object):
6 | """
7 | 爬取器的状态储存在数据库中,包括是否启用爬取器,爬取到的代理数量等
8 | """
9 |
10 | ddls = ["""
11 | CREATE TABLE IF NOT EXISTS fetchers
12 | (
13 | name VARCHAR(255) NOT NULL,
14 | enable BOOLEAN NOT NULL,
15 | sum_proxies_cnt INTEGER NOT NULL,
16 | last_proxies_cnt INTEGER NOT NULL,
17 | last_fetch_date TIMESTAMP,
18 | PRIMARY KEY (name)
19 | )
20 | """]
21 |
22 | def __init__(self):
23 | self.name = None
24 | self.enable = True
25 | self.sum_proxies_cnt = 0
26 | self.last_proxies_cnt = 0
27 | self.last_fetch_date = None
28 |
29 | def params(self):
30 | """
31 | 返回一个元组,包含自身的全部属性
32 | """
33 | return (
34 | self.name, self.enable,
35 | self.sum_proxies_cnt, self.last_proxies_cnt, self.last_fetch_date
36 | )
37 |
38 | def to_dict(self):
39 | """
40 | 返回一个dict,包含自身的全部属性
41 | """
42 | return {
43 | 'name': self.name,
44 | 'enable': self.enable,
45 | 'sum_proxies_cnt': self.sum_proxies_cnt,
46 | 'last_proxies_cnt': self.last_proxies_cnt,
47 | 'last_fetch_date': str(self.last_fetch_date) if self.last_fetch_date is not None else None
48 | }
49 |
50 | @staticmethod
51 | def decode(row):
52 | """
53 | 将sqlite返回的一行解析为Fetcher
54 | row : sqlite返回的一行
55 | """
56 | assert len(row) == 5
57 | f = Fetcher()
58 | f.name = row[0]
59 | f.enable = bool(row[1])
60 | f.sum_proxies_cnt = row[2]
61 | f.last_proxies_cnt = row[3]
62 | f.last_fetch_date = row[4]
63 | return f
64 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/error-500.DOWD7OuR.css:
--------------------------------------------------------------------------------
1 | .spotlight[data-v-4b6f0a29]{background:linear-gradient(45deg,#00dc82,#36e4da 50%,#0047e1);filter:blur(20vh)}.fixed[data-v-4b6f0a29]{position:fixed}.-bottom-1\/2[data-v-4b6f0a29]{bottom:-50%}.left-0[data-v-4b6f0a29]{left:0}.right-0[data-v-4b6f0a29]{right:0}.grid[data-v-4b6f0a29]{display:grid}.mb-16[data-v-4b6f0a29]{margin-bottom:4rem}.mb-8[data-v-4b6f0a29]{margin-bottom:2rem}.h-1\/2[data-v-4b6f0a29]{height:50%}.max-w-520px[data-v-4b6f0a29]{max-width:520px}.min-h-screen[data-v-4b6f0a29]{min-height:100vh}.place-content-center[data-v-4b6f0a29]{place-content:center}.overflow-hidden[data-v-4b6f0a29]{overflow:hidden}.bg-white[data-v-4b6f0a29]{--un-bg-opacity:1;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-8[data-v-4b6f0a29]{padding-left:2rem;padding-right:2rem}.text-center[data-v-4b6f0a29]{text-align:center}.text-8xl[data-v-4b6f0a29]{font-size:6rem;line-height:1}.text-xl[data-v-4b6f0a29]{font-size:1.25rem;line-height:1.75rem}.text-black[data-v-4b6f0a29]{--un-text-opacity:1;color:rgb(0 0 0/var(--un-text-opacity))}.font-light[data-v-4b6f0a29]{font-weight:300}.font-medium[data-v-4b6f0a29]{font-weight:500}.leading-tight[data-v-4b6f0a29]{line-height:1.25}.font-sans[data-v-4b6f0a29]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.antialiased[data-v-4b6f0a29]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media(prefers-color-scheme:dark){.dark\:bg-black[data-v-4b6f0a29]{--un-bg-opacity:1;background-color:rgb(0 0 0/var(--un-bg-opacity))}.dark\:text-white[data-v-4b6f0a29]{--un-text-opacity:1;color:rgb(255 255 255/var(--un-text-opacity))}}@media(min-width:640px){.sm\:px-0[data-v-4b6f0a29]{padding-left:0;padding-right:0}.sm\:text-4xl[data-v-4b6f0a29]{font-size:2.25rem;line-height:2.5rem}}
2 |
--------------------------------------------------------------------------------
/fetchers/XiLaFetcher.py:
--------------------------------------------------------------------------------
1 | import re
2 | import time
3 | import random
4 |
5 | import requests
6 | from pyquery import PyQuery as pq
7 |
8 | from .BaseFetcher import BaseFetcher
9 |
10 | class XiLaFetcher(BaseFetcher):
11 | """
12 | http://www.xiladaili.com/gaoni/
13 | 代码由 [Zealot666](https://github.com/Zealot666) 提供
14 | """
15 | def __init__(self):
16 | super().__init__()
17 | self.index = 0
18 |
19 | def fetch(self):
20 | """
21 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocol是协议名称,目前主要为http
22 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)]
23 | """
24 | self.index += 1
25 | new_index = self.index % 30
26 |
27 | urls = []
28 | urls = urls + [f'http://www.xiladaili.com/gaoni/{page}/' for page in range(new_index, new_index + 11)]
29 | urls = urls + [f'http://www.xiladaili.com/http/{page}/' for page in range(new_index, new_index + 11)]
30 |
31 | proxies = []
32 | ip_regex = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
33 | port_regex = re.compile(r'^\d+$')
34 |
35 | for url in urls:
36 | time.sleep(1)
37 | html = requests.get(url, timeout=10).text
38 | doc = pq(html)
39 | for line in doc('tr').items():
40 | tds = list(line('td').items())
41 | if len(tds) >= 2:
42 | ip = tds[0].text().strip().split(":")[0]
43 | port = tds[0].text().strip().split(":")[1]
44 | if re.match(ip_regex, ip) is not None and re.match(port_regex, port) is not None:
45 | proxies.append(('http', ip, int(port)))
46 |
47 | proxies = list(set(proxies))
48 |
49 | # 这个代理源数据太多了,验证器跑不过来
50 | # 所以只取一部分,一般来说也够用了
51 | if len(proxies) > 200:
52 | proxies = random.sample(proxies, 200)
53 |
54 | return proxies
55 |
--------------------------------------------------------------------------------
/fetchers/KuaidailiFetcher.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from .BaseFetcher import BaseFetcher
4 | from .FetcherUtils import safe_get
5 | from pyquery import PyQuery as pq
6 |
7 | class KuaidailiFetcher(BaseFetcher):
8 | """
9 | https://www.kuaidaili.com/free
10 | """
11 |
12 | def fetch(self):
13 | """
14 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocal是协议名称,目前主要为http
15 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)]
16 | """
17 |
18 | # 减少页面数量,避免触发反爬虫
19 | urls = []
20 | urls = urls + [f'https://www.kuaidaili.com/free/inha/{page}/' for page in range(1, 4)] # 只爬前3页
21 | urls = urls + [f'https://www.kuaidaili.com/free/intr/{page}/' for page in range(1, 4)] # 只爬前3页
22 |
23 | proxies = []
24 |
25 | # 自定义请求头
26 | headers = {
27 | 'Referer': 'https://www.kuaidaili.com/'
28 | }
29 |
30 | for url in urls:
31 | try:
32 | # 使用 safe_get 自动处理反爬虫
33 | response = safe_get(url, headers=headers, delay_range=(1, 3))
34 | html = response.text
35 |
36 | doc = pq(html)
37 | for item in doc('table tbody tr').items():
38 | try:
39 | ip = item.find('td[data-title="IP"]').text()
40 | port_text = item.find('td[data-title="PORT"]').text()
41 |
42 | if ip and port_text:
43 | port = int(port_text)
44 | proxies.append(('http', ip, port))
45 | except (ValueError, AttributeError):
46 | continue
47 |
48 | except Exception as e:
49 | # 单个页面失败不影响其他页面
50 | print(f"爬取 {url} 失败: {e}")
51 | continue
52 |
53 | return list(set(proxies))
54 |
--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------
1 | name: Build & Publish Docker image to GHCR
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | tags: [ '*' ]
7 | pull_request:
8 | branches: [ main ]
9 | workflow_dispatch:
10 |
11 | permissions:
12 | contents: read
13 | packages: write
14 |
15 | jobs:
16 | docker:
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 |
23 | - name: Set lowercase owner/repo
24 | run: |
25 | echo "OWNER_LC=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV
26 | echo "REPO_NAME=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV
27 |
28 | - name: Set up QEMU
29 | uses: docker/setup-qemu-action@v3
30 |
31 | - name: Set up Docker Buildx
32 | uses: docker/setup-buildx-action@v3
33 |
34 | - name: Log in to GHCR
35 | uses: docker/login-action@v3
36 | with:
37 | registry: ghcr.io
38 | username: ${{ github.actor }}
39 | password: ${{ secrets.GITHUB_TOKEN }}
40 |
41 | - name: Extract Docker metadata (tags, labels)
42 | id: meta
43 | uses: docker/metadata-action@v5
44 | with:
45 | images: |
46 | ghcr.io/${{ env.OWNER_LC }}/${{ env.REPO_NAME }}
47 | tags: |
48 | type=raw,value=latest,enable={{is_default_branch}}
49 | type=ref,event=branch
50 | type=raw,value={{date 'YYYYMMDD'}},enable={{is_default_branch}}
51 | type=sha,prefix=sha-
52 | type=ref,event=tag
53 |
54 | - name: Build and push
55 | uses: docker/build-push-action@v6
56 | with:
57 | context: .
58 | file: ./Dockerfile
59 | platforms: linux/amd64,linux/arm64
60 | push: ${{ github.event_name != 'pull_request' }}
61 | tags: ${{ steps.meta.outputs.tags }}
62 | labels: ${{ steps.meta.outputs.labels }}
63 | cache-from: type=gha
64 | cache-to: type=gha,mode=max
65 |
--------------------------------------------------------------------------------
/fetchers/IP89Fetcher.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from .BaseFetcher import BaseFetcher
4 | import requests
5 | from pyquery import PyQuery as pq
6 | import re
7 |
8 | class IP89Fetcher(BaseFetcher):
9 | """
10 | https://www.89ip.cn/
11 | """
12 |
13 | def fetch(self):
14 | """
15 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocal是协议名称,目前主要为http
16 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)]
17 | """
18 |
19 | urls = []
20 | for page in range(1, 6):
21 | url = f'https://www.89ip.cn/index_{page}.html'
22 | urls.append(url)
23 |
24 | proxies = []
25 | ip_regex = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
26 | port_regex = re.compile(r'^\d+$')
27 |
28 | for url in urls:
29 | headers = {
30 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
31 | 'Accept-Encoding': 'gzip, deflate',
32 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
33 | 'Cache-Control': 'no-cache',
34 | 'Connection': 'keep-alive',
35 | 'Pragma': 'no-cache',
36 | 'Upgrade-Insecure-Requests': '1',
37 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/79.0.3945.130 Chrome/79.0.3945.130 Safari/537.36'
38 | }
39 | html = requests.get(url, headers=headers, timeout=10).text
40 | doc = pq(html)
41 | for line in doc('tr').items():
42 | tds = list(line('td').items())
43 | if len(tds) == 5:
44 | ip = tds[0].text().strip()
45 | port = tds[1].text().strip()
46 | if re.match(ip_regex, ip) is not None and re.match(port_regex, port) is not None:
47 | proxies.append(('http', ip, int(port)))
48 |
49 | return list(set(proxies))
50 |
--------------------------------------------------------------------------------
/fetchers/JiangxianliFetcher.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from .BaseFetcher import BaseFetcher
4 | import requests
5 | from pyquery import PyQuery as pq
6 | import re
7 |
8 | class JiangxianliFetcher(BaseFetcher):
9 | """
10 | https://ip.jiangxianli.com/?page=1
11 | """
12 |
13 | def fetch(self):
14 | """
15 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocal是协议名称,目前主要为http
16 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)]
17 | """
18 |
19 | urls = []
20 | for page in range(1, 5):
21 | url = f'https://ip.jiangxianli.com/?page={page}'
22 | urls.append(url)
23 |
24 | proxies = []
25 | ip_regex = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
26 | port_regex = re.compile(r'^\d+$')
27 |
28 | for url in urls:
29 | headers = {
30 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
31 | 'Accept-Encoding': 'gzip, deflate',
32 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
33 | 'Cache-Control': 'no-cache',
34 | 'Connection': 'keep-alive',
35 | 'Pragma': 'no-cache',
36 | 'Upgrade-Insecure-Requests': '1',
37 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/79.0.3945.130 Chrome/79.0.3945.130 Safari/537.36'
38 | }
39 | html = requests.get(url, headers=headers, timeout=10).text
40 | doc = pq(html)
41 | for line in doc('tr').items():
42 | tds = list(line('td').items())
43 | if len(tds) >= 2:
44 | ip = tds[0].text().strip()
45 | port = tds[1].text().strip()
46 | if re.match(ip_regex, ip) is not None and re.match(port_regex, port) is not None:
47 | proxies.append(('http', ip, int(port)))
48 |
49 | return list(set(proxies))
50 |
--------------------------------------------------------------------------------
/fetchers/XiaoShuFetcher.py:
--------------------------------------------------------------------------------
1 | import re
2 | import time
3 | import random
4 |
5 | import requests
6 | from pyquery import PyQuery as pq
7 |
8 | from .BaseFetcher import BaseFetcher
9 |
10 | class XiaoShuFetcher(BaseFetcher):
11 | """
12 | http://www.xsdaili.cn/
13 | 代码由 [Zealot666](https://github.com/Zealot666) 提供
14 | """
15 | def __init__(self):
16 | super().__init__()
17 | self.index = 0
18 |
19 | def fetch(self):
20 | """
21 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocol是协议名称,目前主要为http
22 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)]
23 | """
24 | self.index += 1
25 | new_index = self.index % 10
26 |
27 | urls = set()
28 | proxies = []
29 | headers = {
30 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
31 | }
32 | for page in range(new_index, new_index + 1):
33 | response = requests.get("http://www.xsdaili.cn/dayProxy/" + str(page) + ".html", headers=headers, timeout=10)
34 | for item in pq(response.text)('a').items():
35 | try:
36 | if "/dayProxy/ip" in item.attr("href"):
37 | urls.add("http://www.xsdaili.cn" + item.attr("href"))
38 | except Exception:
39 | continue
40 | for url in urls:
41 | response = requests.get(url, headers=headers, timeout=8)
42 | doc = pq(response.text)
43 | for item in doc(".cont").items():
44 | for line in item.text().split("\n"):
45 | ip = line.split('@')[0].split(':')[0]
46 | port = line.split('@')[0].split(':')[1]
47 | proxies.append(("http", ip, port))
48 |
49 | proxies = list(set(proxies))
50 |
51 | # 这个代理源数据太多了,验证器跑不过来
52 | # 所以只取一部分,一般来说也够用了
53 | if len(proxies) > 200:
54 | proxies = random.sample(proxies, 200)
55 |
56 | return proxies
57 |
--------------------------------------------------------------------------------
/fetchers/IP66Fetcher.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from .BaseFetcher import BaseFetcher
4 | import requests
5 | from pyquery import PyQuery as pq
6 | import re
7 |
8 | class IP66Fetcher(BaseFetcher):
9 | """
10 | http://www.66ip.cn/
11 | """
12 |
13 | def fetch(self):
14 | """
15 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocal是协议名称,目前主要为http
16 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)]
17 | """
18 |
19 | urls = []
20 | for areaindex in range(10):
21 | for page in range(1, 6):
22 | if areaindex == 0:
23 | url = f'http://www.66ip.cn/{page}.html'
24 | else:
25 | url = f'http://www.66ip.cn/areaindex_{areaindex}/{page}.html'
26 | urls.append(url)
27 |
28 | proxies = []
29 | ip_regex = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
30 | port_regex = re.compile(r'^\d+$')
31 |
32 | for url in urls:
33 | headers = {
34 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
35 | 'Accept-Encoding': 'gzip, deflate',
36 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
37 | 'Cache-Control': 'no-cache',
38 | 'Connection': 'keep-alive',
39 | 'Pragma': 'no-cache',
40 | 'Upgrade-Insecure-Requests': '1',
41 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/79.0.3945.130 Chrome/79.0.3945.130 Safari/537.36'
42 | }
43 | html = requests.get(url, headers=headers, timeout=10).text
44 | doc = pq(html)
45 | for line in doc('table tr').items():
46 | tds = list(line('td').items())
47 | if len(tds) == 5:
48 | ip = tds[0].text().strip()
49 | port = tds[1].text().strip()
50 | if re.match(ip_regex, ip) is not None and re.match(port_regex, port) is not None:
51 | proxies.append(('http', ip, int(port)))
52 |
53 | return list(set(proxies))
54 |
--------------------------------------------------------------------------------
/fetchers/ProxyListFetcher.py:
--------------------------------------------------------------------------------
1 | import time
2 | import warnings
3 |
4 | import requests
5 | from requests.adapters import HTTPAdapter
6 | from urllib3.util.retry import Retry
7 | import urllib3
8 |
9 | from .BaseFetcher import BaseFetcher
10 |
11 | # 禁用 SSL 警告
12 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
13 |
14 |
15 | class ProxyListFetcher(BaseFetcher):
16 | """
17 | https://www.proxy-list.download/api/v1/get?type={{ protocol }}&_t={{ timestamp }}
18 | """
19 |
20 | def fetch(self):
21 | proxies = []
22 | type_list = ['socks4', 'socks5', 'http', 'https']
23 |
24 | # 配置重试策略
25 | session = requests.Session()
26 | retry = Retry(
27 | total=3,
28 | backoff_factor=1,
29 | status_forcelist=[500, 502, 503, 504]
30 | )
31 | adapter = HTTPAdapter(max_retries=retry)
32 | session.mount('http://', adapter)
33 | session.mount('https://', adapter)
34 |
35 | for protocol in type_list:
36 | try:
37 | url = "https://www.proxy-list.download/api/v1/get?type=" + protocol + "&_t=" + str(time.time())
38 | # 禁用SSL验证,增加超时
39 | response = session.get(url, verify=False, timeout=15)
40 | proxies_list = response.text.split("\n")
41 |
42 | for data in proxies_list:
43 | if not data or ':' not in data:
44 | continue
45 | flag_idx = data.find(":")
46 | ip = data[:flag_idx].strip()
47 | port_str = data[flag_idx + 1:].strip()
48 |
49 | # 验证IP和端口格式
50 | if ip and port_str:
51 | try:
52 | port = int(port_str)
53 | if 1 <= port <= 65535:
54 | proxies.append((protocol, ip, port))
55 | except ValueError:
56 | continue
57 | except Exception as e:
58 | # 单个协议失败不影响其他协议
59 | print(f"获取 {protocol} 代理失败: {e}")
60 | continue
61 |
62 | return list(set(proxies))
63 |
--------------------------------------------------------------------------------
/frontend/deployment/public/200.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 私人IP池管理界面
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/deployment/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 私人IP池管理界面
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/deployment/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 私人IP池管理界面
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/deployment/public/api/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 私人IP池管理界面
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/deployment/public/login/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 私人IP池管理界面
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/deployment/public/fetchers/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 私人IP池管理界面
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/deployment/public/theme-test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 私人IP池管理界面
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/deployment/public/subscriptions/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 私人IP池管理界面
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/db/README.md:
--------------------------------------------------------------------------------
1 | # 数据库封装
2 |
3 | 这个目录下封装了操作数据库的一些接口。
4 | 为了通用性,本项目使用SQLite作为底层的数据库,使用`sqlite3`提供的接口对数据库进行操作。
5 |
6 | ## 数据表
7 |
8 | 主要包含两个表,分别用于储存代理和爬取器:
9 |
10 | 1. 代理
11 |
12 | | 字段名称 | 数据类型 | 说明 |
13 | |---------------------|----------|--------------------------------------------------------------------------|
14 | | fetcher_name | 字符串 | 这个代理来自哪个爬取器 |
15 | | protocol | 字符串 | 代理协议名称,一般为HTTP |
16 | | ip | 字符串 | 代理的IP地址 |
17 | | port | 整数 | 代理的端口号 |
18 | | validated | 布尔值 | 这个代理是否通过了验证,通过了验证表示当前代理可用 |
19 | | latency | 整数 | 延迟(单位毫秒),表示上次验证所用的时间,越小则代理质量越好 |
20 | | validate_date | 时间戳 | 上一次进行验证的时间 |
21 | | to_validate_date | 时间戳 | 下一次进行验证的时间,如何调整下一次验证的时间可见后文或者代码`Proxy.py` |
22 | | validate_failed_cnt | 整数 | 已经连续验证失败了多少次,会影响下一次验证的时间 |
23 |
24 | 2. 爬取器
25 |
26 | | 字段名称 | 数据类型 | 说明 |
27 | |------------------|----------|----------------------------------------------------------------------------------|
28 | | name | 字符串 | 爬取器的名称 |
29 | | enable | 布尔值 | 是否启用这个爬取器,被禁用的爬取器不会在之后被运行,但是其之前爬取的代理依然存在 |
30 | | sum_proxies_cnt | 整数 | 至今为止总共爬取到了多少个代理 |
31 | | last_proxies_cnt | 整数 | 上次爬取到了多少个代理 |
32 | | last_fetch_date | 时间戳 | 上次爬取的时间 |
33 |
34 | ## 下次验证时间调整算法
35 |
36 | 由于不同代理网站公开的免费代理质量差距较大,因此对于多次验证都失败的代理,我们需要降低对他们进行验证的频率,甚至将他们从数据库中删除。
37 | 而对于现在可用的代理,则需要频繁对其进行验证,以保证其可用性。
38 |
39 | 目前的算法较为简单,可见`Proxy.py`文件中的`validate`函数,核心思想如下:
40 |
41 | 1. 优先验证之前验证通过并且到了验证时间的代理(`conn.py`中的`getToValidate`函数)
42 | 2. 对于爬取器新爬取到的代理,我们需要尽快对其进行验证(设置`to_validate_date`为当前时间)
43 | 3. 如果某个代理验证成功,那么设置它下一次进行验证的时间为5分钟之后
44 | 4. 如果某个代理验证失败,那么设置它下一次进行验证的时间为 5 * 连续失败次数 分钟之后,如果连续3次失败,那么将其从数据库中删除
45 |
46 | 你可以修改为自己的算法,主要代码涉及`Proxy.py`文件以及`conn.py`文件的`pushNewFetch`和`getToValidate`函数。
47 |
--------------------------------------------------------------------------------
/fetchers/IHuanFetcher.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from .BaseFetcher import BaseFetcher
4 | import requests
5 | from pyquery import PyQuery as pq
6 | import re
7 |
8 | class IHuanFetcher(BaseFetcher):
9 | """
10 | https://ip.ihuan.me/
11 | 爬这个网站要温柔点,站长表示可能会永久关站
12 | """
13 |
14 | def fetch(self):
15 | """
16 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocal是协议名称,目前主要为http
17 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)]
18 | """
19 |
20 | proxies = []
21 | ip_regex = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
22 | port_regex = re.compile(r'^\d+$')
23 |
24 | pending_urls = ['https://ip.ihuan.me/']
25 | while len(pending_urls) > 0:
26 | url = pending_urls[0]
27 | pending_urls = pending_urls[1:]
28 |
29 | headers = {
30 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
31 | 'Accept-Encoding': 'gzip, deflate',
32 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
33 | 'Cache-Control': 'no-cache',
34 | 'Connection': 'keep-alive',
35 | 'Pragma': 'no-cache',
36 | 'Upgrade-Insecure-Requests': '1',
37 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/79.0.3945.130 Chrome/79.0.3945.130 Safari/537.36'
38 | }
39 | try:
40 | html = requests.get(url, headers=headers, timeout=10).text
41 | except Exception as e:
42 | print('ERROR in ip.ihuan.me:' + str(e))
43 | continue
44 | doc = pq(html)
45 | for line in doc('tbody tr').items():
46 | tds = list(line('td').items())
47 | if len(tds) == 10:
48 | ip = tds[0].text().strip()
49 | port = tds[1].text().strip()
50 | if re.match(ip_regex, ip) is not None and re.match(port_regex, port) is not None:
51 | proxies.append(('http', ip, int(port)))
52 |
53 | if url.endswith('/'): # 当前是第一页,解析后面几页的链接
54 | for item in list(doc('.pagination a').items())[1:-1]:
55 | href = item.attr('href')
56 | if href is not None and href.startswith('?page='):
57 | pending_urls.append('https://ip.ihuan.me/' + href)
58 |
59 | return list(set(proxies))
60 |
--------------------------------------------------------------------------------
/fetchers/IP3366Fetcher.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from .BaseFetcher import BaseFetcher
4 | import requests
5 | from pyquery import PyQuery as pq
6 | import re
7 |
8 | class IP3366Fetcher(BaseFetcher):
9 | """
10 | http://www.ip3366.net/free/?stype=1
11 | """
12 |
13 | def fetch(self):
14 | """
15 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocal是协议名称,目前主要为http
16 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)]
17 | """
18 |
19 | urls = []
20 | for stype in ['1', '2']:
21 | for page in range(1, 6):
22 | url = f'http://www.ip3366.net/free/?stype={stype}&page={page}'
23 | urls.append(url)
24 |
25 | proxies = []
26 | ip_regex = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
27 | port_regex = re.compile(r'^\d+$')
28 |
29 | for url in urls:
30 | try:
31 | headers = {
32 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
33 | 'Accept-Encoding': 'gzip, deflate',
34 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
35 | 'Cache-Control': 'no-cache',
36 | 'Connection': 'keep-alive',
37 | 'Pragma': 'no-cache',
38 | 'Upgrade-Insecure-Requests': '1',
39 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/79.0.3945.130 Chrome/79.0.3945.130 Safari/537.36'
40 | }
41 | response = requests.get(url, headers=headers, timeout=10)
42 | html = response.text
43 |
44 | # Check if response is empty or invalid
45 | if not html or len(html.strip()) == 0:
46 | continue
47 |
48 | doc = pq(html)
49 | for line in doc('tr').items():
50 | tds = list(line('td').items())
51 | if len(tds) == 7:
52 | ip = tds[0].text().strip()
53 | port = tds[1].text().strip()
54 | if re.match(ip_regex, ip) is not None and re.match(port_regex, port) is not None:
55 | proxies.append(('http', ip, int(port)))
56 | except Exception as e:
57 | # Skip this URL if there's an error and continue with others
58 | continue
59 |
60 | return list(set(proxies))
61 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/KxedcqoH.js:
--------------------------------------------------------------------------------
1 | import{b as p,I as d,r as o}from"#entry";import{s as v}from"./BzLCLO6P.js";var h={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M917.7 148.8l-42.4-42.4c-1.6-1.6-3.6-2.3-5.7-2.3s-4.1.8-5.7 2.3l-76.1 76.1a199.27 199.27 0 00-112.1-34.3c-51.2 0-102.4 19.5-141.5 58.6L432.3 308.7a8.03 8.03 0 000 11.3L704 591.7c1.6 1.6 3.6 2.3 5.7 2.3 2 0 4.1-.8 5.7-2.3l101.9-101.9c68.9-69 77-175.7 24.3-253.5l76.1-76.1c3.1-3.2 3.1-8.3 0-11.4zM769.1 441.7l-59.4 59.4-186.8-186.8 59.4-59.4c24.9-24.9 58.1-38.7 93.4-38.7 35.3 0 68.4 13.7 93.4 38.7 24.9 24.9 38.7 58.1 38.7 93.4 0 35.3-13.8 68.4-38.7 93.4zm-190.2 105a8.03 8.03 0 00-11.3 0L501 613.3 410.7 523l66.7-66.7c3.1-3.1 3.1-8.2 0-11.3L441 408.6a8.03 8.03 0 00-11.3 0L363 475.3l-43-43a7.85 7.85 0 00-5.7-2.3c-2 0-4.1.8-5.7 2.3L206.8 534.2c-68.9 69-77 175.7-24.3 253.5l-76.1 76.1a8.03 8.03 0 000 11.3l42.4 42.4c1.6 1.6 3.6 2.3 5.7 2.3s4.1-.8 5.7-2.3l76.1-76.1c33.7 22.9 72.9 34.3 112.1 34.3 51.2 0 102.4-19.5 141.5-58.6l101.9-101.9c3.1-3.1 3.1-8.2 0-11.3l-43-43 66.7-66.7c3.1-3.1 3.1-8.2 0-11.3l-36.6-36.2zM441.7 769.1a131.32 131.32 0 01-93.4 38.7c-35.3 0-68.4-13.7-93.4-38.7a131.32 131.32 0 01-38.7-93.4c0-35.3 13.7-68.4 38.7-93.4l59.4-59.4 186.8 186.8-59.4 59.4z"}}]},name:"api",theme:"outlined"};function s(r){for(var e=1;e{const r=o("checking"),e=o("");let t=null;const n=async c=>{try{r.value="checking",e.value="";let a=c;a?a==="/"&&(a=""):typeof window<"u"?a="":a="http://localhost:5000";const f=a?`${a}/ping`:"/ping",i=await fetch(f,{method:"GET"});i.ok?(await i.text()).trim()==="API OK"?(r.value="online",e.value=""):(r.value="offline",e.value="后端服务响应异常"):(r.value="offline",e.value=`后端服务响应异常 (${i.status})`)}catch(a){r.value="offline",a.name==="AbortError"?e.value="连接超时,后端服务可能响应缓慢":a.code==="ERR_NETWORK"?e.value="无法连接到后端服务,请检查服务是否启动":a.code==="ECONNABORTED"?e.value="连接超时,后端服务可能响应缓慢":e.value=a.message||"连接后端服务失败",console.error("后端状态检测失败:",a)}};return{backendStatus:r,backendError:e,checkBackendStatus:n,startPeriodicCheck:(c=30,a)=>{t&&clearInterval(t),n(a),t=v(()=>{n(a)},c*1e3)},stopPeriodicCheck:()=>{t&&(clearInterval(t),t=null)}}};export{u as A,P as u};
2 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/C9tsJkms.js:
--------------------------------------------------------------------------------
1 | import{u as v}from"./_4LJ9EKD.js";import{g as f,c as h,b as e,w as l,m as o,o as b,a as s,t as p,n as c,q as x,d as n,_ as w}from"#entry";const y={class:"theme-test-page"},g={class:"test-content"},C={class:"theme-header"},k={class:"theme-indicator"},B={class:"test-sections"},N={class:"component-test"},T={class:"form-test"},V=f({__name:"theme-test",setup(q){const{themeMode:i,themeLabel:u}=v();return(z,t)=>{const a=o("a-card"),d=o("a-button"),_=o("a-input"),r=o("a-select-option"),m=o("a-select");return b(),h("div",y,[e(a,{title:"主题切换测试页面",class:"test-card enhanced-card"},{default:l(()=>[s("div",g,[s("div",C,[s("h2",null,"当前主题模式:"+p(c(i)),1),s("div",k,[s("div",{class:x(["indicator-dot",c(i)])},null,2),s("span",null,p(c(u)),1)])]),t[9]||(t[9]=s("p",null,"这是一个用于测试主题切换效果的页面,展示了不同主题下的视觉美化效果。",-1)),s("div",B,[e(a,{title:"颜色测试",class:"section-card enhanced-card"},{default:l(()=>[...t[0]||(t[0]=[s("div",{class:"color-grid"},[s("div",{class:"color-item primary"},[s("div",{class:"color-preview"}),s("span",null,"主色调")]),s("div",{class:"color-item success"},[s("div",{class:"color-preview"}),s("span",null,"成功色")]),s("div",{class:"color-item warning"},[s("div",{class:"color-preview"}),s("span",null,"警告色")]),s("div",{class:"color-item error"},[s("div",{class:"color-preview"}),s("span",null,"错误色")])],-1)])]),_:1}),e(a,{title:"组件测试",class:"section-card enhanced-card"},{default:l(()=>[s("div",N,[e(d,{type:"primary",class:"enhanced-button"},{default:l(()=>[...t[1]||(t[1]=[n("主要按钮",-1)])]),_:1}),e(d,{class:"enhanced-button"},{default:l(()=>[...t[2]||(t[2]=[n("默认按钮",-1)])]),_:1}),e(d,{type:"dashed",class:"enhanced-button"},{default:l(()=>[...t[3]||(t[3]=[n("虚线按钮",-1)])]),_:1}),e(d,{type:"link",class:"enhanced-button"},{default:l(()=>[...t[4]||(t[4]=[n("链接按钮",-1)])]),_:1})]),s("div",T,[e(_,{placeholder:"输入框测试",class:"enhanced-input"}),e(m,{placeholder:"选择器测试",class:"enhanced-select",style:{width:"200px"}},{default:l(()=>[e(r,{value:"option1"},{default:l(()=>[...t[5]||(t[5]=[n("选项1",-1)])]),_:1}),e(r,{value:"option2"},{default:l(()=>[...t[6]||(t[6]=[n("选项2",-1)])]),_:1})]),_:1})])]),_:1}),e(a,{title:"文本测试",class:"section-card enhanced-card"},{default:l(()=>[...t[7]||(t[7]=[s("div",{class:"text-test"},[s("h1",null,"标题1"),s("h2",null,"标题2"),s("h3",null,"标题3"),s("p",null,"这是一段普通文本,用于测试在不同主题下的显示效果。"),s("p",{class:"text-secondary"},"这是次要文本。"),s("p",{class:"text-disabled"},"这是禁用文本。")],-1)])]),_:1}),e(a,{title:"特殊效果测试",class:"section-card enhanced-card"},{default:l(()=>[...t[8]||(t[8]=[s("div",{class:"effects-test"},[s("div",{class:"gradient-box"},[s("h4",null,"渐变背景"),s("p",null,"展示主题特定的渐变效果")]),s("div",{class:"glass-effect"},[s("h4",null,"玻璃态效果"),s("p",null,"毛玻璃背景效果")]),s("div",{class:"neon-effect"},[s("h4",null,"霓虹效果"),s("p",null,"发光边框效果")])],-1)])]),_:1})])])]),_:1})])}}}),I=w(V,[["__scopeId","data-v-cd44458d"]]);export{I as default};
2 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/DvlYdpVS.js:
--------------------------------------------------------------------------------
1 | import{b as l,I as i}from"#entry";var f={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M704 446H320c-4.4 0-8 3.6-8 8v402c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8V454c0-4.4-3.6-8-8-8zm-328 64h272v117H376V510zm272 290H376V683h272v117z"}},{tag:"path",attrs:{d:"M424 748a32 32 0 1064 0 32 32 0 10-64 0zm0-178a32 32 0 1064 0 32 32 0 10-64 0z"}},{tag:"path",attrs:{d:"M811.4 368.9C765.6 248 648.9 162 512.2 162S258.8 247.9 213 368.8C126.9 391.5 63.5 470.2 64 563.6 64.6 668 145.6 752.9 247.6 762c4.7.4 8.7-3.3 8.7-8v-60.4c0-4-3-7.4-7-7.9-27-3.4-52.5-15.2-72.1-34.5-24-23.5-37.2-55.1-37.2-88.6 0-28 9.1-54.4 26.2-76.4 16.7-21.4 40.2-36.9 66.1-43.7l37.9-10 13.9-36.7c8.6-22.8 20.6-44.2 35.7-63.5 14.9-19.2 32.6-36 52.4-50 41.1-28.9 89.5-44.2 140-44.2s98.9 15.3 140 44.3c19.9 14 37.5 30.8 52.4 50 15.1 19.3 27.1 40.7 35.7 63.5l13.8 36.6 37.8 10c54.2 14.4 92.1 63.7 92.1 120 0 33.6-13.2 65.1-37.2 88.6-19.5 19.2-44.9 31.1-71.9 34.5-4 .5-6.9 3.9-6.9 7.9V754c0 4.7 4.1 8.4 8.8 8 101.7-9.2 182.5-94 183.2-198.2.6-93.4-62.7-172.1-148.6-194.9z"}}]},name:"cloud-server",theme:"outlined"};function c(r){for(var e=1;e{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)})).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1{font-size:inherit;font-weight:inherit}h1,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(g,n)=>(i(),a("div",l,[n[0]||(n[0]=e("div",{class:"-bottom-1/2 fixed h-1/2 left-0 right-0 spotlight"},null,-1)),e("div",c,[e("h1",{class:"font-medium mb-8 sm:text-10xl text-8xl",textContent:o(t.statusCode)},null,8,d),e("p",{class:"font-light leading-tight mb-16 px-8 sm:px-0 sm:text-4xl text-xl",textContent:o(t.description)},null,8,p)])]))}},h=s(f,[["__scopeId","data-v-4b6f0a29"]]);export{h as default};
2 |
--------------------------------------------------------------------------------
/frontend/src/composables/useBackendStatus.ts:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue'
2 | import { message } from 'ant-design-vue'
3 |
4 | // 后端连接状态类型
5 | export type BackendStatus = 'checking' | 'online' | 'offline'
6 |
7 | // 后端状态管理
8 | export const useBackendStatus = () => {
9 | const backendStatus = ref('checking')
10 | const backendError = ref('')
11 | let statusCheckInterval: ReturnType | null = null
12 |
13 | // 统一的后端状态检测函数
14 | // 参考index页面的逻辑,通过/proxies_status接口检测
15 | const checkBackendStatus = async (baseURL?: string) => {
16 | try {
17 | backendStatus.value = 'checking'
18 | backendError.value = ''
19 |
20 | // 处理API基础URL
21 | let apiBaseURL = baseURL
22 | if (!apiBaseURL) {
23 | if (typeof window !== 'undefined') {
24 | // 在浏览器环境中,使用相对路径
25 | apiBaseURL = ''
26 | } else {
27 | // 在服务端渲染时,使用默认的开发端口
28 | apiBaseURL = 'http://localhost:5000'
29 | }
30 | } else if (apiBaseURL === '/') {
31 | // 如果传入的是根路径,则使用空字符串表示相对路径
32 | apiBaseURL = ''
33 | }
34 |
35 | // 使用 /ping 接口检测后端是否在线,这个接口不需要认证
36 | // 如果ping成功,再尝试使用 /proxies_status 接口获取更详细的状态
37 | const pingUrl = apiBaseURL ? `${apiBaseURL}/ping` : '/ping'
38 | const response = await fetch(pingUrl, {
39 | method: 'GET'
40 | })
41 |
42 | if (response.ok) {
43 | // /ping 端点返回简单的文本 "API OK"
44 | const text = await response.text()
45 | if (text.trim() === 'API OK') {
46 | backendStatus.value = 'online'
47 | backendError.value = ''
48 | } else {
49 | backendStatus.value = 'offline'
50 | backendError.value = '后端服务响应异常'
51 | }
52 | } else {
53 | backendStatus.value = 'offline'
54 | backendError.value = `后端服务响应异常 (${response.status})`
55 | }
56 | } catch (error: any) {
57 | backendStatus.value = 'offline'
58 |
59 | // 根据错误类型显示不同的提示
60 | if (error.name === 'AbortError') {
61 | backendError.value = '连接超时,后端服务可能响应缓慢'
62 | } else if (error.code === 'ERR_NETWORK') {
63 | backendError.value = '无法连接到后端服务,请检查服务是否启动'
64 | } else if (error.code === 'ECONNABORTED') {
65 | backendError.value = '连接超时,后端服务可能响应缓慢'
66 | } else {
67 | backendError.value = error.message || '连接后端服务失败'
68 | }
69 |
70 | console.error('后端状态检测失败:', error)
71 | }
72 | }
73 |
74 | // 启动定期状态检测
75 | const startPeriodicCheck = (intervalSeconds: number = 30, baseURL?: string) => {
76 | // 清除现有的定时器
77 | if (statusCheckInterval) {
78 | clearInterval(statusCheckInterval)
79 | }
80 |
81 | // 立即执行一次检测
82 | checkBackendStatus(baseURL)
83 |
84 | // 设置定期检测
85 | statusCheckInterval = setInterval(() => {
86 | checkBackendStatus(baseURL)
87 | }, intervalSeconds * 1000)
88 | }
89 |
90 | // 停止定期状态检测
91 | const stopPeriodicCheck = () => {
92 | if (statusCheckInterval) {
93 | clearInterval(statusCheckInterval)
94 | statusCheckInterval = null
95 | }
96 | }
97 |
98 | // 注意:组件卸载时需要手动调用 stopPeriodicCheck() 来清理定时器
99 |
100 | return {
101 | backendStatus,
102 | backendError,
103 | checkBackendStatus,
104 | startPeriodicCheck,
105 | stopPeriodicCheck
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/fetchers/FetcherUtils.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """
4 | 爬取器通用工具函数
5 | 提供统一的请求处理、反爬虫规避等功能
6 | """
7 |
8 | import random
9 | import time
10 | import requests
11 | from requests.adapters import HTTPAdapter
12 | from urllib3.util.retry import Retry
13 | import urllib3
14 |
15 | # 禁用 SSL 警告
16 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
17 |
18 | # 常用的 User-Agent 列表
19 | USER_AGENTS = [
20 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
21 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36',
22 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
23 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
24 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
25 | ]
26 |
27 | def get_default_headers(referer=None):
28 | """
29 | 获取默认的请求头,模拟浏览器
30 |
31 | Args:
32 | referer: 可选的 Referer 头
33 |
34 | Returns:
35 | dict: 请求头字典
36 | """
37 | headers = {
38 | 'User-Agent': random.choice(USER_AGENTS),
39 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
40 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
41 | 'Accept-Encoding': 'gzip, deflate, br',
42 | 'Connection': 'keep-alive',
43 | 'Upgrade-Insecure-Requests': '1',
44 | 'Cache-Control': 'max-age=0',
45 | }
46 |
47 | if referer:
48 | headers['Referer'] = referer
49 |
50 | return headers
51 |
52 | def create_session_with_retry(retries=3, backoff_factor=1):
53 | """
54 | 创建带有重试机制的 requests Session
55 |
56 | Args:
57 | retries: 重试次数
58 | backoff_factor: 退避因子
59 |
60 | Returns:
61 | requests.Session: 配置好的 Session 对象
62 | """
63 | session = requests.Session()
64 | retry = Retry(
65 | total=retries,
66 | backoff_factor=backoff_factor,
67 | status_forcelist=[500, 502, 503, 504, 429]
68 | )
69 | adapter = HTTPAdapter(max_retries=retry)
70 | session.mount('http://', adapter)
71 | session.mount('https://', adapter)
72 | return session
73 |
74 | def safe_get(url, headers=None, timeout=15, verify=True, delay_range=(0.5, 2)):
75 | """
76 | 安全的 GET 请求,自动添加请求头、重试、延迟等
77 |
78 | Args:
79 | url: 请求 URL
80 | headers: 自定义请求头(会与默认请求头合并)
81 | timeout: 超时时间(秒)
82 | verify: 是否验证 SSL 证书
83 | delay_range: 随机延迟范围(秒),设为 None 则不延迟
84 |
85 | Returns:
86 | requests.Response: 响应对象
87 |
88 | Raises:
89 | Exception: 请求失败时抛出异常
90 | """
91 | # 添加随机延迟,避免请求过快
92 | if delay_range:
93 | time.sleep(random.uniform(delay_range[0], delay_range[1]))
94 |
95 | # 准备请求头
96 | default_headers = get_default_headers()
97 | if headers:
98 | default_headers.update(headers)
99 |
100 | # 创建带重试的 session
101 | session = create_session_with_retry()
102 |
103 | # 发送请求
104 | response = session.get(url, headers=default_headers, timeout=timeout, verify=verify)
105 | response.raise_for_status()
106 |
107 | return response
108 |
109 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/error-404.BSvats-j.css:
--------------------------------------------------------------------------------
1 | .spotlight[data-v-06403dcb]{background:linear-gradient(45deg,#00dc82,#36e4da 50%,#0047e1);bottom:-30vh;filter:blur(20vh);height:40vh}.gradient-border[data-v-06403dcb]{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:.5rem;position:relative}@media(prefers-color-scheme:light){.gradient-border[data-v-06403dcb]{background-color:#ffffff4d}.gradient-border[data-v-06403dcb]:before{background:linear-gradient(90deg,#e2e2e2,#e2e2e2 25%,#00dc82,#36e4da 75%,#0047e1)}}@media(prefers-color-scheme:dark){.gradient-border[data-v-06403dcb]{background-color:#1414144d}.gradient-border[data-v-06403dcb]:before{background:linear-gradient(90deg,#303030,#303030 25%,#00dc82,#36e4da 75%,#0047e1)}}.gradient-border[data-v-06403dcb]:before{background-size:400% auto;border-radius:.5rem;content:"";inset:0;-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:.5;padding:2px;position:absolute;transition:background-position .3s ease-in-out,opacity .2s ease-in-out;width:100%}.gradient-border[data-v-06403dcb]:hover:before{background-position:-50% 0;opacity:1}.fixed[data-v-06403dcb]{position:fixed}.left-0[data-v-06403dcb]{left:0}.right-0[data-v-06403dcb]{right:0}.z-10[data-v-06403dcb]{z-index:10}.z-20[data-v-06403dcb]{z-index:20}.grid[data-v-06403dcb]{display:grid}.mb-16[data-v-06403dcb]{margin-bottom:4rem}.mb-8[data-v-06403dcb]{margin-bottom:2rem}.max-w-520px[data-v-06403dcb]{max-width:520px}.min-h-screen[data-v-06403dcb]{min-height:100vh}.w-full[data-v-06403dcb]{width:100%}.flex[data-v-06403dcb]{display:flex}.cursor-pointer[data-v-06403dcb]{cursor:pointer}.place-content-center[data-v-06403dcb]{place-content:center}.items-center[data-v-06403dcb]{align-items:center}.justify-center[data-v-06403dcb]{justify-content:center}.overflow-hidden[data-v-06403dcb]{overflow:hidden}.bg-white[data-v-06403dcb]{--un-bg-opacity:1;background-color:rgb(255 255 255/var(--un-bg-opacity))}.px-4[data-v-06403dcb]{padding-left:1rem;padding-right:1rem}.px-8[data-v-06403dcb]{padding-left:2rem;padding-right:2rem}.py-2[data-v-06403dcb]{padding-bottom:.5rem;padding-top:.5rem}.text-center[data-v-06403dcb]{text-align:center}.text-8xl[data-v-06403dcb]{font-size:6rem;line-height:1}.text-xl[data-v-06403dcb]{font-size:1.25rem;line-height:1.75rem}.text-black[data-v-06403dcb]{--un-text-opacity:1;color:rgb(0 0 0/var(--un-text-opacity))}.font-light[data-v-06403dcb]{font-weight:300}.font-medium[data-v-06403dcb]{font-weight:500}.leading-tight[data-v-06403dcb]{line-height:1.25}.font-sans[data-v-06403dcb]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.antialiased[data-v-06403dcb]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media(prefers-color-scheme:dark){.dark\:bg-black[data-v-06403dcb]{--un-bg-opacity:1;background-color:rgb(0 0 0/var(--un-bg-opacity))}.dark\:text-white[data-v-06403dcb]{--un-text-opacity:1;color:rgb(255 255 255/var(--un-text-opacity))}}@media(min-width:640px){.sm\:px-0[data-v-06403dcb]{padding-left:0;padding-right:0}.sm\:px-6[data-v-06403dcb]{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-3[data-v-06403dcb]{padding-bottom:.75rem;padding-top:.75rem}.sm\:text-4xl[data-v-06403dcb]{font-size:2.25rem;line-height:2.5rem}.sm\:text-xl[data-v-06403dcb]{font-size:1.25rem;line-height:1.75rem}}
2 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/DzUz1rp7.js:
--------------------------------------------------------------------------------
1 | import{b as o,I as i}from"#entry";var u={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M854.4 800.9c.2-.3.5-.6.7-.9C920.6 722.1 960 621.7 960 512s-39.4-210.1-104.8-288c-.2-.3-.5-.5-.7-.8-1.1-1.3-2.1-2.5-3.2-3.7-.4-.5-.8-.9-1.2-1.4l-4.1-4.7-.1-.1c-1.5-1.7-3.1-3.4-4.6-5.1l-.1-.1c-3.2-3.4-6.4-6.8-9.7-10.1l-.1-.1-4.8-4.8-.3-.3c-1.5-1.5-3-2.9-4.5-4.3-.5-.5-1-1-1.6-1.5-1-1-2-1.9-3-2.8-.3-.3-.7-.6-1-1C736.4 109.2 629.5 64 512 64s-224.4 45.2-304.3 119.2c-.3.3-.7.6-1 1-1 .9-2 1.9-3 2.9-.5.5-1 1-1.6 1.5-1.5 1.4-3 2.9-4.5 4.3l-.3.3-4.8 4.8-.1.1c-3.3 3.3-6.5 6.7-9.7 10.1l-.1.1c-1.6 1.7-3.1 3.4-4.6 5.1l-.1.1c-1.4 1.5-2.8 3.1-4.1 4.7-.4.5-.8.9-1.2 1.4-1.1 1.2-2.1 2.5-3.2 3.7-.2.3-.5.5-.7.8C103.4 301.9 64 402.3 64 512s39.4 210.1 104.8 288c.2.3.5.6.7.9l3.1 3.7c.4.5.8.9 1.2 1.4l4.1 4.7c0 .1.1.1.1.2 1.5 1.7 3 3.4 4.6 5l.1.1c3.2 3.4 6.4 6.8 9.6 10.1l.1.1c1.6 1.6 3.1 3.2 4.7 4.7l.3.3c3.3 3.3 6.7 6.5 10.1 9.6 80.1 74 187 119.2 304.5 119.2s224.4-45.2 304.3-119.2a300 300 0 0010-9.6l.3-.3c1.6-1.6 3.2-3.1 4.7-4.7l.1-.1c3.3-3.3 6.5-6.7 9.6-10.1l.1-.1c1.5-1.7 3.1-3.3 4.6-5 0-.1.1-.1.1-.2 1.4-1.5 2.8-3.1 4.1-4.7.4-.5.8-.9 1.2-1.4a99 99 0 003.3-3.7zm4.1-142.6c-13.8 32.6-32 62.8-54.2 90.2a444.07 444.07 0 00-81.5-55.9c11.6-46.9 18.8-98.4 20.7-152.6H887c-3 40.9-12.6 80.6-28.5 118.3zM887 484H743.5c-1.9-54.2-9.1-105.7-20.7-152.6 29.3-15.6 56.6-34.4 81.5-55.9A373.86 373.86 0 01887 484zM658.3 165.5c39.7 16.8 75.8 40 107.6 69.2a394.72 394.72 0 01-59.4 41.8c-15.7-45-35.8-84.1-59.2-115.4 3.7 1.4 7.4 2.9 11 4.4zm-90.6 700.6c-9.2 7.2-18.4 12.7-27.7 16.4V697a389.1 389.1 0 01115.7 26.2c-8.3 24.6-17.9 47.3-29 67.8-17.4 32.4-37.8 58.3-59 75.1zm59-633.1c11 20.6 20.7 43.3 29 67.8A389.1 389.1 0 01540 327V141.6c9.2 3.7 18.5 9.1 27.7 16.4 21.2 16.7 41.6 42.6 59 75zM540 640.9V540h147.5c-1.6 44.2-7.1 87.1-16.3 127.8l-.3 1.2A445.02 445.02 0 00540 640.9zm0-156.9V383.1c45.8-2.8 89.8-12.5 130.9-28.1l.3 1.2c9.2 40.7 14.7 83.5 16.3 127.8H540zm-56 56v100.9c-45.8 2.8-89.8 12.5-130.9 28.1l-.3-1.2c-9.2-40.7-14.7-83.5-16.3-127.8H484zm-147.5-56c1.6-44.2 7.1-87.1 16.3-127.8l.3-1.2c41.1 15.6 85 25.3 130.9 28.1V484H336.5zM484 697v185.4c-9.2-3.7-18.5-9.1-27.7-16.4-21.2-16.7-41.7-42.7-59.1-75.1-11-20.6-20.7-43.3-29-67.8 37.2-14.6 75.9-23.3 115.8-26.1zm0-370a389.1 389.1 0 01-115.7-26.2c8.3-24.6 17.9-47.3 29-67.8 17.4-32.4 37.8-58.4 59.1-75.1 9.2-7.2 18.4-12.7 27.7-16.4V327zM365.7 165.5c3.7-1.5 7.3-3 11-4.4-23.4 31.3-43.5 70.4-59.2 115.4-21-12-40.9-26-59.4-41.8 31.8-29.2 67.9-52.4 107.6-69.2zM165.5 365.7c13.8-32.6 32-62.8 54.2-90.2 24.9 21.5 52.2 40.3 81.5 55.9-11.6 46.9-18.8 98.4-20.7 152.6H137c3-40.9 12.6-80.6 28.5-118.3zM137 540h143.5c1.9 54.2 9.1 105.7 20.7 152.6a444.07 444.07 0 00-81.5 55.9A373.86 373.86 0 01137 540zm228.7 318.5c-39.7-16.8-75.8-40-107.6-69.2 18.5-15.8 38.4-29.7 59.4-41.8 15.7 45 35.8 84.1 59.2 115.4-3.7-1.4-7.4-2.9-11-4.4zm292.6 0c-3.7 1.5-7.3 3-11 4.4 23.4-31.3 43.5-70.4 59.2-115.4 21 12 40.9 26 59.4 41.8a373.81 373.81 0 01-107.6 69.2z"}}]},name:"global",theme:"outlined"};function a(e){for(var c=1;c {
6 | // 主题状态
7 | const themeMode = ref('light')
8 | const isDark = ref(false)
9 |
10 | // 从本地存储加载主题设置
11 | const loadTheme = () => {
12 | if (typeof window !== 'undefined') {
13 | const saved = localStorage.getItem('theme-mode')
14 | if (saved && ['light', 'dark'].includes(saved)) {
15 | themeMode.value = saved as ThemeMode
16 | } else {
17 | // 默认为浅色模式
18 | themeMode.value = 'light'
19 | }
20 | updateTheme()
21 | }
22 | }
23 |
24 | // 更新主题
25 | const updateTheme = () => {
26 | if (typeof window !== 'undefined') {
27 | const shouldBeDark = themeMode.value === 'dark'
28 | isDark.value = shouldBeDark
29 |
30 | // 更新 HTML 类名
31 | const html = document.documentElement
32 | if (shouldBeDark) {
33 | html.classList.add('dark')
34 | html.classList.remove('light')
35 | } else {
36 | html.classList.add('light')
37 | html.classList.remove('dark')
38 | }
39 |
40 | // 更新 Ant Design 主题
41 | updateAntdTheme(shouldBeDark)
42 | }
43 | }
44 |
45 | // 更新 Ant Design 主题
46 | const updateAntdTheme = (dark: boolean) => {
47 | if (typeof window !== 'undefined') {
48 | const configProvider = document.querySelector('.ant-config-provider')
49 | if (configProvider) {
50 | configProvider.setAttribute('data-theme', dark ? 'dark' : 'light')
51 | }
52 |
53 | // 强制更新 Ant Design 的 CSS 变量
54 | const root = document.documentElement
55 | if (dark) {
56 | root.style.setProperty('--ant-layout-bg', '#1a1a1a')
57 | root.style.setProperty('--ant-layout-sider-bg', '#2d2d2d')
58 | root.style.setProperty('--ant-layout-header-bg', '#2d2d2d')
59 | root.style.setProperty('--ant-layout-content-bg', '#1a1a1a')
60 | root.style.setProperty('--ant-layout-footer-bg', '#2d2d2d')
61 | } else {
62 | root.style.removeProperty('--ant-layout-bg')
63 | root.style.removeProperty('--ant-layout-sider-bg')
64 | root.style.removeProperty('--ant-layout-header-bg')
65 | root.style.removeProperty('--ant-layout-content-bg')
66 | root.style.removeProperty('--ant-layout-footer-bg')
67 | }
68 | }
69 | }
70 |
71 | // 切换主题
72 | const toggleTheme = () => {
73 | themeMode.value = themeMode.value === 'light' ? 'dark' : 'light'
74 | }
75 |
76 | // 设置特定主题
77 | const setTheme = (mode: ThemeMode) => {
78 | themeMode.value = mode
79 | }
80 |
81 | // 监听主题模式变化
82 | watch(themeMode, (newMode) => {
83 | if (typeof window !== 'undefined') {
84 | localStorage.setItem('theme-mode', newMode)
85 | updateTheme()
86 | }
87 | })
88 |
89 | // 监听系统主题变化(已移除,只支持手动切换)
90 | const watchSystemTheme = () => {
91 | // 不再需要监听系统主题变化
92 | }
93 |
94 | // 主题图标
95 | const themeIcon = computed(() => {
96 | return themeMode.value === 'light' ? 'SunOutlined' : 'MoonOutlined'
97 | })
98 |
99 | // 主题标签
100 | const themeLabel = computed(() => {
101 | return themeMode.value === 'light' ? '浅色模式' : '深色模式'
102 | })
103 |
104 | return {
105 | themeMode,
106 | isDark,
107 | themeIcon,
108 | themeLabel,
109 | loadTheme,
110 | toggleTheme,
111 | setTheme,
112 | updateTheme,
113 | watchSystemTheme
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/BNRG3dZD.js:
--------------------------------------------------------------------------------
1 | import{_ as a}from"./CmFl7aLf.js";import{_ as i,c as u,o as c,a as e,t as r,b as l,w as d,d as p}from"#entry";import{u as f}from"./Hsbwpiff.js";const m={class:"antialiased bg-white dark:bg-black dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-black"},g={class:"max-w-520px text-center z-20"},b=["textContent"],h=["textContent"],x={class:"flex items-center justify-center w-full"},y={__name:"error-404",props:{appName:{type:String,default:"Nuxt"},version:{type:String,default:""},statusCode:{type:Number,default:404},statusMessage:{type:String,default:"Not Found"},description:{type:String,default:"Sorry, the page you are looking for could not be found."},backHome:{type:String,default:"Go back home"}},setup(t){const n=t;return f({title:`${n.statusCode} - ${n.statusMessage} | ${n.appName}`,script:[{innerHTML:`!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver((e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)})).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}h1,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(k,o)=>{const s=a;return c(),u("div",m,[o[0]||(o[0]=e("div",{class:"fixed left-0 right-0 spotlight z-10"},null,-1)),e("div",g,[e("h1",{class:"font-medium mb-8 sm:text-10xl text-8xl",textContent:r(t.statusCode)},null,8,b),e("p",{class:"font-light leading-tight mb-16 px-8 sm:px-0 sm:text-4xl text-xl",textContent:r(t.description)},null,8,h),e("div",x,[l(s,{to:"/",class:"cursor-pointer gradient-border px-4 py-2 sm:px-6 sm:py-3 sm:text-xl text-md"},{default:d(()=>[p(r(t.backHome),1)]),_:1})])])])}}},z=i(y,[["__scopeId","data-v-06403dcb"]]);export{z as default};
2 |
--------------------------------------------------------------------------------
/proc/run_fetcher.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | """
3 | 定时运行爬取器
4 | """
5 |
6 | import sys
7 | import threading
8 | from queue import Queue
9 | import logging
10 | import time
11 | from db import conn
12 | from fetchers import fetchers
13 | from data.config import PROC_FETCHER_SLEEP
14 | from func_timeout import func_set_timeout
15 | from func_timeout.exceptions import FunctionTimedOut
16 |
17 | logging.basicConfig(stream=sys.stdout, format="%(asctime)s-%(levelname)s:%(name)s:%(message)s", level='INFO')
18 |
19 | def main(proc_lock):
20 | """
21 | 定时运行爬取器
22 | 主要逻辑:
23 | While True:
24 | for 爬取器 in 所有爬取器:
25 | 查询数据库,判断当前爬取器是否需要运行
26 | 如果需要运行,那么启动线程运行该爬取器
27 | 等待所有线程结束
28 | 将爬取到的代理放入数据库中
29 | 睡眠一段时间
30 | """
31 | logger = logging.getLogger('fetcher')
32 | conn.set_proc_lock(proc_lock)
33 |
34 | while True:
35 | logger.info('开始运行一轮爬取器')
36 | status = conn.getProxiesStatus()
37 | if status['pending_proxies_cnt'] > 2000:
38 | logger.info(f"还有{status['pending_proxies_cnt']}个代理等待验证,数量过多,跳过本次爬取")
39 | time.sleep(PROC_FETCHER_SLEEP)
40 | continue
41 |
42 | @func_set_timeout(30)
43 | def fetch_worker(fetcher):
44 | f = fetcher()
45 | proxies = f.fetch()
46 | return proxies
47 |
48 | def run_thread(name, fetcher, que):
49 | """
50 | name: 爬取器名称
51 | fetcher: 爬取器class
52 | que: 队列,用于返回数据
53 | """
54 | try:
55 | proxies = fetch_worker(fetcher)
56 | que.put((name, proxies))
57 | except Exception as e:
58 | logger.error(f'运行爬取器{name}出错:' + str(e))
59 | que.put((name, []))
60 | except FunctionTimedOut:
61 | pass
62 |
63 | threads = []
64 | que = Queue()
65 | for item in fetchers:
66 | data = conn.getFetcher(item.name)
67 | if data is None:
68 | logger.error(f'没有在数据库中找到对应的信息:{item.name}')
69 | raise ValueError('不可恢复错误')
70 | if not data.enable:
71 | logger.info(f'跳过爬取器{item.name}')
72 | continue
73 | threads.append(threading.Thread(target=run_thread, args=(item.name, item.fetcher, que)))
74 | [t.start() for t in threads]
75 | [t.join() for t in threads]
76 | while not que.empty():
77 | fetcher_name, proxies = que.get()
78 | for proxy in proxies:
79 | # 支持多种格式:
80 | # 1. (protocol, ip, port) - 只有代理
81 | # 2. (protocol, ip, port, username, password) - 有认证
82 | # 3. (protocol, ip, port, username, password, country, address) - 完整信息
83 | if len(proxy) == 3:
84 | protocol, ip, port = proxy
85 | conn.pushNewFetch(fetcher_name, protocol, ip, port)
86 | elif len(proxy) == 5:
87 | protocol, ip, port, username, password = proxy
88 | conn.pushNewFetch(fetcher_name, protocol, ip, port, username, password)
89 | elif len(proxy) == 7:
90 | protocol, ip, port, username, password, country, address = proxy
91 | conn.pushNewFetch(fetcher_name, protocol, ip, port, username, password, country, address)
92 | else:
93 | logger.warning(f'爬取器{fetcher_name}返回了格式错误的代理: {proxy},长度={len(proxy)}')
94 | conn.pushFetcherResult(fetcher_name, len(proxies))
95 |
96 | logger.info(f'完成运行{len(threads)}个爬取器,睡眠{PROC_FETCHER_SLEEP}秒')
97 | time.sleep(PROC_FETCHER_SLEEP)
98 |
--------------------------------------------------------------------------------
/utils/ip_location.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import requests
4 | import time
5 |
6 | class IPLocation:
7 | """
8 | IP地理位置查询工具
9 | 使用免费的IP API服务查询IP地址的国家和详细地址
10 | """
11 |
12 | @staticmethod
13 | def get_location(ip: str) -> dict:
14 | """
15 | 获取IP地址的地理位置信息
16 | 返回: {'country': '国家', 'address': '详细地址'}
17 | """
18 | # 检查是否为内网IP
19 | if ip.startswith('192.168') or ip.startswith('10.') or ip.startswith('172.16') or ip.startswith('127.'):
20 | return {'country': '本地', 'address': '内网地址'}
21 |
22 | # 尝试多个免费API服务
23 | result = IPLocation._try_ip_api_com(ip)
24 | if result:
25 | return result
26 |
27 | # 如果第一个API失败,尝试第二个
28 | result = IPLocation._try_ipapi_co(ip)
29 | if result:
30 | return result
31 |
32 | # 如果都失败,返回默认值
33 | return {'country': '未知', 'address': '无法获取'}
34 |
35 | @staticmethod
36 | def _try_ip_api_com(ip: str) -> dict:
37 | """
38 | 使用 ip-api.com 查询
39 | 免费限制: 45次/分钟
40 | """
41 | try:
42 | url = f'http://ip-api.com/json/{ip}?lang=zh-CN&fields=status,country,regionName,city,isp'
43 | response = requests.get(url, timeout=3)
44 | if response.status_code == 200:
45 | data = response.json()
46 | if data.get('status') == 'success':
47 | country = data.get('country', '未知')
48 | region = data.get('regionName', '')
49 | city = data.get('city', '')
50 | isp = data.get('isp', '')
51 |
52 | # 组合详细地址
53 | address_parts = [part for part in [country, region, city, isp] if part]
54 | address = ' '.join(address_parts)
55 |
56 | return {'country': country, 'address': address}
57 | except Exception as e:
58 | print(f"IP查询失败(ip-api.com): {e}")
59 |
60 | return None
61 |
62 | @staticmethod
63 | def _try_ipapi_co(ip: str) -> dict:
64 | """
65 | 使用 ipapi.co 查询
66 | 免费限制: 1000次/天
67 | """
68 | try:
69 | url = f'https://ipapi.co/{ip}/json/'
70 | response = requests.get(url, timeout=3)
71 | if response.status_code == 200:
72 | data = response.json()
73 | country = data.get('country_name', '未知')
74 | region = data.get('region', '')
75 | city = data.get('city', '')
76 | org = data.get('org', '')
77 |
78 | # 组合详细地址
79 | address_parts = [part for part in [country, region, city, org] if part]
80 | address = ' '.join(address_parts)
81 |
82 | return {'country': country, 'address': address}
83 | except Exception as e:
84 | print(f"IP查询失败(ipapi.co): {e}")
85 |
86 | return None
87 |
88 | # 缓存,避免重复查询同一个IP
89 | _ip_cache = {}
90 | _cache_expire_time = 3600 # 缓存1小时
91 |
92 | def get_ip_location_cached(ip: str) -> dict:
93 | """
94 | 带缓存的IP位置查询
95 | """
96 | current_time = time.time()
97 |
98 | # 检查缓存
99 | if ip in _ip_cache:
100 | cached_data, cache_time = _ip_cache[ip]
101 | if current_time - cache_time < _cache_expire_time:
102 | return cached_data
103 |
104 | # 查询新数据
105 | result = IPLocation.get_location(ip)
106 | _ip_cache[ip] = (result, current_time)
107 |
108 | return result
109 |
110 |
--------------------------------------------------------------------------------
/frontend/src/plugins/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import type { AxiosInstance } from 'axios'
3 | import { message } from 'ant-design-vue'
4 |
5 | // @ts-ignore - Nuxt 3 auto-import
6 | export default defineNuxtPlugin((nuxtApp) => {
7 | // @ts-ignore - Nuxt 3 auto-import
8 | const config = useRuntimeConfig()
9 | const baseURL = config.public.apiBase as string
10 | // @ts-ignore - Nuxt 3 auto-import
11 | const router = useRouter()
12 |
13 | const axiosInstance: AxiosInstance = axios.create({
14 | baseURL,
15 | timeout: 30000, // 增加到 30 秒,避免某些操作超時
16 | withCredentials: true
17 | })
18 |
19 | // 请求拦截器 - 添加 token
20 | axiosInstance.interceptors.request.use(
21 | (config) => {
22 | // 从 localStorage 获取 token
23 | const token = localStorage.getItem('token')
24 | if (token) {
25 | config.headers.Authorization = `Bearer ${token}`
26 | }
27 | return config
28 | },
29 | (error) => {
30 | return Promise.reject(error)
31 | }
32 | )
33 |
34 | // 响应拦截器 - 处理错误和未授权
35 | axiosInstance.interceptors.response.use(
36 | (response) => {
37 | return response
38 | },
39 | async (error) => {
40 | if (error.response) {
41 | const status = error.response.status
42 | const data = error.response.data
43 |
44 | // 401 未授权 - token无效或已过期
45 | if (status === 401) {
46 | // 清除本地存储
47 | localStorage.removeItem('token')
48 | localStorage.removeItem('user')
49 |
50 | // 如果不是在登录页面,显示提示并跳转
51 | if (router.currentRoute.value.path !== '/login') {
52 | message.error('登录已过期,请重新登录')
53 | // 跳转到登录页
54 | setTimeout(() => {
55 | router.push('/login')
56 | }, 1000)
57 | }
58 | }
59 | // 403 禁止访问
60 | else if (status === 403) {
61 | message.error('没有权限访问')
62 | }
63 | // 其他错误
64 | else {
65 | console.error('API错误:', data.message || error.message)
66 | }
67 | } else if (error.code === 'ECONNABORTED') {
68 | // 超时错误,不打印到控制台,让调用者处理
69 | console.warn('请求超时:', error.config?.url)
70 | } else if (error.code === 'ERR_NETWORK') {
71 | // 网络错误
72 | console.error('网络连接失败,请检查后端服务是否启动')
73 | } else {
74 | console.error('请求错误:', error.message)
75 | }
76 |
77 | return Promise.reject(error)
78 | }
79 | )
80 |
81 | class Http {
82 | baseURL: string
83 |
84 | constructor() {
85 | this.baseURL = baseURL
86 | }
87 |
88 | async get(url: string, params?: any) {
89 | try {
90 | const response = await axiosInstance.get(url, { params })
91 | const data = response.data
92 |
93 | if (!data.success) {
94 | throw new Error(data.message || 'API 返回错误')
95 | }
96 |
97 | return data
98 | } catch (error: any) {
99 | // 不在这里重复打印,由拦截器处理
100 | throw error
101 | }
102 | }
103 |
104 | async post(url: string, data?: any, params?: any) {
105 | try {
106 | const response = await axiosInstance.post(url, data, { params })
107 | const resData = response.data
108 |
109 | if (!resData.success) {
110 | throw new Error(resData.message || 'API 返回错误')
111 | }
112 |
113 | return resData
114 | } catch (error: any) {
115 | // 不在这里重复打印,由拦截器处理
116 | throw error
117 | }
118 | }
119 | }
120 |
121 | const $http = new Http()
122 |
123 | return {
124 | provide: {
125 | http: $http
126 | }
127 | }
128 | })
129 |
130 |
--------------------------------------------------------------------------------
/setup_security.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # encoding: utf-8
3 |
4 | """
5 | ProxyPool 安全配置脚本
6 | 用于生成和配置 JWT 密钥等安全设置
7 | """
8 |
9 | import os
10 | import sys
11 | import secrets
12 | import subprocess
13 | import platform
14 |
15 | def generate_jwt_secret_key(length=32):
16 | """生成安全的JWT密钥"""
17 | return secrets.token_urlsafe(length)
18 |
19 | def print_banner():
20 | """打印欢迎横幅"""
21 | print("=" * 80)
22 | print("🔐 ProxyPool 安全配置工具")
23 | print("=" * 80)
24 | print("此工具将帮助您配置 ProxyPool 的安全设置")
25 | print("=" * 80)
26 |
27 | def check_current_config():
28 | """检查当前配置"""
29 | print("\n📋 检查当前配置...")
30 |
31 | current_key = os.environ.get('JWT_SECRET_KEY')
32 | if current_key:
33 | print(f"✅ 已设置 JWT_SECRET_KEY (长度: {len(current_key)})")
34 | if len(current_key) >= 32:
35 | print("✅ 密钥长度符合安全要求")
36 | else:
37 | print("⚠️ 密钥长度不足,建议至少32字符")
38 | else:
39 | print("❌ 未设置 JWT_SECRET_KEY 环境变量")
40 |
41 | def generate_new_key():
42 | """生成新的JWT密钥"""
43 | print("\n🔑 生成新的JWT密钥...")
44 |
45 | # 生成32字符的强密钥
46 | new_key = generate_jwt_secret_key(32)
47 | print(f"新密钥: {new_key}")
48 | print(f"密钥长度: {len(new_key)} 字符")
49 |
50 | return new_key
51 |
52 | def show_setup_commands(key):
53 | """显示配置信息"""
54 | print("\n📝 配置信息:")
55 | print("-" * 50)
56 | print("✅ 密钥已生成并保存到 .env 文件中")
57 | print("✅ 程序会自动读取 .env 文件中的配置")
58 | print("✅ 无需手动设置环境变量")
59 | print("")
60 | print("🔧 如果需要手动修改配置:")
61 | print(" 编辑 .env 文件,修改 JWT_SECRET_KEY 的值")
62 | print("")
63 | print("🔍 当前配置的密钥:")
64 | print(f" {key[:20]}...{key[-10:]} (长度: {len(key)})")
65 |
66 | def create_env_file(key):
67 | """创建 .env 文件"""
68 | env_file = ".env"
69 | print(f"\n📄 创建 {env_file} 文件...")
70 |
71 | try:
72 | with open(env_file, 'w', encoding='utf-8') as f:
73 | f.write(f"# ProxyPool 环境变量配置\n")
74 | f.write(f"# 生成时间: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
75 | f.write(f"JWT_SECRET_KEY={key}\n")
76 |
77 | print(f"✅ 已创建 {env_file} 文件")
78 | print("💡 提示: 请将 .env 文件添加到 .gitignore 中,避免提交到版本控制")
79 |
80 | except Exception as e:
81 | print(f"❌ 创建 {env_file} 文件失败: {e}")
82 |
83 | def test_configuration():
84 | """测试配置"""
85 | print("\n🧪 测试配置...")
86 |
87 | try:
88 | # 尝试导入配置
89 | sys.path.insert(0, os.path.dirname(__file__))
90 | from data.config import JWT_SECRET_KEY
91 |
92 | print("✅ 配置导入成功")
93 | print(f"✅ JWT密钥长度: {len(JWT_SECRET_KEY)}")
94 |
95 | if len(JWT_SECRET_KEY) >= 32:
96 | print("✅ 安全配置验证通过")
97 | return True
98 | else:
99 | print("❌ 密钥长度不足")
100 | return False
101 |
102 | except Exception as e:
103 | print(f"❌ 配置测试失败: {e}")
104 | return False
105 |
106 | def main():
107 | """主函数"""
108 | print_banner()
109 |
110 | # 检查当前配置
111 | check_current_config()
112 |
113 | # 生成新密钥
114 | new_key = generate_new_key()
115 |
116 | # 显示设置命令
117 | show_setup_commands(new_key)
118 |
119 | # 创建 .env 文件
120 | create_env_file(new_key)
121 |
122 | # 测试配置
123 | print("\n🧪 测试配置...")
124 | if test_configuration():
125 | print("\n🎉 安全配置完成!")
126 | print("✅ 现在可以安全地运行 ProxyPool 了")
127 | print("\n💡 提示:")
128 | print(" - 配置已保存到 .env 文件中")
129 | print(" - 直接运行: python main.py")
130 | else:
131 | print("\n⚠️ 配置测试失败,请检查设置")
132 |
133 | if __name__ == "__main__":
134 | main()
135 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/fetchers.u5Umj_Q-.css:
--------------------------------------------------------------------------------
1 | .fetchers-page[data-v-388abd13]{padding:0}.control-row[data-v-388abd13]{margin-bottom:24px}.control-card[data-v-388abd13]{align-items:center;border-radius:12px;display:flex;flex-direction:column;gap:12px;height:130px;justify-content:center;padding:20px;text-align:center}.control-header[data-v-388abd13]{color:#000000d9;font-size:16px;font-weight:500}.control-header[data-v-388abd13],.control-info[data-v-388abd13]{align-items:center;display:flex;gap:8px}.control-info[data-v-388abd13]{color:#000000a6;font-family:Courier New,monospace;font-size:14px}.help-icon[data-v-388abd13]{cursor:help;margin-top:8px;opacity:.6}.stats-card[data-v-388abd13]{border-radius:12px;height:130px;justify-content:space-around;padding:20px}.stats-card[data-v-388abd13],.stats-item[data-v-388abd13]{align-items:center;display:flex}.stats-item[data-v-388abd13]{flex-direction:column;gap:8px}.stats-label[data-v-388abd13]{color:#000000a6;font-size:13px}.stats-value[data-v-388abd13]{color:#1890ff;font-size:28px;font-weight:700}.stats-value.success[data-v-388abd13]{color:#52c41a}.stats-value.disabled[data-v-388abd13]{color:#00000040}.fetchers-grid[data-v-388abd13]{display:grid;gap:20px;grid-template-columns:repeat(auto-fill,minmax(320px,1fr))}.fetcher-card[data-v-388abd13]{border-radius:12px;overflow:hidden;padding:16px;position:relative;transition:all .3s}.fetcher-card[data-v-388abd13]:before{background:linear-gradient(90deg,#1890ff,#52c41a);content:"";height:3px;left:0;position:absolute;right:0;top:0;transform:scaleX(0);transition:transform .3s}.fetcher-card[data-v-388abd13]:hover:before{transform:scaleX(1)}.fetcher-status[data-v-388abd13]{align-items:center;background:#0000000a;border-radius:12px;display:flex;font-size:12px;gap:6px;padding:4px 12px;position:absolute;right:16px;top:16px}.fetcher-status.active[data-v-388abd13]{background:#52c41a1a}.status-dot[data-v-388abd13]{animation:pulse 2s ease-in-out infinite;background:#00000040;border-radius:50%;height:6px;width:6px}.fetcher-status.active .status-dot[data-v-388abd13]{background:#52c41a}.status-text[data-v-388abd13]{color:#000000a6;font-weight:500}.fetcher-status.active .status-text[data-v-388abd13]{color:#52c41a}.fetcher-header[data-v-388abd13]{justify-content:space-between;margin-bottom:12px;padding-right:80px}.fetcher-header[data-v-388abd13],.fetcher-name[data-v-388abd13]{align-items:center;display:flex}.fetcher-name[data-v-388abd13]{color:#000000d9;font-size:14px;font-weight:600;gap:6px;margin:0}.fetcher-stats[data-v-388abd13]{flex-direction:column;margin-bottom:12px}.fetcher-stats[data-v-388abd13],.stat-row[data-v-388abd13]{display:flex;gap:8px}.stat-item[data-v-388abd13]{align-items:center;background:#00000005;border-radius:6px;display:flex;flex:1;gap:6px;padding:8px 10px;transition:all .3s}.stat-item.compact[data-v-388abd13]{justify-content:flex-start}.stat-item[data-v-388abd13]:hover{background:#0000000a}.stat-icon[data-v-388abd13]{flex-shrink:0;font-size:14px}.stat-icon.success[data-v-388abd13]{color:#52c41a}.stat-icon.info[data-v-388abd13]{color:#1890ff}.stat-icon.primary[data-v-388abd13]{color:#667eea}.stat-icon.warning[data-v-388abd13]{color:#faad14}.stat-label[data-v-388abd13]{color:#00000073;font-size:11px;white-space:nowrap}.stat-number[data-v-388abd13]{color:#000000d9;font-size:14px;font-weight:600;margin-left:auto}.fetcher-footer[data-v-388abd13]{border-top:1px solid rgba(0,0,0,.06);justify-content:space-between;margin-top:12px;padding-top:12px}.fetcher-footer[data-v-388abd13],.footer-item[data-v-388abd13]{align-items:center;display:flex}.footer-item[data-v-388abd13]{color:#000000a6;font-size:13px;gap:6px}.footer-time[data-v-388abd13]{color:#00000073;font-family:Courier New,monospace;font-size:12px}.no-data[data-v-388abd13]{color:#00000040}.fetcher-progress[data-v-388abd13]{margin-top:12px}@media(max-width:768px){.fetchers-grid[data-v-388abd13]{grid-template-columns:1fr}.control-card[data-v-388abd13]{height:auto;min-height:110px;padding:16px}.stats-card[data-v-388abd13]{gap:12px;height:auto;padding:16px}.fetcher-stats .stat-row[data-v-388abd13],.stats-card[data-v-388abd13]{flex-direction:column}}@media(max-width:480px){.fetcher-header[data-v-388abd13]{align-items:flex-start;flex-direction:column;gap:12px;padding-right:0}.fetcher-status[data-v-388abd13]{margin-bottom:12px;position:static}}
2 |
--------------------------------------------------------------------------------
/proc/run_validator.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | """
3 | 验证器逻辑
4 | """
5 |
6 | import sys
7 | import socket
8 | import threading
9 | from queue import Queue
10 | import logging
11 | import time
12 | import requests
13 | from func_timeout import func_set_timeout
14 | from func_timeout.exceptions import FunctionTimedOut
15 | from db import conn
16 | from data.config import PROC_VALIDATOR_SLEEP, VALIDATE_THREAD_NUM
17 | from data.config import VALIDATE_METHOD, VALIDATE_KEYWORD, VALIDATE_HEADER, VALIDATE_URL, VALIDATE_TIMEOUT, VALIDATE_MAX_FAILS
18 |
19 | logging.basicConfig(stream=sys.stdout, format="%(asctime)s-%(levelname)s:%(name)s:%(message)s", level='INFO')
20 |
21 | def main(proc_lock):
22 | """
23 | 验证器
24 | 主要逻辑:
25 | 创建VALIDATE_THREAD_NUM个验证线程,这些线程会不断运行
26 | While True:
27 | 检查验证线程是否返回了代理的验证结果
28 | 从数据库中获取若干当前待验证的代理
29 | 将代理发送给前面创建的线程
30 | """
31 | logger = logging.getLogger('validator')
32 | conn.set_proc_lock(proc_lock)
33 |
34 | in_que = Queue()
35 | out_que = Queue()
36 | running_proxies = set() # 储存哪些代理正在运行,以字符串的形式储存
37 |
38 | threads = []
39 | for _ in range(VALIDATE_THREAD_NUM):
40 | threads.append(threading.Thread(target=validate_thread, args=(in_que, out_que)))
41 | [_.start() for _ in threads]
42 |
43 | while True:
44 | out_cnt = 0
45 | while not out_que.empty():
46 | proxy, success, latency = out_que.get()
47 | conn.pushValidateResult(proxy, success, latency)
48 | uri = f'{proxy.protocol}://{proxy.ip}:{proxy.port}'
49 | running_proxies.remove(uri)
50 | out_cnt = out_cnt + 1
51 | if out_cnt > 0:
52 | logger.info(f'完成了{out_cnt}个代理的验证')
53 |
54 | # 如果正在进行验证的代理足够多,那么就不着急添加新代理
55 | if len(running_proxies) >= VALIDATE_THREAD_NUM * 2:
56 | time.sleep(PROC_VALIDATOR_SLEEP)
57 | continue
58 |
59 | # 找一些新的待验证的代理放入队列中
60 | added_cnt = 0
61 | for proxy in conn.getToValidate(VALIDATE_THREAD_NUM * 4):
62 | uri = f'{proxy.protocol}://{proxy.ip}:{proxy.port}'
63 | # 这里找出的代理有可能是正在进行验证的代理,要避免重复加入
64 | if uri not in running_proxies:
65 | running_proxies.add(uri)
66 | in_que.put(proxy)
67 | added_cnt += 1
68 |
69 | if added_cnt == 0:
70 | time.sleep(PROC_VALIDATOR_SLEEP)
71 |
72 | @func_set_timeout(VALIDATE_TIMEOUT * 2)
73 | def validate_once(proxy):
74 | """
75 | 进行一次验证,如果验证成功则返回True,否则返回False或者是异常
76 | """
77 | proxies = {
78 | 'http': f'{proxy.protocol}://{proxy.ip}:{proxy.port}',
79 | 'https': f'{proxy.protocol}://{proxy.ip}:{proxy.port}'
80 | }
81 | if VALIDATE_METHOD == "GET":
82 | r = requests.get(VALIDATE_URL, timeout=VALIDATE_TIMEOUT, proxies=proxies)
83 | r.encoding = "utf-8"
84 | html = r.text
85 | if VALIDATE_KEYWORD in html:
86 | return True
87 | return False
88 | else:
89 | r = requests.get(VALIDATE_URL, timeout=VALIDATE_TIMEOUT, proxies=proxies, allow_redirects=False)
90 | resp_headers = r.headers
91 | if VALIDATE_HEADER in resp_headers.keys() and VALIDATE_KEYWORD in resp_headers[VALIDATE_HEADER]:
92 | return True
93 | return False
94 |
95 | def validate_thread(in_que, out_que):
96 | """
97 | 验证函数,这个函数会在一个线程中被调用
98 | in_que: 输入队列,用于接收验证任务
99 | out_que: 输出队列,用于返回验证结果
100 | in_que和out_que都是线程安全队列,并且如果队列为空,调用in_que.get()会阻塞线程
101 | """
102 |
103 | while True:
104 | proxy = in_que.get()
105 |
106 | success = False
107 | latency = None
108 | for _ in range(VALIDATE_MAX_FAILS):
109 | try:
110 | start_time = time.time()
111 | if validate_once(proxy):
112 | end_time = time.time()
113 | latency = int((end_time-start_time)*1000)
114 | success = True
115 | break
116 | except Exception as e:
117 | pass
118 | except FunctionTimedOut:
119 | pass
120 |
121 | out_que.put((proxy, success, latency))
122 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/theme-test.BaDAO6fs.css:
--------------------------------------------------------------------------------
1 | .theme-test-page[data-v-cd44458d]{background:var(--bg-primary);min-height:100vh;padding:24px;position:relative}.test-card[data-v-cd44458d],.theme-test-page[data-v-cd44458d]{transition:all var(--transition-normal)}.test-card[data-v-cd44458d]{background:var(--bg-card);border:1px solid var(--border-color);box-shadow:var(--box-shadow);margin:0 auto;max-width:1200px}.theme-header[data-v-cd44458d]{align-items:center;background:var(--accent-light);border:1px solid var(--border-color);border-radius:var(--border-radius);display:flex;justify-content:space-between;margin-bottom:16px;padding:16px}.theme-header h2[data-v-cd44458d]{color:var(--text-primary);margin:0;transition:color var(--transition-normal)}.theme-indicator[data-v-cd44458d]{align-items:center;color:var(--text-secondary);display:flex;gap:8px}.indicator-dot[data-v-cd44458d]{border-radius:50%;height:12px;transition:all var(--transition-normal);width:12px}.indicator-dot.light[data-v-cd44458d]{background:#faad14;box-shadow:0 0 8px #faad1480}.indicator-dot.dark[data-v-cd44458d]{background:#722ed1;box-shadow:0 0 8px #722ed180}.test-content p[data-v-cd44458d]{color:var(--text-secondary);margin-bottom:24px;transition:color var(--transition-normal)}.test-sections[data-v-cd44458d]{display:grid;gap:24px;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));margin-top:24px}.section-card[data-v-cd44458d]{background:var(--bg-card);border:1px solid var(--border-color);transition:all var(--transition-normal)}.color-grid[data-v-cd44458d]{display:grid;gap:16px;grid-template-columns:repeat(2,1fr)}.color-item[data-v-cd44458d]{align-items:center;border-radius:8px;color:#fff;display:flex;flex-direction:column;font-weight:500;gap:8px;padding:16px;text-align:center;transition:all var(--transition-normal)}.color-preview[data-v-cd44458d]{border:2px solid hsla(0,0%,100%,.3);border-radius:50%;height:40px;width:40px}.color-item.primary[data-v-cd44458d],.color-item.primary .color-preview[data-v-cd44458d]{background:var(--primary-color)}.color-item.success[data-v-cd44458d],.color-item.success .color-preview[data-v-cd44458d]{background:var(--success-color)}.color-item.warning[data-v-cd44458d],.color-item.warning .color-preview[data-v-cd44458d]{background:var(--warning-color)}.color-item.error[data-v-cd44458d],.color-item.error .color-preview[data-v-cd44458d]{background:var(--error-color)}.component-test[data-v-cd44458d]{margin-bottom:16px}.component-test[data-v-cd44458d],.form-test[data-v-cd44458d]{display:flex;flex-wrap:wrap;gap:12px}.enhanced-input[data-v-cd44458d],.enhanced-select[data-v-cd44458d]{border-radius:var(--border-radius);transition:all var(--transition-normal)}.enhanced-input[data-v-cd44458d]:focus,.enhanced-select[data-v-cd44458d]:focus{box-shadow:0 0 0 2px var(--accent-color)}.text-test h1[data-v-cd44458d],.text-test h2[data-v-cd44458d],.text-test h3[data-v-cd44458d]{margin:16px 0 8px}.text-test h1[data-v-cd44458d],.text-test h2[data-v-cd44458d],.text-test h3[data-v-cd44458d],.text-test p[data-v-cd44458d]{color:var(--text-primary);transition:color var(--transition-normal)}.text-test p[data-v-cd44458d]{margin:8px 0}.text-test .text-secondary[data-v-cd44458d]{color:var(--text-secondary)}.text-test .text-disabled[data-v-cd44458d]{color:var(--text-disabled)}.effects-test[data-v-cd44458d]{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.gradient-box[data-v-cd44458d]{background:var(--gradient-primary);border-radius:var(--border-radius);color:#fff;padding:20px;text-align:center;transition:all var(--transition-normal)}.gradient-box[data-v-cd44458d]:hover{box-shadow:var(--box-shadow-hover);transform:translateY(-2px)}.glass-effect[data-v-cd44458d]{backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:#ffffff1a;border:1px solid hsla(0,0%,100%,.2);border-radius:var(--border-radius);color:var(--text-primary);padding:20px;text-align:center;transition:all var(--transition-normal)}.glass-effect[data-v-cd44458d]:hover{background:#ffffff26;transform:translateY(-2px)}.neon-effect[data-v-cd44458d]{background:var(--bg-card);border:2px solid var(--accent-color);border-radius:var(--border-radius);color:var(--text-primary);overflow:hidden;padding:20px;position:relative;text-align:center;transition:all var(--transition-normal)}.neon-effect[data-v-cd44458d]:before{background:linear-gradient(90deg,transparent,var(--accent-color),transparent);content:"";height:100%;left:-100%;position:absolute;top:0;transition:left .6s ease;width:100%}.neon-effect[data-v-cd44458d]:hover:before{left:100%}.neon-effect[data-v-cd44458d]:hover{box-shadow:0 0 20px var(--accent-color);transform:translateY(-2px)}@media(max-width:768px){.theme-test-page[data-v-cd44458d]{padding:16px}.test-sections[data-v-cd44458d]{grid-template-columns:1fr}.component-test[data-v-cd44458d],.form-test[data-v-cd44458d],.theme-header[data-v-cd44458d]{flex-direction:column}.theme-header[data-v-cd44458d]{gap:12px;text-align:center}.effects-test[data-v-cd44458d]{grid-template-columns:1fr}}
2 |
--------------------------------------------------------------------------------
/data/config.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """
4 | 配置文件,一般来说不需要修改
5 | 如果需要启用或者禁用某些网站的爬取器,可在网页上进行配置
6 | """
7 |
8 | import os
9 | import secrets
10 |
11 | # 尝试加载 .env 文件
12 | def load_env_file():
13 | """加载 .env 文件中的环境变量"""
14 | env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
15 | if os.path.exists(env_file):
16 | try:
17 | with open(env_file, 'r', encoding='utf-8') as f:
18 | for line in f:
19 | line = line.strip()
20 | if line and not line.startswith('#') and '=' in line:
21 | key, value = line.split('=', 1)
22 | os.environ[key.strip()] = value.strip()
23 | except Exception as e:
24 | print(f"⚠️ 加载 .env 文件失败: {e}")
25 |
26 | # 加载 .env 文件
27 | load_env_file()
28 |
29 | # 数据目录路径(当前目录就是data目录)
30 | DATA_DIR = os.path.dirname(__file__)
31 |
32 | # 确保数据目录存在
33 | if not os.path.exists(DATA_DIR):
34 | os.makedirs(DATA_DIR)
35 |
36 | # 数据库文件路径
37 | DATABASE_PATH = os.path.join(DATA_DIR, 'data.db')
38 |
39 | # 其他数据文件路径
40 | USERS_FILE = os.path.join(DATA_DIR, 'users.json')
41 | API_STATUS_FILE = os.path.join(DATA_DIR, 'api_status.json')
42 | SUBSCRIPTIONS_FILE = os.path.join(DATA_DIR, 'sub.json')
43 |
44 | # 每次运行所有爬取器之后,睡眠多少时间,单位秒
45 | PROC_FETCHER_SLEEP = 5 * 60
46 |
47 | # 验证器每次睡眠的时间,单位秒
48 | PROC_VALIDATOR_SLEEP = 5
49 |
50 | # 验证器的配置参数
51 | VALIDATE_THREAD_NUM = 200 # 验证线程数量
52 | # 验证器的逻辑是:
53 | # 使用代理访问 VALIDATE_URL 网站,超时时间设置为 VALIDATE_TIMEOUT
54 | # 如果没有超时:
55 | # 1、若选择的验证方式为GET: 返回的网页中包含 VALIDATE_KEYWORD 文字,那么就认为本次验证成功
56 | # 2、若选择的验证方式为HEAD: 返回的响应头中,对于的 VALIDATE_HEADER 响应字段内容包含 VALIDATE_KEYWORD 内容,那么就认为本次验证成功
57 | # 上述过程最多进行 VALIDATE_MAX_FAILS 次,只要有一次成功,就认为代理可用
58 | VALIDATE_URL = 'https://qq.com'
59 | VALIDATE_METHOD = 'HEAD' # 验证方式,可选:GET、HEAD
60 | VALIDATE_HEADER = 'location' # 仅用于HEAD验证方式,百度响应头Server字段KEYWORD可填:bfe
61 | VALIDATE_KEYWORD = 'www.qq.com'
62 | VALIDATE_TIMEOUT = 5 # 超时时间,单位s
63 | VALIDATE_MAX_FAILS = 3
64 |
65 | # ============= 认证配置 =============
66 |
67 | # JWT密钥 - 从 .env 文件或环境变量读取
68 | JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
69 |
70 | # 安全验证:检查JWT密钥配置
71 | if not JWT_SECRET_KEY:
72 | print("=" * 60)
73 | print("🔴 安全警告:未找到 JWT_SECRET_KEY 配置!")
74 | print("=" * 60)
75 | print("请运行以下命令进行安全配置:")
76 | print("python setup_security.py")
77 | print("")
78 | print("或者手动创建 .env 文件并添加:")
79 | print("JWT_SECRET_KEY=your-strong-secret-key")
80 | print("=" * 60)
81 | raise ValueError("必须配置 JWT_SECRET_KEY!请运行 python setup_security.py")
82 |
83 | # 验证密钥强度
84 | if len(JWT_SECRET_KEY) < 32:
85 | print("=" * 60)
86 | print("🔴 安全警告:JWT_SECRET_KEY 长度不足!")
87 | print("=" * 60)
88 | print(f"当前长度: {len(JWT_SECRET_KEY)} 字符")
89 | print("建议长度: 至少 32 字符")
90 | print("")
91 | print("生成强密钥命令:")
92 | print("python -c \"import secrets; print(secrets.token_urlsafe(32))\"")
93 | print("=" * 60)
94 | raise ValueError("JWT_SECRET_KEY 长度必须至少32位")
95 |
96 | # 检查是否使用默认密钥(安全警告)
97 | if JWT_SECRET_KEY in ['your-secret-key-change-it-in-production-2025', 'admin123', 'password', 'secret']:
98 | print("=" * 60)
99 | print("🔴 安全警告:检测到弱密钥!")
100 | print("=" * 60)
101 | print("当前密钥过于简单,存在安全风险!")
102 | print("请立即更换为强密钥:")
103 | print("python -c \"import secrets; print(secrets.token_urlsafe(32))\"")
104 | print("=" * 60)
105 | raise ValueError("检测到弱密钥,请使用强密钥!")
106 |
107 | print("JWT密钥配置检查通过")
108 |
109 | # Token过期时间(小时)
110 | TOKEN_EXPIRATION_HOURS = 24
111 |
112 | # 默认管理员账户
113 | # 首次启动会自动创建,用户名:admin,密码:admin123
114 | # 登录后请立即修改密码
115 |
116 | # 初始化认证管理器
117 | from auth import AuthManager
118 | auth_manager = AuthManager(
119 | secret_key=JWT_SECRET_KEY,
120 | token_expiration_hours=TOKEN_EXPIRATION_HOURS
121 | )
122 |
123 | def generate_jwt_secret_key(length=32):
124 | """
125 | 生成安全的JWT密钥
126 | :param length: 密钥长度,默认32字符
127 | :return: 生成的密钥字符串
128 | """
129 | return secrets.token_urlsafe(length)
130 |
131 | def print_security_setup_guide():
132 | """
133 | 打印安全配置指南
134 | """
135 | print("\n" + "=" * 80)
136 | print("🔐 ProxyPool 安全配置指南")
137 | print("=" * 80)
138 | print("1. 生成强密钥:")
139 | print(f" python -c \"import secrets; print(secrets.token_urlsafe(32))\"")
140 | print("")
141 | print("2. 设置环境变量:")
142 | print(" Windows:")
143 | print(" set JWT_SECRET_KEY=your-generated-secret-key")
144 | print("")
145 | print(" Linux/Mac:")
146 | print(" export JWT_SECRET_KEY=your-generated-secret-key")
147 | print("")
148 | print("3. 永久设置(推荐):")
149 | print(" 在系统环境变量中设置 JWT_SECRET_KEY")
150 | print("")
151 | print("4. 验证配置:")
152 | print(" python -c \"from config import JWT_SECRET_KEY; print('密钥长度:', len(JWT_SECRET_KEY))\"")
153 | print("=" * 80)
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/CmFl7aLf.js:
--------------------------------------------------------------------------------
1 | import{g as B,Y as q,Z as O,r as j,B as R,k as T,$ as N,a0 as E,a1 as U,a2 as I,K as A,m as L,a3 as D,a4 as w,a5 as F,j as b,a6 as _,a7 as H,l as V,a8 as z,a9 as M,aa as W,ab as $}from"#entry";const G=(...t)=>t.find(o=>o!==void 0);function K(t){const o=t.componentName||"NuxtLink";function v(e){return typeof e=="string"&&e.startsWith("#")}function S(e,u,f){const r=f??t.trailingSlash;if(!e||r!=="append"&&r!=="remove")return e;if(typeof e=="string")return k(e,r);const l="path"in e&&e.path!==void 0?e.path:u(e).path;return{...e,name:void 0,path:k(l,r)}}function C(e){const u=q(),f=V(),r=b(()=>!!e.target&&e.target!=="_self"),l=b(()=>{const i=e.to||e.href||"";return typeof i=="string"&&_(i,{acceptRelative:!0})}),y=L("RouterLink"),h=y&&typeof y!="string"?y.useLink:void 0,c=b(()=>{if(e.external)return!0;const i=e.to||e.href||"";return typeof i=="object"?!1:i===""||l.value}),n=b(()=>{const i=e.to||e.href||"";return c.value?i:S(i,u.resolve,e.trailingSlash)}),g=c.value?void 0:h?.({...e,to:n}),m=b(()=>{const i=e.trailingSlash??t.trailingSlash;if(!n.value||l.value||v(n.value))return n.value;if(c.value){const p=typeof n.value=="object"&&"path"in n.value?w(n.value):n.value,x=typeof p=="object"?u.resolve(p).href:p;return k(x,i)}return typeof n.value=="object"?u.resolve(n.value)?.href??null:k(H(f.app.baseURL,n.value),i)});return{to:n,hasTarget:r,isAbsoluteUrl:l,isExternal:c,href:m,isActive:g?.isActive??b(()=>n.value===u.currentRoute.value.path),isExactActive:g?.isExactActive??b(()=>n.value===u.currentRoute.value.path),route:g?.route??b(()=>u.resolve(n.value)),async navigate(i){await z(m.value,{replace:e.replace,external:c.value||r.value})}}}return B({name:o,props:{to:{type:[String,Object],default:void 0,required:!1},href:{type:[String,Object],default:void 0,required:!1},target:{type:String,default:void 0,required:!1},rel:{type:String,default:void 0,required:!1},noRel:{type:Boolean,default:void 0,required:!1},prefetch:{type:Boolean,default:void 0,required:!1},prefetchOn:{type:[String,Object],default:void 0,required:!1},noPrefetch:{type:Boolean,default:void 0,required:!1},activeClass:{type:String,default:void 0,required:!1},exactActiveClass:{type:String,default:void 0,required:!1},prefetchedClass:{type:String,default:void 0,required:!1},replace:{type:Boolean,default:void 0,required:!1},ariaCurrentValue:{type:String,default:void 0,required:!1},external:{type:Boolean,default:void 0,required:!1},custom:{type:Boolean,default:void 0,required:!1},trailingSlash:{type:String,default:void 0,required:!1}},useLink:C,setup(e,{slots:u}){const f=q(),{to:r,href:l,navigate:y,isExternal:h,hasTarget:c,isAbsoluteUrl:n}=C(e),g=O(!1),m=j(null),i=s=>{m.value=e.custom?s?.$el?.nextElementSibling:s?.$el};function p(s){return!g.value&&(typeof e.prefetchOn=="string"?e.prefetchOn===s:e.prefetchOn?.[s]??t.prefetchOn?.[s])&&(e.prefetch??t.prefetch)!==!1&&e.noPrefetch!==!0&&e.target!=="_blank"&&!Z()}async function x(s=R()){if(g.value)return;g.value=!0;const d=typeof r.value=="string"?r.value:h.value?w(r.value):f.resolve(r.value).fullPath,a=h.value?new URL(d,window.location.href).href:d;await Promise.all([s.hooks.callHook("link:prefetch",a).catch(()=>{}),!h.value&&!c.value&&F(r.value,f).catch(()=>{})])}if(p("visibility")){const s=R();let d,a=null;T(()=>{const P=Q();N(()=>{d=E(()=>{m?.value?.tagName&&(a=P.observe(m.value,async()=>{a?.(),a=null,await x(s)}))})})}),U(()=>{d&&I(d),a?.(),a=null})}return()=>{if(!h.value&&!c.value&&!v(r.value)){const a={ref:i,to:r.value,activeClass:e.activeClass||t.activeClass,exactActiveClass:e.exactActiveClass||t.exactActiveClass,replace:e.replace,ariaCurrentValue:e.ariaCurrentValue,custom:e.custom};return e.custom||(p("interaction")&&(a.onPointerenter=x.bind(null,void 0),a.onFocus=x.bind(null,void 0)),g.value&&(a.class=e.prefetchedClass||t.prefetchedClass),a.rel=e.rel||void 0),A(L("RouterLink"),a,u.default)}const s=e.target||null,d=G(e.noRel?"":e.rel,t.externalRelAttribute,n.value||c.value?"noopener noreferrer":"")||null;return e.custom?u.default?u.default({href:l.value,navigate:y,prefetch:x,get route(){if(!l.value)return;const a=new URL(l.value,window.location.href);return{path:a.pathname,fullPath:a.pathname,get query(){return D(a.search)},hash:a.hash,params:{},name:void 0,matched:[],redirectedFrom:void 0,meta:{},href:l.value}},rel:d,target:s,isExternal:h.value||c.value,isActive:!1,isExactActive:!1}):null:A("a",{ref:m,href:l.value||null,rel:d,target:s,onClick:a=>{if(!(h.value||c.value))return a.preventDefault(),e.replace?f.replace(l.value):f.push(l.value)}},u.default?.())}}})}const X=K($);function k(t,o){const v=o==="append"?M:W;return _(t)&&!t.startsWith("http")?t:v(t,!0)}function Q(){const t=R();if(t._observer)return t._observer;let o=null;const v=new Map,S=(e,u)=>(o||=new IntersectionObserver(f=>{for(const r of f){const l=v.get(r.target);(r.isIntersecting||r.intersectionRatio>0)&&l&&l()}}),v.set(e,u),o.observe(e),()=>{v.delete(e),o?.unobserve(e),v.size===0&&(o?.disconnect(),o=null)});return t._observer={observe:S}}const Y=/2g/;function Z(){const t=navigator.connection;return!!(t&&(t.saveData||Y.test(t.effectiveType)))}export{X as _};
2 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import sys, os, signal
4 | sys.path.append(os.path.dirname(__file__) + os.sep + '../')
5 | from multiprocessing import Process
6 | import time
7 | from proc import run_fetcher, run_validator
8 | from api import api
9 | import multiprocessing
10 |
11 | # 导入单实例管理器
12 | sys.path.append(os.path.join(os.path.dirname(__file__), 'utils'))
13 | from single_instance import check_single_instance
14 |
15 | # 进程锁
16 | proc_lock = multiprocessing.Lock()
17 |
18 | # 单实例管理器
19 | instance_manager = None
20 |
21 | class Item:
22 | def __init__(self, target, name):
23 | self.target = target
24 | self.name = name
25 | self.process = None
26 | self.start_time = 0
27 |
28 | def main():
29 | global instance_manager
30 |
31 | # 启动安全检查
32 | print("🔐 启动 ProxyPool 管理系统...")
33 | print("=" * 60)
34 |
35 | # 检查JWT密钥配置
36 | try:
37 | from data.config import JWT_SECRET_KEY, print_security_setup_guide
38 | print("✅ JWT密钥配置检查通过")
39 | print(f"✅ 密钥长度: {len(JWT_SECRET_KEY)} 字符")
40 | except ValueError as e:
41 | print(f"❌ 安全配置错误: {e}")
42 | print_security_setup_guide()
43 | sys.exit(1)
44 | except Exception as e:
45 | print(f"❌ 配置加载失败: {e}")
46 | print_security_setup_guide()
47 | sys.exit(1)
48 |
49 | print("=" * 60)
50 |
51 | # 检查单实例运行
52 | print("🔒 检查单实例运行...")
53 |
54 | # 创建单实例管理器
55 | from utils.single_instance import SingleInstanceManager
56 | instance_manager = SingleInstanceManager("ProxyPoolWithUI", 5000)
57 |
58 | # 尝试获取锁,如果失败会自动清理
59 | if not instance_manager.acquire_lock():
60 | print("启动失败:无法获取单实例锁")
61 | sys.exit(1)
62 |
63 | print("单实例检查通过,开始启动服务...")
64 |
65 | processes = []
66 | processes.append(Item(target=run_fetcher.main, name='fetcher'))
67 | processes.append(Item(target=run_validator.main, name='validator'))
68 | processes.append(Item(target=api.main, name='api'))
69 |
70 | try:
71 | while True:
72 | for p in processes:
73 | if p.process is None:
74 | p.process = Process(target=p.target, name=p.name, daemon=False, args=(proc_lock, ))
75 | p.process.start()
76 | print(f'启动{p.name}进程,pid={p.process.pid}')
77 | p.start_time = time.time()
78 |
79 | for p in processes:
80 | if p.process is not None:
81 | if not p.process.is_alive():
82 | print(f'进程{p.name}异常退出, exitcode={p.process.exitcode}')
83 | p.process.terminate()
84 | p.process = None
85 | # 解除进程锁
86 | try:
87 | proc_lock.release()
88 | except ValueError:
89 | pass
90 | elif p.start_time + 60 * 60 < time.time(): # 最长运行1小时就重启
91 | print(f'进程{p.name}运行太久,重启')
92 | p.process.terminate()
93 | p.process = None
94 | # 解除进程锁
95 | try:
96 | proc_lock.release()
97 | except ValueError:
98 | pass
99 |
100 | time.sleep(0.2)
101 |
102 | except KeyboardInterrupt:
103 | print("\n收到中断信号,正在停止服务...")
104 | except Exception as e:
105 | print(f"系统异常: {e}")
106 | finally:
107 | # 清理资源
108 | print("正在清理资源...")
109 | for p in processes:
110 | if p.process is not None and p.process.is_alive():
111 | print(f"停止 {p.name} 进程...")
112 | p.process.terminate()
113 | p.process.join(timeout=5)
114 | if p.process.is_alive():
115 | print(f"强制终止 {p.name} 进程...")
116 | p.process.kill()
117 |
118 | # 释放单实例锁
119 | if instance_manager:
120 | instance_manager.release_lock()
121 |
122 | print("系统已安全退出")
123 |
124 | def citest():
125 | """
126 | 此函数仅用于检查程序是否可运行,一般情况下使用本项目可忽略
127 | """
128 | global instance_manager
129 |
130 | # CI测试时跳过单实例检查,但使用不同的端口
131 | print("运行CI测试模式...")
132 |
133 | processes = []
134 | processes.append(Item(target=run_fetcher.main, name='fetcher'))
135 | processes.append(Item(target=run_validator.main, name='validator'))
136 | processes.append(Item(target=api.main, name='api'))
137 |
138 | for p in processes:
139 | assert p.process is None
140 | p.process = Process(target=p.target, name=p.name, daemon=False)
141 | p.process.start()
142 | print(f'running {p.name}, pid={p.process.pid}')
143 | p.start_time = time.time()
144 |
145 | time.sleep(10)
146 |
147 | for p in processes:
148 | assert p.process is not None
149 | assert p.process.is_alive()
150 | p.process.terminate()
151 |
152 | if __name__ == '__main__':
153 | try:
154 | if len(sys.argv) >= 2 and sys.argv[1] == 'citest':
155 | citest()
156 | else:
157 | main()
158 | sys.exit(0)
159 | except Exception as e:
160 | print('========FATAL ERROR=========')
161 | print(e)
162 | sys.exit(1)
163 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/subscriptions.vmYHVIar.css:
--------------------------------------------------------------------------------
1 | .subscriptions-page[data-v-fc9f453e]{background:#f5f5f5;min-height:100vh;padding:24px;transition:all var(--transition-normal)}.dark .subscriptions-page[data-v-fc9f453e]{background:var(--bg-primary)}.main-card[data-v-fc9f453e]{margin-bottom:16px;transition:all var(--transition-normal)}.dark .main-card[data-v-fc9f453e],.main-card[data-v-fc9f453e]{background:var(--bg-card);border:1px solid var(--border-color)}.stats-row[data-v-fc9f453e]{margin-bottom:24px}.filter-section[data-v-fc9f453e]{background:#fafafa;border-radius:6px;margin-bottom:16px;padding:16px;transition:all var(--transition-normal)}.dark .filter-section[data-v-fc9f453e]{background:linear-gradient(135deg,#2d2d2d,#3a3a3a);border:1px solid var(--border-color)}.subscription-table[data-v-fc9f453e]{margin-top:16px}.dark .subscription-table[data-v-fc9f453e] .ant-table{background:var(--bg-card)!important;color:var(--text-primary)!important}.dark .subscription-table[data-v-fc9f453e] .ant-table-thead>tr>th{background:#2d2d2d!important;border-bottom:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .subscription-table[data-v-fc9f453e] .ant-table-tbody>tr{background:var(--bg-card)!important}.dark .subscription-table[data-v-fc9f453e] .ant-table-tbody>tr:hover{background:#2d2d2d!important}.dark .subscription-table[data-v-fc9f453e] .ant-table-tbody>tr>td{background:var(--bg-card)!important;border-bottom:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .filter-section[data-v-fc9f453e] .ant-select-selector{background:var(--bg-card)!important;border:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .filter-section[data-v-fc9f453e] .ant-select-selection-item{color:var(--text-primary)!important}.dark .filter-section[data-v-fc9f453e] .ant-select-selection-placeholder{color:var(--text-secondary)!important}.dark .filter-section[data-v-fc9f453e] .ant-input{background:var(--bg-card)!important;border:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .filter-section[data-v-fc9f453e] .ant-input::-moz-placeholder{color:var(--text-secondary)!important}.dark .filter-section[data-v-fc9f453e] .ant-input::placeholder{color:var(--text-secondary)!important}.dark .filter-section[data-v-fc9f453e] .ant-btn{background:var(--bg-card)!important;border:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .filter-section[data-v-fc9f453e] .ant-btn:hover{background:var(--bg-secondary)!important;border:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .filter-section[data-v-fc9f453e] .ant-btn-primary{background:#1890ff!important;border:1px solid #1890ff!important;color:#fff!important}.dark .filter-section[data-v-fc9f453e] .ant-btn-primary:hover{background:#40a9ff!important;border:1px solid #40a9ff!important;color:#fff!important}.dark .stats-row[data-v-fc9f453e] .ant-statistic-title{color:var(--text-secondary)!important}.dark .stats-row[data-v-fc9f453e] .ant-statistic-content,.dark .stats-row[data-v-fc9f453e] .ant-statistic-content-value,.dark .main-card[data-v-fc9f453e] .ant-card-head-title,.dark .main-card[data-v-fc9f453e] .ant-card-extra{color:var(--text-primary)!important}.dark .main-card[data-v-fc9f453e] .ant-btn{background:var(--bg-card)!important;border:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .main-card[data-v-fc9f453e] .ant-btn:hover{background:var(--bg-secondary)!important;border:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .main-card[data-v-fc9f453e] .ant-btn-primary{background:#1890ff!important;border:1px solid #1890ff!important;color:#fff!important}.dark .main-card[data-v-fc9f453e] .ant-btn-primary:hover{background:#40a9ff!important;border:1px solid #40a9ff!important;color:#fff!important}.dark .subscription-table[data-v-fc9f453e] .ant-btn{background:var(--bg-card)!important;border:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .subscription-table[data-v-fc9f453e] .ant-btn:hover{background:var(--bg-secondary)!important;border:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .subscription-table[data-v-fc9f453e] .ant-btn-primary{background:#1890ff!important;border:1px solid #1890ff!important;color:#fff!important}.dark .subscription-table[data-v-fc9f453e] .ant-btn-primary:hover{background:#40a9ff!important;border:1px solid #40a9ff!important;color:#fff!important}.dark .url-cell[data-v-fc9f453e],.dark .url-text[data-v-fc9f453e]{color:var(--text-primary)!important}.dark .subscription-table[data-v-fc9f453e] .ant-tag{background:var(--bg-secondary)!important;border:1px solid var(--border-color)!important;color:var(--text-primary)!important}.dark .subscription-table[data-v-fc9f453e] .ant-tag-blue{background:#1890ff!important;border:1px solid #1890ff!important;color:#fff!important}.dark .subscription-table[data-v-fc9f453e] .ant-tag-green{background:#52c41a!important;border:1px solid #52c41a!important;color:#fff!important}.dark .subscription-table[data-v-fc9f453e] .ant-tag-red{background:#ff4d4f!important;border:1px solid #ff4d4f!important;color:#fff!important}.url-cell[data-v-fc9f453e]{align-items:center;display:flex;gap:8px}.url-input[data-v-fc9f453e]{flex:1}.text-muted[data-v-fc9f453e]{color:#999}.text-success[data-v-fc9f453e]{color:#52c41a}.blink[data-v-fc9f453e]{animation:blink-fc9f453e 1s infinite}@keyframes blink-fc9f453e{0%,50%{opacity:1}51%,to{opacity:.5}}.empty-state[data-v-fc9f453e]{margin:40px 0}.batch-actions-card[data-v-fc9f453e]{bottom:24px;box-shadow:0 4px 12px #00000026;left:50%;position:fixed;transform:translate(-50%);z-index:1000}.batch-actions[data-v-fc9f453e]{align-items:center;display:flex;gap:16px;justify-content:space-between}.batch-actions span[data-v-fc9f453e]{color:#1890ff;font-weight:500}
2 |
--------------------------------------------------------------------------------
/db/Proxy.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import datetime
4 | import random
5 | class Proxy(object):
6 | """
7 | 代理,用于表示数据库中的一个记录
8 | """
9 |
10 | ddls = ["""
11 | CREATE TABLE IF NOT EXISTS proxies
12 | (
13 | fetcher_name VARCHAR(255) NOT NULL,
14 | protocol VARCHAR(32) NOT NULL,
15 | ip VARCHAR(255) NOT NULL,
16 | port INTEGER NOT NULL,
17 | validated BOOLEAN NOT NULL,
18 | latency INTEGER,
19 | validate_date TIMESTAMP,
20 | to_validate_date TIMESTAMP NOT NULL,
21 | validate_failed_cnt INTEGER NOT NULL,
22 | created_date TIMESTAMP NOT NULL,
23 | country VARCHAR(100),
24 | address VARCHAR(255),
25 | username VARCHAR(100),
26 | password VARCHAR(100),
27 | PRIMARY KEY (protocol, ip, port)
28 | )
29 | """,
30 | """
31 | CREATE INDEX IF NOT EXISTS proxies_fetcher_name_index
32 | ON proxies(fetcher_name)
33 | """,
34 | """
35 | CREATE INDEX IF NOT EXISTS proxies_to_validate_date_index
36 | ON proxies(to_validate_date ASC)
37 | """]
38 |
39 | def __init__(self):
40 | self.fetcher_name = None
41 | self.protocol = None
42 | self.ip = None
43 | self.port = None
44 | self.validated = False
45 | self.latency = None
46 | self.validate_date = None
47 | self.to_validate_date = datetime.datetime.now()
48 | self.validate_failed_cnt = 0
49 | self.created_date = datetime.datetime.now()
50 | self.country = None
51 | self.address = None
52 | self.username = None # 用户名,None表示无认证
53 | self.password = None # 密码,None表示无认证
54 |
55 | def params(self):
56 | """
57 | 返回一个元组,包含自身的全部属性
58 | """
59 | return (
60 | self.fetcher_name,
61 | self.protocol, self.ip, self.port,
62 | self.validated, self.latency,
63 | self.validate_date, self.to_validate_date, self.validate_failed_cnt,
64 | self.created_date,
65 | self.country, self.address, self.username, self.password
66 | )
67 |
68 | def to_dict(self):
69 | """
70 | 返回一个dict,包含自身的全部属性
71 | """
72 | # 计算存活时间(秒)
73 | alive_time = None
74 | if self.created_date:
75 | alive_time = int((datetime.datetime.now() - self.created_date).total_seconds())
76 |
77 | return {
78 | 'fetcher_name': self.fetcher_name,
79 | 'protocol': self.protocol,
80 | 'ip': self.ip,
81 | 'port': self.port,
82 | 'validated': self.validated,
83 | 'latency': self.latency,
84 | 'validate_date': str(self.validate_date) if self.validate_date is not None else None,
85 | 'to_validate_date': str(self.to_validate_date) if self.to_validate_date is not None else None,
86 | 'validate_failed_cnt': self.validate_failed_cnt,
87 | 'created_date': str(self.created_date) if self.created_date is not None else None,
88 | 'alive_time': alive_time,
89 | 'country': self.country,
90 | 'address': self.address,
91 | 'username': self.username,
92 | 'password': self.password
93 | }
94 |
95 | @staticmethod
96 | def decode(row):
97 | """
98 | 将sqlite返回的一行解析为Proxy
99 | row : sqlite返回的一行
100 | """
101 | # 兼容旧数据(9, 10个字段)和新数据(14个字段)
102 | assert len(row) in [9, 10, 14]
103 | p = Proxy()
104 | p.fetcher_name = row[0]
105 | p.protocol = row[1]
106 | p.ip = row[2]
107 | p.port = row[3]
108 | p.validated = bool(row[4])
109 | p.latency = row[5]
110 | p.validate_date = row[6]
111 | p.to_validate_date = row[7]
112 | p.validate_failed_cnt = row[8]
113 |
114 | # 处理 created_date (第10个字段)
115 | if len(row) >= 10:
116 | p.created_date = row[9] if row[9] else datetime.datetime.now()
117 | else:
118 | p.created_date = datetime.datetime.now()
119 |
120 | # 处理新增字段 (第11-14个字段)
121 | if len(row) >= 14:
122 | p.country = row[10]
123 | p.address = row[11]
124 | p.username = row[12] # 可以为 None
125 | p.password = row[13] # 可以为 None
126 | else:
127 | p.country = None
128 | p.address = None
129 | p.username = None
130 | p.password = None
131 |
132 | return p
133 |
134 | def validate(self, success, latency):
135 | """
136 | 传入一次验证结果,根据验证结果调整自身属性,并返回是否删除这个代理
137 | success : True/False,表示本次验证是否成功
138 | 返回 : True/False,True表示这个代理太差了,应该从数据库中删除
139 | """
140 | self.latency = latency
141 | if success: # 验证成功
142 | self.validated = True
143 | self.validate_date = datetime.datetime.now()
144 | self.validate_failed_cnt = 0
145 | #self.to_validate_date = datetime.datetime.now() + datetime.timedelta(minutes=30) # 30分钟之后继续验证
146 | self.to_validate_date = datetime.datetime.now() + datetime.timedelta(minutes=random.randint(10, 60)) # 10·60分钟之后继续验证
147 | return False
148 | else:
149 | self.validated = False
150 | self.validate_date = datetime.datetime.now()
151 | self.validate_failed_cnt = self.validate_failed_cnt + 1
152 |
153 | # 验证失败的次数越多,距离下次验证的时间越长
154 | delay_minutes = self.validate_failed_cnt * 10
155 | self.to_validate_date = datetime.datetime.now() + datetime.timedelta(minutes=delay_minutes)
156 |
157 | if self.validate_failed_cnt >= 6:
158 | return True
159 | else:
160 | return False
161 |
--------------------------------------------------------------------------------
/fetchers/FETCHER_AUTH_EXAMPLE.md:
--------------------------------------------------------------------------------
1 | # 爬取器扩展信息支持说明
2 |
3 | ## 📝 概述
4 |
5 | 爬取器支持返回代理的扩展信息,包括:
6 | - **账号密码**:由爬取器从网站爬取(如果有)
7 | - **地理位置**:国家和地址信息(如果网站提供)
8 |
9 | 这些信息都是**可选的**,如果爬取器能获取到就直接返回,如果获取不到:
10 | - 账号密码保持为 `None`(表示无需认证)
11 | - 国家地址在验证成功后自动获取
12 |
13 | ## 🔧 返回格式
14 |
15 | 爬取器的 `fetch()` 方法支持三种格式:
16 |
17 | ### 1️⃣ 基本格式(只有代理信息)
18 | ```python
19 | ('http', '127.0.0.1', 8080)
20 | ```
21 |
22 | ### 2️⃣ 包含认证信息
23 | ```python
24 | ('socks5', '1.2.3.4', 1080, 'user123', 'pass456')
25 | ```
26 |
27 | ### 3️⃣ 完整信息(认证 + 地理位置)
28 | ```python
29 | ('socks5', '1.2.3.4', 1080, 'user123', 'pass456', '美国', '洛杉矶')
30 | ```
31 |
32 | ### 4️⃣ 只有地理位置,无认证
33 | ```python
34 | ('http', '5.6.7.8', 8080, None, None, '日本', '东京')
35 | ```
36 |
37 | ## 📚 示例代码
38 |
39 | ### 示例 1: 爬取无认证代理(现有所有爬取器)
40 |
41 | ```python
42 | # encoding: utf-8
43 |
44 | from .BaseFetcher import BaseFetcher
45 | from pyquery import PyQuery as pq
46 | import requests
47 |
48 | class ExampleFetcher(BaseFetcher):
49 | """
50 | 示例爬取器 - 无认证代理
51 | """
52 |
53 | def fetch(self):
54 | proxies = []
55 |
56 | url = 'https://example.com/free-proxy'
57 | response = requests.get(url, timeout=15)
58 | doc = pq(response.text)
59 |
60 | for item in doc('table tr').items():
61 | ip = item.find('td:nth-child(1)').text()
62 | port = int(item.find('td:nth-child(2)').text())
63 | protocol = item.find('td:nth-child(3)').text().lower()
64 |
65 | # 返回三元组: (protocol, ip, port)
66 | proxies.append((protocol, ip, port))
67 |
68 | return proxies
69 | ```
70 |
71 | ### 示例 2: 爬取有认证代理(新功能)
72 |
73 | ```python
74 | # encoding: utf-8
75 |
76 | from .BaseFetcher import BaseFetcher
77 | from pyquery import PyQuery as pq
78 | import requests
79 |
80 | class PaidProxyFetcher(BaseFetcher):
81 | """
82 | 示例爬取器 - 付费代理(带账号密码)
83 | """
84 |
85 | def fetch(self):
86 | proxies = []
87 |
88 | url = 'https://example.com/paid-proxy-list'
89 | response = requests.get(url, timeout=15)
90 | doc = pq(response.text)
91 |
92 | for item in doc('table tr').items():
93 | ip = item.find('td:nth-child(1)').text()
94 | port = int(item.find('td:nth-child(2)').text())
95 | protocol = item.find('td:nth-child(3)').text().lower()
96 | username = item.find('td:nth-child(4)').text()
97 | password = item.find('td:nth-child(5)').text()
98 |
99 | # 如果爬到了账号密码,返回五元组
100 | if username and password:
101 | proxies.append((protocol, ip, port, username, password))
102 | else:
103 | # 如果没有账号密码,返回三元组
104 | proxies.append((protocol, ip, port))
105 |
106 | return proxies
107 | ```
108 |
109 | ### 示例 3: 包含地理位置信息
110 |
111 | ```python
112 | # encoding: utf-8
113 |
114 | from .BaseFetcher import BaseFetcher
115 | import requests
116 |
117 | class LocationProxyFetcher(BaseFetcher):
118 | """
119 | 示例爬取器 - 包含地理位置
120 | """
121 |
122 | def fetch(self):
123 | proxies = []
124 |
125 | url = 'https://example.com/proxy-with-location'
126 | response = requests.get(url, timeout=15)
127 | data = response.json()
128 |
129 | for item in data['proxies']:
130 | protocol = item['protocol']
131 | ip = item['ip']
132 | port = item['port']
133 | country = item.get('country') # 可能为 None
134 | address = item.get('address') # 可能为 None
135 | username = item.get('username') # 可能为 None
136 | password = item.get('password') # 可能为 None
137 |
138 | # 返回完整信息(7元组)
139 | if username or password or country or address:
140 | proxies.append((protocol, ip, port, username, password, country, address))
141 | # 或者返回基本信息(3元组)
142 | else:
143 | proxies.append((protocol, ip, port))
144 |
145 | return proxies
146 | ```
147 |
148 | ### 示例 4: 混合返回(不同格式)
149 |
150 | ```python
151 | # encoding: utf-8
152 |
153 | from .BaseFetcher import BaseFetcher
154 | import requests
155 |
156 | class MixedFormatFetcher(BaseFetcher):
157 | """
158 | 示例爬取器 - 混合格式返回
159 | """
160 |
161 | def fetch(self):
162 | proxies = []
163 |
164 | # 场景1: 免费代理,无任何额外信息
165 | proxies.append(('http', '1.2.3.4', 8080))
166 |
167 | # 场景2: 付费代理,有认证信息
168 | proxies.append(('socks5', '5.6.7.8', 1080, 'user1', 'pass1'))
169 |
170 | # 场景3: 完整信息(认证 + 地理位置)
171 | proxies.append(('socks5', '9.10.11.12', 1080, 'user2', 'pass2', '美国', '纽约'))
172 |
173 | # 场景4: 只有地理位置,无认证
174 | proxies.append(('http', '13.14.15.16', 8080, None, None, '日本', '东京'))
175 |
176 | return proxies
177 | ```
178 |
179 | ## ✅ 数据流程
180 |
181 | ### 入库阶段(爬取器 → 数据库)
182 | 1. **爬取器** 从网站爬取代理信息
183 | - 如果网站提供了 username/password/country/address,直接返回
184 | - 如果没有,返回 `None` 或省略该字段
185 | 2. **run_fetcher.py** 自动识别格式(3/5/7元组)
186 | 3. **conn.pushNewFetch()** 将数据写入数据库
187 | - 爬取器提供的信息直接写入
188 | - 未提供的信息保持为 `NULL`
189 |
190 | ### 验证阶段(验证器 → 更新地理位置)
191 | 4. **验证器** 验证代理是否可用
192 | 5. **conn.pushValidateResult()**
193 | - 如果验证成功 **且** 没有地理位置信息
194 | - 自动调用 IP 地理位置 API 获取 country 和 address
195 | - 更新到数据库
196 |
197 | ### 展示阶段(前端)
198 | 6. **前端** 显示代理信息
199 | - 有值显示实际值
200 | - `NULL` 显示"未知"
201 |
202 | ## ⚠️ 注意事项
203 |
204 | ### ✅ 推荐做法
205 | 1. **爬取器能获取到的信息,直接返回**
206 | - 减少后续 API 查询次数
207 | - 提高系统效率
208 | 2. **如果网站提供地理位置信息,建议返回**
209 | - 格式:`(protocol, ip, port, username, password, country, address)`
210 | - username/password 可以为 `None`
211 | 3. **账号密码从网站爬取**
212 | - 不要硬编码默认值
213 |
214 | ### ❌ 不推荐做法
215 | 1. 不要在爬取器中调用 IP 地理位置 API
216 | - 会导致爬取速度变慢
217 | - 系统会在验证成功后自动获取
218 | 2. 不要设置默认值(如 'test1', 'unknown')
219 | - 返回 `None` 即可
220 |
221 | ## 🎯 格式选择指南
222 |
223 | | 网站提供的信息 | 推荐返回格式 | 示例 |
224 | |---|---|---|
225 | | 只有IP和端口 | 3元组 | `('http', '1.2.3.4', 8080)` |
226 | | IP + 认证信息 | 5元组 | `('socks5', '1.2.3.4', 1080, 'user', 'pass')` |
227 | | IP + 地理位置 | 7元组 | `('http', '1.2.3.4', 8080, None, None, '美国', '纽约')` |
228 | | 完整信息 | 7元组 | `('socks5', '1.2.3.4', 1080, 'user', 'pass', '日本', '东京')` |
229 |
230 | ## 🔄 兼容性
231 |
232 | - **现有爬取器**:无需修改,继续返回 3元组即可
233 | - **新爬取器**:根据网站提供的信息,选择合适的格式
234 |
235 |
--------------------------------------------------------------------------------
/frontend/src/README_NUXT3.md:
--------------------------------------------------------------------------------
1 | # Nuxt 3 前端使用指南
2 |
3 | ## 🎉 已升级到 Nuxt 3
4 |
5 | 本项目前端已成功从 Nuxt 2 升级到 Nuxt 3,使用最新的技术栈。
6 |
7 | ## 📋 技术栈
8 |
9 | - **Nuxt 3.19.3** - 现代化的 Vue 框架
10 | - **Vue 3.5.22** - 最新的 Vue.js
11 | - **TypeScript** - 类型安全
12 | - **Ant Design Vue 4.0** - UI 组件库
13 | - **Vite 7.1** - 极速构建工具
14 | - **Composition API** - Vue 3 推荐的 API 风格
15 |
16 | ## 🚀 快速开始
17 |
18 | ### 生产模式(推荐)
19 |
20 | 直接运行已构建好的静态文件:
21 |
22 | ```bash
23 | # 在项目根目录
24 | python main.py
25 | ```
26 |
27 | 访问:http://localhost:5000/web
28 |
29 | ### 开发模式
30 |
31 | 用于开发和调试:
32 |
33 | **终端 1 - 后端:**
34 | ```bash
35 | python main.py
36 | ```
37 |
38 | **终端 2 - 前端热重载:**
39 | ```bash
40 | cd frontend\src
41 | npm run dev
42 | ```
43 |
44 | 访问:http://localhost:3000
45 |
46 | ## 📦 常用命令
47 |
48 | ```bash
49 | # 安装依赖
50 | npm install
51 |
52 | # 开发模式(热重载)
53 | npm run dev
54 |
55 | # 构建生产版本
56 | npm run generate
57 |
58 | # 预览构建结果
59 | npm run preview
60 |
61 | # 类型检查
62 | npx nuxi typecheck
63 | ```
64 |
65 | ## 📁 项目结构
66 |
67 | ```
68 | frontend/src/
69 | ├── app.vue # 应用入口
70 | ├── nuxt.config.ts # Nuxt 配置
71 | ├── package.json # 依赖管理
72 | ├── pages/ # 页面
73 | │ ├── index.vue # 首页(代理列表)
74 | │ └── fetchers.vue # 爬取器状态
75 | ├── layouts/ # 布局
76 | │ └── default.vue # 默认布局(侧边栏)
77 | ├── plugins/ # 插件
78 | │ ├── axios.ts # HTTP 客户端
79 | │ └── antd.ts # Ant Design Vue
80 | └── types/ # TypeScript 类型
81 | ├── nuxt.d.ts # Nuxt 自动导入类型
82 | └── app.d.ts # 应用类型定义
83 | ```
84 |
85 | ## 🔧 开发指南
86 |
87 | ### 添加新页面
88 |
89 | 在 `pages/` 目录创建 `.vue` 文件:
90 |
91 | ```vue
92 |
93 |
94 |
新页面
95 |
96 |
97 |
98 |
102 | ```
103 |
104 | Nuxt 会自动创建路由:
105 | - `pages/example.vue` → `/web/example`
106 |
107 | ### 使用 HTTP 客户端
108 |
109 | ```vue
110 |
124 | ```
125 |
126 | ### 添加新组件
127 |
128 | 在 `components/` 目录创建组件:
129 |
130 | ```vue
131 |
132 |
133 | {{ message }}
134 |
135 |
136 |
141 | ```
142 |
143 | 使用时无需导入(自动导入):
144 |
145 | ```vue
146 |
147 |
148 |
149 | ```
150 |
151 | ### TypeScript 类型
152 |
153 | 定义接口:
154 |
155 | ```typescript
156 | // types/models.ts
157 | export interface Proxy {
158 | protocol: string
159 | ip: string
160 | port: number
161 | latency: number
162 | alive_time: number
163 | created_date: string
164 | validated: boolean
165 | }
166 | ```
167 |
168 | 使用:
169 |
170 | ```vue
171 |
176 | ```
177 |
178 | ## 🎨 样式
179 |
180 | ### 全局样式
181 |
182 | 在 `nuxt.config.ts` 中配置:
183 |
184 | ```typescript
185 | export default defineNuxtConfig({
186 | css: [
187 | 'ant-design-vue/dist/reset.css',
188 | '~/assets/css/global.css'
189 | ]
190 | })
191 | ```
192 |
193 | ### 组件样式
194 |
195 | ```vue
196 |
202 |
203 |
206 | ```
207 |
208 | ## 🔄 重新构建
209 |
210 | 修改代码后需要重新构建:
211 |
212 | ```bash
213 | cd frontend\src
214 | npm run generate
215 | ```
216 |
217 | 构建输出位置:`frontend/deployment/public/`
218 |
219 | ## ⚙️ 配置
220 |
221 | ### API 基础 URL
222 |
223 | 在 `nuxt.config.ts` 中配置:
224 |
225 | ```typescript
226 | export default defineNuxtConfig({
227 | runtimeConfig: {
228 | public: {
229 | apiBase: process.env.NODE_ENV === 'production'
230 | ? '/'
231 | : 'http://localhost:5000'
232 | }
233 | }
234 | })
235 | ```
236 |
237 | ### 环境变量
238 |
239 | 创建 `.env` 文件:
240 |
241 | ```bash
242 | # .env
243 | NUXT_PUBLIC_API_BASE=http://localhost:5000
244 | ```
245 |
246 | ## 🐛 常见问题
247 |
248 | ### TypeScript 错误
249 |
250 | **问题**:`Cannot find name 'useNuxtApp'`
251 |
252 | **解决**:
253 | 1. 确保 `types/nuxt.d.ts` 存在
254 | 2. 重启 TypeScript 服务器(VS Code: Ctrl+Shift+P → "TypeScript: Restart TS Server")
255 | 3. 运行 `npm run postinstall` 生成类型
256 |
257 | ### 构建错误
258 |
259 | **问题**:构建失败
260 |
261 | **解决**:
262 | 1. 删除 `node_modules` 和 `package-lock.json`
263 | 2. 重新安装:`npm install`
264 | 3. 清除缓存:`rm -rf .nuxt`
265 |
266 | ### 页面空白
267 |
268 | **问题**:访问页面显示空白
269 |
270 | **解决**:
271 | 1. 清除浏览器缓存(Ctrl+F5)
272 | 2. 检查浏览器控制台错误
273 | 3. 确认后端正在运行
274 | 4. 检查 API 路径配置
275 |
276 | ## 📚 学习资源
277 |
278 | - [Nuxt 3 文档](https://nuxt.com/)
279 | - [Vue 3 文档](https://vuejs.org/)
280 | - [Composition API](https://vuejs.org/guide/extras/composition-api-faq.html)
281 | - [TypeScript 文档](https://www.typescriptlang.org/)
282 | - [Ant Design Vue](https://antdv.com/)
283 |
284 | ## 🔐 最佳实践
285 |
286 | 1. **使用 TypeScript**:为所有新代码添加类型
287 | 2. **使用 Composition API**:比 Options API 更灵活
288 | 3. **使用自动导入**:Nuxt 3 会自动导入组件和 composables
289 | 4. **保持组件小而专注**:每个组件只做一件事
290 | 5. **使用 ESLint**:保持代码风格一致
291 |
292 | ## 🆕 新功能
293 |
294 | ### 存活时间列
295 |
296 | 首页代理列表新增"存活时间"列,显示代理在数据库中的时长:
297 |
298 | ```vue
299 |
300 |
301 |
302 |
303 |
304 |
305 |
317 | ```
318 |
319 | ## 🎯 性能优化
320 |
321 | 1. **代码分割**:使用动态导入
322 | ```typescript
323 | const MyComponent = defineAsyncComponent(() =>
324 | import('~/components/MyComponent.vue')
325 | )
326 | ```
327 |
328 | 2. **图片优化**:使用 Nuxt Image
329 | ```vue
330 |
331 | ```
332 |
333 | 3. **懒加载**:延迟加载非关键组件
334 | ```vue
335 |
336 | ```
337 |
338 | ## 📞 支持
339 |
340 | 遇到问题?
341 |
342 | 1. 查看 [Nuxt 3 文档](https://nuxt.com/)
343 | 2. 检查 `frontend/src_backup/` 中的原始代码
344 | 3. 查看浏览器开发者工具控制台
345 | 4. 检查终端错误信息
346 |
347 | ---
348 |
349 | **版本**: 2.0.0
350 | **更新时间**: 2025-10-18
351 | **状态**: ✅ 生产就绪
352 |
353 |
--------------------------------------------------------------------------------
/auth/auth_manager.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import jwt
4 | import datetime
5 | import hashlib
6 | import os
7 | import json
8 | from functools import wraps
9 | from flask import request, jsonify
10 | from data.config import USERS_FILE
11 |
12 | class AuthManager:
13 | """
14 | 认证管理器 - 处理用户认证、JWT生成和验证
15 | """
16 |
17 | def __init__(self, secret_key, token_expiration_hours=24):
18 | """
19 | 初始化认证管理器
20 | :param secret_key: JWT签名密钥
21 | :param token_expiration_hours: Token过期时间(小时)
22 | """
23 | self.secret_key = secret_key
24 | self.token_expiration_hours = token_expiration_hours
25 | self.users_file = USERS_FILE
26 | self._init_users_file()
27 |
28 | def _init_users_file(self):
29 | """初始化用户文件,如果不存在则创建默认管理员账户"""
30 | if not os.path.exists(self.users_file):
31 | default_users = {
32 | "admin": {
33 | "password": self._hash_password("admin123"),
34 | "role": "admin",
35 | "created_at": datetime.datetime.now().isoformat()
36 | }
37 | }
38 | with open(self.users_file, 'w', encoding='utf-8') as f:
39 | json.dump(default_users, f, indent=2, ensure_ascii=False)
40 | print(f"[Auth] 创建默认管理员账户: admin / admin123")
41 |
42 | def _hash_password(self, password):
43 | """对密码进行哈希"""
44 | return hashlib.sha256(password.encode()).hexdigest()
45 |
46 | def _load_users(self):
47 | """加载用户数据"""
48 | try:
49 | with open(self.users_file, 'r', encoding='utf-8') as f:
50 | return json.load(f)
51 | except Exception as e:
52 | print(f"[Auth] 加载用户数据失败: {e}")
53 | return {}
54 |
55 | def _save_users(self, users):
56 | """保存用户数据"""
57 | try:
58 | with open(self.users_file, 'w', encoding='utf-8') as f:
59 | json.dump(users, f, indent=2, ensure_ascii=False)
60 | return True
61 | except Exception as e:
62 | print(f"[Auth] 保存用户数据失败: {e}")
63 | return False
64 |
65 | def authenticate(self, username, password):
66 | """
67 | 验证用户名和密码
68 | :param username: 用户名
69 | :param password: 密码(明文)
70 | :return: 验证成功返回用户信息,失败返回None
71 | """
72 | users = self._load_users()
73 | user = users.get(username)
74 |
75 | if not user:
76 | return None
77 |
78 | if user['password'] == self._hash_password(password):
79 | return {
80 | 'username': username,
81 | 'role': user.get('role', 'user')
82 | }
83 |
84 | return None
85 |
86 | def generate_token(self, username, role='user'):
87 | """
88 | 生成JWT Token
89 | :param username: 用户名
90 | :param role: 用户角色
91 | :return: JWT Token字符串
92 | """
93 | payload = {
94 | 'username': username,
95 | 'role': role,
96 | 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=self.token_expiration_hours),
97 | 'iat': datetime.datetime.utcnow()
98 | }
99 |
100 | token = jwt.encode(payload, self.secret_key, algorithm='HS256')
101 | return token
102 |
103 | def verify_token(self, token):
104 | """
105 | 验证JWT Token
106 | :param token: JWT Token字符串
107 | :return: 验证成功返回payload,失败返回None
108 | """
109 | try:
110 | payload = jwt.decode(token, self.secret_key, algorithms=['HS256'])
111 | return payload
112 | except jwt.ExpiredSignatureError:
113 | print("[Auth] Token已过期")
114 | return None
115 | except jwt.InvalidTokenError as e:
116 | print(f"[Auth] Token无效: {e}")
117 | return None
118 |
119 | def create_user(self, username, password, role='user'):
120 | """
121 | 创建新用户
122 | :param username: 用户名
123 | :param password: 密码(明文)
124 | :param role: 用户角色
125 | :return: 成功返回True,失败返回False
126 | """
127 | users = self._load_users()
128 |
129 | if username in users:
130 | return False
131 |
132 | users[username] = {
133 | 'password': self._hash_password(password),
134 | 'role': role,
135 | 'created_at': datetime.datetime.now().isoformat()
136 | }
137 |
138 | return self._save_users(users)
139 |
140 | def change_password(self, username, old_password, new_password):
141 | """
142 | 修改用户密码
143 | :param username: 用户名
144 | :param old_password: 旧密码
145 | :param new_password: 新密码
146 | :return: 成功返回True,失败返回False
147 | """
148 | users = self._load_users()
149 | user = users.get(username)
150 |
151 | if not user:
152 | return False
153 |
154 | if user['password'] != self._hash_password(old_password):
155 | return False
156 |
157 | user['password'] = self._hash_password(new_password)
158 | user['password_changed_at'] = datetime.datetime.now().isoformat()
159 |
160 | return self._save_users(users)
161 |
162 | def delete_user(self, username):
163 | """
164 | 删除用户
165 | :param username: 用户名
166 | :return: 成功返回True,失败返回False
167 | """
168 | if username == 'admin':
169 | return False # 不允许删除管理员账户
170 |
171 | users = self._load_users()
172 |
173 | if username not in users:
174 | return False
175 |
176 | del users[username]
177 | return self._save_users(users)
178 |
179 | def list_users(self):
180 | """
181 | 列出所有用户(不包含密码)
182 | :return: 用户列表
183 | """
184 | users = self._load_users()
185 | result = []
186 |
187 | for username, info in users.items():
188 | result.append({
189 | 'username': username,
190 | 'role': info.get('role', 'user'),
191 | 'created_at': info.get('created_at', '')
192 | })
193 |
194 | return result
195 |
196 | def token_required(f):
197 | """
198 | 装饰器:要求请求必须带有有效的JWT Token
199 | """
200 | @wraps(f)
201 | def decorated_function(*args, **kwargs):
202 | # 从请求头获取token
203 | token = request.headers.get('Authorization')
204 |
205 | if not token:
206 | return jsonify({
207 | 'success': False,
208 | 'message': '缺少认证Token',
209 | 'error': 'MISSING_TOKEN'
210 | }), 401
211 |
212 | # 移除 "Bearer " 前缀
213 | if token.startswith('Bearer '):
214 | token = token[7:]
215 |
216 | # 验证token
217 | from data.config import auth_manager
218 | payload = auth_manager.verify_token(token)
219 |
220 | if not payload:
221 | return jsonify({
222 | 'success': False,
223 | 'message': 'Token无效或已过期',
224 | 'error': 'INVALID_TOKEN'
225 | }), 401
226 |
227 | # 将用户信息添加到请求上下文
228 | request.user = payload
229 |
230 | return f(*args, **kwargs)
231 |
232 | return decorated_function
233 |
234 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/1nLSuJ89.js:
--------------------------------------------------------------------------------
1 | import{h as w}from"./BLMuvzoS.js";import{b as l,I as C,g as G,B as Q,r as g,j as k,k as U,E as q,c as p,a as t,w as m,F as J,s as T,A as v,m as f,o as _,n as d,y as $,t as u,d as L,O as W,Q as X,J as Z,p as z,q as K,C as tt,_ as et}from"#entry";import{s as st}from"./BzLCLO6P.js";import{C as at}from"./DdFW8pdL.js";import{G as nt}from"./DzUz1rp7.js";import{D as lt}from"./CoiHVIeb.js";var ot={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM305.8 637.7c3.1 3.1 8.1 3.1 11.3 0l138.3-137.6L583 628.5c3.1 3.1 8.2 3.1 11.3 0l275.4-275.3c3.1-3.1 3.1-8.2 0-11.3l-39.6-39.6a8.03 8.03 0 00-11.3 0l-230 229.9L461.4 404a8.03 8.03 0 00-11.3 0L266.3 586.7a8.03 8.03 0 000 11.3l39.5 39.7z"}}]},name:"line-chart",theme:"outlined"};function D(n){for(var e=1;es.value.filter(i=>i.enable).length),B=k(()=>s.value.filter(i=>!i.enable).length),b=async()=>{try{const i=await e.get("/fetchers_status");s.value=i.fetchers.map(a=>({...a,loading:!1})),c.value=w().format("HH:mm:ss")}catch(i){console.error("更新数据失败:",i),v.error("更新数据失败")}},E=async()=>{O.value=!0;try{await e.get("/clear_fetchers_status"),v.success("清空成功"),await b()}catch{v.error("清空失败")}finally{O.value=!1}},H=async i=>{i.loading=!0;try{const a=i.enable?"0":"1";await e.get("/fetcher_enable",{name:i.name,enable:a}),v.success(`${i.name} ${i.enable?"已禁用":"已启用"}`),await b()}catch{v.error("修改失败"),i.loading=!1}};return U(()=>{h=st(()=>{o.value&&b()},2e3),b()}),q(()=>{h&&(clearInterval(h),h=null)}),(i,a)=>{const j=f("a-switch"),y=f("a-col"),I=f("a-button"),V=f("a-tooltip"),R=f("a-row"),Y=f("a-progress");return _(),p("div",pt,[l(R,{gutter:[16,16],class:"control-row"},{default:m(()=>[l(y,{xs:24,sm:12,md:8},{default:m(()=>[t("div",_t,[t("div",mt,[l(d(P),{spin:o.value},null,8,["spin"]),a[1]||(a[1]=t("span",null,"自动刷新",-1))]),l(j,{checked:o.value,"onUpdate:checked":a[0]||(a[0]=r=>o.value=r),size:"large"},null,8,["checked"]),t("div",ft,[l(d($)),t("span",null,u(c.value),1)])])]),_:1}),l(y,{xs:24,sm:12,md:8},{default:m(()=>[t("div",vt,[t("div",ht,[l(d(at)),a[2]||(a[2]=t("span",null,"数据管理",-1))]),l(I,{type:"primary",danger:"",onClick:E,loading:O.value,size:"large"},{default:m(()=>[l(d(W)),a[3]||(a[3]=L(" 清空统计信息 ",-1))]),_:1},8,["loading"]),l(V,{title:"清空'总共爬取代理数量'等,已经爬取到的代理不会删除"},{default:m(()=>[l(d(X),{class:"help-icon"})]),_:1})])]),_:1}),l(y,{xs:24,sm:24,md:8},{default:m(()=>[t("div",bt,[t("div",gt,[a[4]||(a[4]=t("span",{class:"stats-label"},"爬取器总数",-1)),t("span",Ot,u(s.value.length),1)]),t("div",yt,[a[5]||(a[5]=t("span",{class:"stats-label"},"已启用",-1)),t("span",wt,u(A.value),1)]),t("div",Ct,[a[6]||(a[6]=t("span",{class:"stats-label"},"已禁用",-1)),t("span",xt,u(B.value),1)])])]),_:1})]),_:1}),t("div",St,[(_(!0),p(J,null,T(s.value,(r,F)=>(_(),p("div",{key:r.name,class:"fetcher-card enhanced-card scale-in hover-lift",style:Z({animationDelay:`${F*.05}s`})},[t("div",{class:K(["fetcher-status",{active:r.enable}])},[a[7]||(a[7]=t("span",{class:"status-dot"},null,-1)),t("span",Pt,u(r.enable?"启用中":"已禁用"),1)],2),t("div",jt,[t("h3",kt,[l(d(nt)),L(" "+u(r.name),1)]),l(j,{checked:r.enable,onChange:qt=>H(r),loading:r.loading},null,8,["checked","onChange","loading"])]),t("div",$t,[t("div",Lt,[t("div",zt,[l(d(tt),{class:"stat-icon success"}),a[8]||(a[8]=t("span",{class:"stat-label"},"可用",-1)),t("span",Dt,u(r.validated_cnt||0),1)]),t("div",Nt,[l(d(lt),{class:"stat-icon info"}),a[9]||(a[9]=t("span",{class:"stat-label"},"库存",-1)),t("span",Mt,u(r.in_db_cnt||0),1)])]),t("div",At,[t("div",Bt,[l(d(x),{class:"stat-icon primary"}),a[10]||(a[10]=t("span",{class:"stat-label"},"总数",-1)),t("span",Et,u(r.sum_proxies_cnt||0),1)]),t("div",Ht,[l(d(S),{class:"stat-icon warning"}),a[11]||(a[11]=t("span",{class:"stat-label"},"本次",-1)),t("span",It,u(r.last_proxies_cnt||0),1)])])]),t("div",Vt,[t("div",Rt,[l(d($)),r.last_fetch_date?(_(),p("span",Yt,u(d(w)(r.last_fetch_date).fromNow()),1)):(_(),p("span",Ft,"暂无数据"))]),r.last_fetch_date?(_(),p("div",Gt,u(d(w)(r.last_fetch_date).format("YYYY-MM-DD HH:mm")),1)):z("",!0)]),r.sum_proxies_cnt>0?(_(),p("div",Qt,[l(Y,{percent:Math.min(100,r.validated_cnt/r.sum_proxies_cnt*100),"show-info":!1,"stroke-color":{"0%":"#108ee9","100%":"#87d068"},size:"small"},null,8,["percent"])])):z("",!0)],4))),128))])])}}}),te=et(Ut,[["__scopeId","data-v-388abd13"]]);export{te as default};
2 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/BR_WPFp-.js:
--------------------------------------------------------------------------------
1 | import{b as l,I as _,g as R,B as G,G as q,r as y,k as J,H as W,c as f,a as r,F as O,s as L,n as g,w as u,q as Q,m as d,o as m,J as X,K as M,d as h,L as Y,x as Z,M as K,t as ee,N as te,A as $,_ as ne}from"#entry";import{C as oe,U as re}from"./DvlYdpVS.js";import{L as ae}from"./UewoGXxE.js";import{T as se}from"./1nlWnoXy.js";import{G as le}from"./DzUz1rp7.js";var ie={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"defs",attrs:{},children:[{tag:"style",attrs:{}}]},{tag:"path",attrs:{d:"M521.7 82c-152.5-.4-286.7 78.5-363.4 197.7-3.4 5.3.4 12.3 6.7 12.3h70.3c4.8 0 9.3-2.1 12.3-5.8 7-8.5 14.5-16.7 22.4-24.5 32.6-32.5 70.5-58.1 112.7-75.9 43.6-18.4 90-27.8 137.9-27.8 47.9 0 94.3 9.3 137.9 27.8 42.2 17.8 80.1 43.4 112.7 75.9 32.6 32.5 58.1 70.4 76 112.5C865.7 417.8 875 464.1 875 512c0 47.9-9.4 94.2-27.8 137.8-17.8 42.1-43.4 80-76 112.5s-70.5 58.1-112.7 75.9A352.8 352.8 0 01520.6 866c-47.9 0-94.3-9.4-137.9-27.8A353.84 353.84 0 01270 762.3c-7.9-7.9-15.3-16.1-22.4-24.5-3-3.7-7.6-5.8-12.3-5.8H165c-6.3 0-10.2 7-6.7 12.3C234.9 863.2 368.5 942 520.6 942c236.2 0 428-190.1 430.4-425.6C953.4 277.1 761.3 82.6 521.7 82zM395.02 624v-76h-314c-4.4 0-8-3.6-8-8v-56c0-4.4 3.6-8 8-8h314v-76c0-6.7 7.8-10.5 13-6.3l141.9 112a8 8 0 010 12.6l-141.9 112c-5.2 4.1-13 .4-13-6.3z"}}]},name:"login",theme:"outlined"};function C(a){for(var t=1;t{const e=(b,p)=>Math.random()*(p-b)+b;return{left:`${e(0,100)}%`,top:`${e(0,100)}%`,width:`${e(2,8)}px`,height:`${e(2,8)}px`,animationDuration:`${e(20,40)}s`,animationDelay:`${e(0,10)}s`,opacity:e(.3,.8)}},P=()=>{S.value=!0},z=()=>{S.value=!1},F=()=>{te.info({title:"忘记密码",content:"请联系系统管理员重置密码,或使用默认账户登录。",okText:"知道了"})},A=async()=>{s.value=!0,v.value=!1;try{const c=await t.post("/auth/login",{username:o.username,password:o.password});c.success&&(localStorage.setItem("token",c.token),localStorage.setItem("user",JSON.stringify(c.user)),o.remember?localStorage.setItem("remember","true"):localStorage.removeItem("remember"),$.success({content:"登录成功!正在跳转...",duration:2}),setTimeout(()=>{n.push("/")},800))}catch(c){console.error("登录失败:",c),v.value=!0,$.error({content:c.message||"登录失败,请检查用户名和密码",duration:3}),setTimeout(()=>{v.value=!1},1e3)}finally{s.value=!1}};return J(()=>{localStorage.getItem("token")&&n.push("/")}),(c,e)=>{const b=d("a-input"),p=d("a-form-item"),D=d("a-input-password"),H=d("a-checkbox"),T=d("a-button"),U=d("a-form"),E=d("a-divider");return m(),f("div",fe,[r("div",ge,[e[3]||(e[3]=r("div",{class:"gradient-orb orb-1"},null,-1)),e[4]||(e[4]=r("div",{class:"gradient-orb orb-2"},null,-1)),e[5]||(e[5]=r("div",{class:"gradient-orb orb-3"},null,-1)),r("div",ve,[(m(),f(O,null,L(80,i=>r("div",{class:"particle",key:i,style:X(N())},null,4)),64))]),e[6]||(e[6]=r("div",{class:"grid-overlay"},null,-1))]),r("div",{class:Q(["login-card",{"card-shake":v.value}])},[e[14]||(e[14]=r("div",{class:"card-decoration"},[r("div",{class:"decoration-circle circle-1"}),r("div",{class:"decoration-circle circle-2"}),r("div",{class:"decoration-line"})],-1)),r("div",be,[r("div",ye,[r("div",Oe,[l(g(oe),{class:"logo-icon"}),e[7]||(e[7]=r("div",{class:"logo-glow"},null,-1))]),e[8]||(e[8]=r("h1",{class:"title"},"代理池管理系统",-1))]),e[9]||(e[9]=r("p",{class:"subtitle"},"ProxyPool Management System",-1))]),l(U,{model:o,name:"login",onFinish:A,class:"login-form",rules:I},{default:u(()=>[l(p,{name:"username",class:"form-item"},{default:u(()=>[l(b,{value:o.username,"onUpdate:value":e[0]||(e[0]=i=>o.username=i),size:"large",placeholder:"请输入用户名",prefix:M(g(re),{class:"input-icon"}),autocomplete:"username",onFocus:P,onBlur:z,class:"custom-input"},null,8,["value","prefix"])]),_:1}),l(p,{name:"password",class:"form-item"},{default:u(()=>[l(D,{value:o.password,"onUpdate:value":e[1]||(e[1]=i=>o.password=i),size:"large",placeholder:"请输入密码",prefix:M(g(ae),{class:"input-icon"}),autocomplete:"current-password",onFocus:P,onBlur:z,class:"custom-input"},null,8,["value","prefix"])]),_:1}),l(p,{class:"form-item remember-item"},{default:u(()=>[l(H,{checked:o.remember,"onUpdate:checked":e[2]||(e[2]=i=>o.remember=i),class:"custom-checkbox"},{default:u(()=>[...e[10]||(e[10]=[h(" 记住我 ",-1)])]),_:1},8,["checked"]),r("a",{class:"forgot-password",onClick:F},"忘记密码?")]),_:1}),l(p,{class:"form-item"},{default:u(()=>[l(T,{type:"primary","html-type":"submit",size:"large",loading:s.value,class:"login-button",block:""},{default:u(()=>[s.value?(m(),f(O,{key:1},[l(g(Y),{class:"button-icon"}),e[12]||(e[12]=h(" 登录中... ",-1))],64)):(m(),f(O,{key:0},[l(g(k),{class:"button-icon"}),e[11]||(e[11]=h(" 立即登录 ",-1))],64))]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"]),r("div",he,[l(E,{class:"divider"},{default:u(()=>[...e[13]||(e[13]=[r("span",{class:"divider-text"},"系统特性",-1)])]),_:1}),r("div",_e,[(m(!0),f(O,null,L(V.value,i=>(m(),f("div",{class:"feature-item",key:i.key},[(m(),Z(K(i.icon),{class:"feature-icon"})),r("span",ke,ee(i.text),1)]))),128))])])],2),e[15]||(e[15]=r("div",{class:"copyright"},[r("div",{class:"copyright-content"},[r("span",null,"© 2025 ProxyPool Management System"),r("div",{class:"social-links"},[r("a",{href:"https://github.com/huppugo1/ProxyPoolWithUI",class:"social-link"},"GitHub")])])],-1))])}}}),Me=ne(xe,[["__scopeId","data-v-86301b44"]]);export{Me as default};
2 |
--------------------------------------------------------------------------------
/frontend/deployment/public/_nuxt/Bb3hHU9n.js:
--------------------------------------------------------------------------------
1 | import{u as ce}from"./_4LJ9EKD.js";import{g as pe,r as $,G as K,j as M,k as de,c as y,b as s,x as h,p as v,w as a,A as p,l as D,m,H as fe,o as u,a as B,d as i,t as w,n as k,v as j,F as P,s as me,q as _e,R as U,O as Q,_ as ye}from"#entry";import{L as ge}from"./BAlS7_RU.js";const ve={class:"subscriptions-page"},ke={class:"filter-section"},he={key:2,class:"url-cell"},we={key:0},xe={key:1,class:"text-muted"},be={key:0,class:"text-success"},Ce={key:1},Le={key:2,class:"text-muted"},Te={class:"batch-actions"},$e=pe({__name:"subscriptions",setup(Se){fe(),ce();const C=$(!1),S=$(!1),x=$([]),g=$([]),d=K({type:"",permanent:"",search:""}),O=K({current:1,pageSize:10,total:0,showSizeChanger:!0,showQuickJumper:!0,showTotal:(t,e)=>`第 ${e[0]}-${e[1]} 条,共 ${t} 条`}),W=[{title:"类型",dataIndex:"type",key:"type",width:80,align:"center"},{title:"链接类型",dataIndex:"permanent",key:"permanent",width:100,align:"center"},{title:"订阅链接",dataIndex:"url",key:"url",ellipsis:!0},{title:"参数",dataIndex:"params",key:"params",width:200},{title:"创建时间",dataIndex:"created_at",key:"created_at",width:150,sorter:(t,e)=>new Date(t.created_at).getTime()-new Date(e.created_at).getTime()},{title:"过期时间",dataIndex:"expires_at",key:"expires_at",width:150},{title:"状态",dataIndex:"status",key:"status",width:100,align:"center"},{title:"操作",key:"actions",width:200,align:"center"}],A=M(()=>{let t=g.value;if(d.type&&(t=t.filter(e=>e.type===d.type)),d.permanent!==""&&(t=t.filter(e=>e.permanent===d.permanent)),d.search){const e=d.search.toLowerCase();t=t.filter(n=>n.url.toLowerCase().includes(e)||JSON.stringify(n.params).toLowerCase().includes(e))}return t}),L=M(()=>{const t=g.value.length,e=g.value.filter(o=>o.type==="clash").length,n=g.value.filter(o=>o.type==="v2ray").length,l=g.value.filter(o=>o.permanent).length;return{total:t,clash:e,v2ray:n,permanent:l}}),I=async()=>{C.value=!0;try{const t=localStorage.getItem("token");if(!t){p.error("请先登录");return}const l=`${D().public.apiBase}/subscription_links`;console.log("请求订阅链接API:",l),console.log("Token:",t?"已获取":"未获取");const o=await fetch(l,{headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"}});if(console.log("响应状态:",o.status),console.log("响应头:",o.headers),!o.ok){const _=await o.text();throw console.error("API错误响应:",_),new Error(`HTTP error! status: ${o.status}`)}const c=await o.json();console.log("API响应数据:",c),c.success?(g.value=c.links.map(_=>({..._,refreshing:!1})),O.total=c.links.length):p.error(c.message||"获取订阅链接失败")}catch(t){console.error("加载订阅链接失败:",t),p.error("加载订阅链接失败")}finally{C.value=!1}},X=()=>{I()},Y=()=>{},Z=()=>{d.type="",d.permanent="",d.search=""},R=async t=>{try{await navigator.clipboard.writeText(t),p.success("链接已复制到剪贴板")}catch(e){console.error("复制失败:",e),p.error("复制失败")}},E=async t=>{t.refreshing=!0;try{const e=localStorage.getItem("token"),l=D().public.apiBase,c=await(await fetch(`${l}/subscription_links/${t.id}/refresh`,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"}})).json();c.success?(t.url=c.new_url,p.success("订阅链接刷新成功")):p.error(c.message||"刷新失败")}catch(e){console.error("刷新失败:",e),p.error("刷新失败")}finally{t.refreshing=!1}},V=async t=>{try{const e=localStorage.getItem("token");if(!e){p.error("请先登录");return}const l=D().public.apiBase,c=await(await fetch(`${l}/subscription_links/${t.id}`,{method:"DELETE",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"}})).json();c.success?(p.success(c.message||"订阅链接删除成功"),await I()):p.error(c.message||"删除失败")}catch(e){console.error("删除失败:",e),p.error("删除失败")}},ee=()=>{const t=x.value.map(e=>g.value.find(n=>n.id===e)?.url).filter(Boolean).join(`
2 | `);R(t)},te=async()=>{S.value=!0;try{const t=x.value.map(e=>{const n=g.value.find(l=>l.id===e);return n?E(n):Promise.resolve()});await Promise.all(t),p.success("批量刷新完成")}catch{p.error("批量刷新失败")}finally{S.value=!1}},ae=async()=>{try{const t=x.value.map(e=>{const n=g.value.find(l=>l.id===e);return n?V(n):Promise.resolve()});await Promise.all(t),x.value=[],p.success("批量删除完成")}catch{p.error("批量删除失败")}},se=t=>new Date(t).toLocaleString("zh-CN"),ne=t=>t.permanent?"green":N(t)?"red":z(t)?"orange":"blue",oe=t=>t.permanent?"有效":N(t)?"已过期":z(t)?"即将过期":"有效",N=t=>t.permanent||!t.expires_at?!1:new Date(t.expires_at){if(t.permanent||!t.expires_at)return!1;const e=new Date,l=new Date(t.expires_at).getTime()-e.getTime();return l>0&&l<1800*1e3};return de(()=>{I()}),(t,e)=>{const n=m("a-button"),l=m("a-statistic"),o=m("a-col"),c=m("a-row"),_=m("a-select-option"),F=m("a-select"),le=m("a-input-search"),T=m("a-tag"),re=m("a-input"),H=m("a-popconfirm"),J=m("a-space"),ie=m("a-table"),ue=m("a-empty"),q=m("a-card");return u(),y("div",ve,[s(q,{title:"订阅链接管理",class:"main-card"},{extra:a(()=>[s(n,{type:"primary",onClick:X},{icon:a(()=>[s(k(U))]),default:a(()=>[e[4]||(e[4]=i(" 刷新 ",-1))]),_:1})]),default:a(()=>[s(c,{gutter:16,class:"stats-row"},{default:a(()=>[s(o,{span:6},{default:a(()=>[s(l,{title:"总订阅数",value:L.value.total},null,8,["value"])]),_:1}),s(o,{span:6},{default:a(()=>[s(l,{title:"Clash订阅",value:L.value.clash},null,8,["value"])]),_:1}),s(o,{span:6},{default:a(()=>[s(l,{title:"V2Ray订阅",value:L.value.v2ray},null,8,["value"])]),_:1}),s(o,{span:6},{default:a(()=>[s(l,{title:"永久链接",value:L.value.permanent},null,8,["value"])]),_:1})]),_:1}),B("div",ke,[s(c,{gutter:16,align:"middle"},{default:a(()=>[s(o,{span:6},{default:a(()=>[s(F,{value:d.type,"onUpdate:value":e[0]||(e[0]=f=>d.type=f),placeholder:"选择类型","allow-clear":"",style:{width:"100%"}},{default:a(()=>[s(_,{value:""},{default:a(()=>[...e[5]||(e[5]=[i("全部类型",-1)])]),_:1}),s(_,{value:"clash"},{default:a(()=>[...e[6]||(e[6]=[i("Clash",-1)])]),_:1}),s(_,{value:"v2ray"},{default:a(()=>[...e[7]||(e[7]=[i("V2Ray",-1)])]),_:1})]),_:1},8,["value"])]),_:1}),s(o,{span:6},{default:a(()=>[s(F,{value:d.permanent,"onUpdate:value":e[1]||(e[1]=f=>d.permanent=f),placeholder:"链接类型","allow-clear":"",style:{width:"100%"}},{default:a(()=>[s(_,{value:""},{default:a(()=>[...e[8]||(e[8]=[i("全部",-1)])]),_:1}),s(_,{value:!0},{default:a(()=>[...e[9]||(e[9]=[i("永久链接",-1)])]),_:1}),s(_,{value:!1},{default:a(()=>[...e[10]||(e[10]=[i("临时链接",-1)])]),_:1})]),_:1},8,["value"])]),_:1}),s(o,{span:8},{default:a(()=>[s(le,{value:d.search,"onUpdate:value":e[2]||(e[2]=f=>d.search=f),placeholder:"搜索链接或参数","enter-button":"",onSearch:Y},null,8,["value"])]),_:1}),s(o,{span:4},{default:a(()=>[s(n,{onClick:Z},{default:a(()=>[...e[11]||(e[11]=[i("清除筛选",-1)])]),_:1})]),_:1})]),_:1})]),s(ie,{columns:W,"data-source":A.value,loading:C.value,pagination:O,"row-key":"id",class:"subscription-table"},{bodyCell:a(({column:f,record:r})=>[f.key==="type"?(u(),h(T,{key:0,color:r.type==="clash"?"blue":"green"},{default:a(()=>[i(w(r.type==="clash"?"Clash":"V2Ray"),1)]),_:2},1032,["color"])):v("",!0),f.key==="permanent"?(u(),h(T,{key:1,color:r.permanent?"green":"orange"},{default:a(()=>[i(w(r.permanent?"永久":"临时"),1)]),_:2},1032,["color"])):v("",!0),f.key==="url"?(u(),y("div",he,[s(re,{value:r.url,readonly:"",class:"url-input"},null,8,["value"]),s(n,{type:"link",size:"small",onClick:b=>R(r.url)},{icon:a(()=>[s(k(j))]),_:1},8,["onClick"])])):v("",!0),f.key==="params"?(u(),y(P,{key:3},[r.params&&Object.keys(r.params).length>0?(u(),y("div",we,[(u(!0),y(P,null,me(r.params,(b,G)=>(u(),h(T,{key:G,size:"small"},{default:a(()=>[i(w(G)+": "+w(b),1)]),_:2},1024))),128))])):(u(),y("span",xe,"无参数"))],64)):v("",!0),f.key==="expires_at"?(u(),y(P,{key:4},[r.permanent?(u(),y("span",be,"永不过期")):r.expires_at?(u(),y("span",Ce,w(se(r.expires_at)),1)):(u(),y("span",Le,"-"))],64)):v("",!0),f.key==="status"?(u(),h(T,{key:5,color:ne(r),class:_e({blink:z(r)})},{default:a(()=>[i(w(oe(r)),1)]),_:2},1032,["color","class"])):v("",!0),f.key==="actions"?(u(),h(J,{key:6},{default:a(()=>[s(n,{type:"link",size:"small",onClick:b=>R(r.url)},{icon:a(()=>[s(k(j))]),default:a(()=>[e[12]||(e[12]=i(" 复制 ",-1))]),_:1},8,["onClick"]),s(n,{type:"link",size:"small",onClick:b=>E(r),loading:r.refreshing},{icon:a(()=>[s(k(U))]),default:a(()=>[e[13]||(e[13]=i(" 刷新 ",-1))]),_:1},8,["onClick","loading"]),s(H,{title:"确定要删除这个订阅链接吗?",onConfirm:b=>V(r),"ok-text":"确定","cancel-text":"取消"},{default:a(()=>[s(n,{type:"link",size:"small",danger:""},{icon:a(()=>[s(k(Q))]),default:a(()=>[e[14]||(e[14]=i(" 删除 ",-1))]),_:1})]),_:1},8,["onConfirm"])]),_:2},1024)):v("",!0)]),_:1},8,["data-source","loading","pagination"]),!C.value&&A.value.length===0?(u(),h(ue,{key:0,description:"暂无订阅链接",class:"empty-state"},{image:a(()=>[s(k(ge),{style:{"font-size":"64px",color:"#d9d9d9"}})]),default:a(()=>[s(n,{type:"primary",onClick:e[3]||(e[3]=f=>t.$router.push("/"))},{default:a(()=>[...e[15]||(e[15]=[i(" 去生成订阅链接 ",-1)])]),_:1})]),_:1})):v("",!0)]),_:1}),x.value.length>0?(u(),h(q,{key:0,class:"batch-actions-card"},{default:a(()=>[B("div",Te,[B("span",null,"已选择 "+w(x.value.length)+" 项",1),s(J,null,{default:a(()=>[s(n,{onClick:ee},{icon:a(()=>[s(k(j))]),default:a(()=>[e[16]||(e[16]=i(" 批量复制 ",-1))]),_:1}),s(n,{onClick:te,loading:S.value},{icon:a(()=>[s(k(U))]),default:a(()=>[e[17]||(e[17]=i(" 批量刷新 ",-1))]),_:1},8,["loading"]),s(H,{title:"确定要删除选中的订阅链接吗?",onConfirm:ae,"ok-text":"确定","cancel-text":"取消"},{default:a(()=>[s(n,{danger:""},{icon:a(()=>[s(k(Q))]),default:a(()=>[e[18]||(e[18]=i(" 批量删除 ",-1))]),_:1})]),_:1})]),_:1})])]),_:1})):v("",!0)])}}}),De=ye($e,[["__scopeId","data-v-fc9f453e"]]);export{De as default};
3 |
--------------------------------------------------------------------------------