├── api └── __init__.py ├── proc ├── __init__.py ├── README.md ├── run_fetcher.py └── run_validator.py ├── data ├── data.db ├── users.json ├── api_status.json ├── sub.json └── config.py ├── db ├── __init__.py ├── init.py ├── Fetcher.py ├── README.md └── Proxy.py ├── docs ├── term.png ├── workflow.png ├── screenshot1.png └── screenshot2.png ├── frontend ├── deployment │ ├── public │ │ ├── _nuxt │ │ │ ├── BzLCLO6P.js │ │ │ ├── builds │ │ │ │ ├── latest.json │ │ │ │ └── meta │ │ │ │ │ └── a3eded11-27b5-44d2-9d03-622cfea75ff5.json │ │ │ ├── empty.DqNK5KfW.css │ │ │ ├── DvTSu5G4.js │ │ │ ├── Hsbwpiff.js │ │ │ ├── 1nlWnoXy.js │ │ │ ├── CoiHVIeb.js │ │ │ ├── UewoGXxE.js │ │ │ ├── BnoVSOQD.js │ │ │ ├── DdFW8pdL.js │ │ │ ├── BAlS7_RU.js │ │ │ ├── _4LJ9EKD.js │ │ │ ├── error-500.DOWD7OuR.css │ │ │ ├── KxedcqoH.js │ │ │ ├── C9tsJkms.js │ │ │ ├── DvlYdpVS.js │ │ │ ├── DPAwVfCt.js │ │ │ ├── error-404.BSvats-j.css │ │ │ ├── DzUz1rp7.js │ │ │ ├── BNRG3dZD.js │ │ │ ├── fetchers.u5Umj_Q-.css │ │ │ ├── theme-test.BaDAO6fs.css │ │ │ ├── CmFl7aLf.js │ │ │ ├── subscriptions.vmYHVIar.css │ │ │ ├── 1nLSuJ89.js │ │ │ ├── BR_WPFp-.js │ │ │ └── Bb3hHU9n.js │ │ ├── 200.html │ │ ├── 404.html │ │ ├── index.html │ │ ├── api │ │ │ └── index.html │ │ ├── login │ │ │ └── index.html │ │ ├── fetchers │ │ │ └── index.html │ │ ├── theme-test │ │ │ └── index.html │ │ └── subscriptions │ │ │ └── index.html │ └── nitro.json ├── src │ ├── app.vue │ ├── plugins │ │ ├── antd.ts │ │ └── axios.ts │ ├── build.sh │ ├── layouts │ │ └── empty.vue │ ├── jsconfig.json │ ├── .editorconfig │ ├── middleware │ │ └── auth.global.ts │ ├── package.json │ ├── types │ │ ├── app.d.ts │ │ └── nuxt.d.ts │ ├── .eslintrc.js │ ├── nuxt.config.ts │ ├── tsconfig.json │ ├── .gitignore │ ├── composables │ │ ├── useBackendStatus.ts │ │ └── useTheme.ts │ └── README_NUXT3.md └── README.md ├── auth ├── __init__.py └── auth_manager.py ├── Dockerfile ├── requirements.txt ├── fetchers ├── README.md ├── ProxyScrapeFetcher.py ├── ProxyscanFetcher.py ├── BaseFetcher.py ├── __init__.py ├── UUFetcher.py ├── KaiXinFetcher.py ├── GoubanjiaFetcher.py ├── XiLaFetcher.py ├── KuaidailiFetcher.py ├── IP89Fetcher.py ├── JiangxianliFetcher.py ├── XiaoShuFetcher.py ├── IP66Fetcher.py ├── ProxyListFetcher.py ├── IHuanFetcher.py ├── IP3366Fetcher.py ├── FetcherUtils.py └── FETCHER_AUTH_EXAMPLE.md ├── .gitignore ├── LICENSE ├── .travis.yml ├── .github └── workflows │ └── docker-publish.yml ├── utils └── ip_location.py ├── setup_security.py └── main.py /api/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 -------------------------------------------------------------------------------- /proc/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | -------------------------------------------------------------------------------- /data/data.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huppugo1/ProxyPoolWithUI/HEAD/data/data.db -------------------------------------------------------------------------------- /db/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from .init import init 4 | 5 | init() 6 | -------------------------------------------------------------------------------- /docs/term.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huppugo1/ProxyPoolWithUI/HEAD/docs/term.png -------------------------------------------------------------------------------- /docs/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huppugo1/ProxyPoolWithUI/HEAD/docs/workflow.png -------------------------------------------------------------------------------- /docs/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huppugo1/ProxyPoolWithUI/HEAD/docs/screenshot1.png -------------------------------------------------------------------------------- /docs/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huppugo1/ProxyPoolWithUI/HEAD/docs/screenshot2.png -------------------------------------------------------------------------------- /frontend/deployment/public/_nuxt/BzLCLO6P.js: -------------------------------------------------------------------------------- 1 | import"#entry";const s=globalThis.setInterval;export{s}; 2 | -------------------------------------------------------------------------------- /frontend/src/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/deployment/public/_nuxt/builds/latest.json: -------------------------------------------------------------------------------- 1 | {"id":"a3eded11-27b5-44d2-9d03-622cfea75ff5","timestamp":1760883715264} -------------------------------------------------------------------------------- /auth/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from .auth_manager import AuthManager 4 | 5 | __all__ = ['AuthManager'] 6 | 7 | -------------------------------------------------------------------------------- /frontend/deployment/public/_nuxt/empty.DqNK5KfW.css: -------------------------------------------------------------------------------- 1 | .empty-layout[data-v-f4f82991]{height:100%;min-height:100vh;width:100%} 2 | -------------------------------------------------------------------------------- /frontend/src/plugins/antd.ts: -------------------------------------------------------------------------------- 1 | import Antd from 'ant-design-vue' 2 | 3 | export default defineNuxtPlugin((nuxtApp) => { 4 | nuxtApp.vueApp.use(Antd) 5 | }) 6 | 7 | -------------------------------------------------------------------------------- /frontend/deployment/public/_nuxt/builds/meta/a3eded11-27b5-44d2-9d03-622cfea75ff5.json: -------------------------------------------------------------------------------- 1 | {"id":"a3eded11-27b5-44d2-9d03-622cfea75ff5","timestamp":1760883715264,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]} -------------------------------------------------------------------------------- /frontend/src/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd `dirname $0` 6 | 7 | rm -rf dist # 删除已经存在的目录 8 | npm run generate # 生成静态文件 9 | 10 | rm -rf ../deployment 11 | mv dist ../deployment 12 | 13 | echo 'Done.' 14 | -------------------------------------------------------------------------------- /data/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin": { 3 | "password": "240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9", 4 | "role": "admin", 5 | "created_at": "2025-10-18T21:18:47.500635", 6 | "password_changed_at": "2025-10-19T12:27:46.997139" 7 | } 8 | } -------------------------------------------------------------------------------- /frontend/src/layouts/empty.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/deployment/public/_nuxt/DvTSu5G4.js: -------------------------------------------------------------------------------- 1 | import{_ as t,c as o,o as s,X as c}from"#entry";const a={},n={class:"empty-layout"};function r(e,_){return s(),o("div",n,[c(e.$slots,"default",{},void 0)])}const f=t(a,[["render",r],["__scopeId","data-v-f4f82991"]]);export{f as default}; 2 | -------------------------------------------------------------------------------- /frontend/deployment/public/_nuxt/Hsbwpiff.js: -------------------------------------------------------------------------------- 1 | import{u as a,e as s,h as u,i as r,f as o}from"#entry";function i(e){const t=e||s();return t?.ssrContext?.head||t?.runWithContext(()=>{if(u())return r(o)})}function x(e,t={}){const n=i(t.nuxt);if(n)return a(e,{head:n,...t})}export{x as u}; 2 | -------------------------------------------------------------------------------- /frontend/src/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./*"], 6 | "@/*": ["./*"], 7 | "~~/*": ["./*"], 8 | "@@/*": ["./*"] 9 | } 10 | }, 11 | "exclude": ["node_modules", ".nuxt", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /proc/README.md: -------------------------------------------------------------------------------- 1 | # 进程管理 2 | 3 | 包含爬取器和验证器的运行代码。 4 | 5 | ## 文件说明 6 | 7 | - `run_fetcher.py` - 爬取器进程,定时运行注册的爬取器,将代理存入数据库 8 | - `run_validator.py` - 验证器进程,持续验证数据库中的代理可用性 9 | 10 | ## 工作流程 11 | 12 | 1. **爬取器**:定时从各代理源爬取代理,存入数据库 13 | 2. **验证器**:从数据库获取待验证代理,进行可用性验证 14 | 3. **进程管理**:主进程监控子进程状态,自动重启异常进程 15 | -------------------------------------------------------------------------------- /frontend/deployment/nitro.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": "2025-10-19T14:22:13.446Z", 3 | "preset": "static", 4 | "framework": { 5 | "name": "nuxt", 6 | "version": "3.19.3" 7 | }, 8 | "versions": { 9 | "nitro": "2.12.7" 10 | }, 11 | "commands": { 12 | "preview": "npx serve public" 13 | }, 14 | "config": {} 15 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | WORKDIR /proxy 4 | 5 | COPY requirements.txt . 6 | RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ \ 7 | && pip install -U pip --no-cache-dir \ 8 | && pip install --no-cache-dir -r requirements.txt 9 | 10 | # 复制所有文件(包括 setup_security.py) 11 | COPY . . 12 | 13 | # 执行安全配置(重要!) 14 | RUN python setup_security.py 15 | 16 | EXPOSE 5000 17 | CMD ["python", "main.py"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi>=2023.7.22 2 | chardet>=5.2.0 3 | click>=8.1.0 4 | cssselect>=1.2.0 5 | Flask>=2.3.0,<3.0.0 6 | idna>=3.4 7 | itsdangerous>=2.1.0 8 | Jinja2>=3.1.2,<4.0.0 9 | lxml>=4.9.0 10 | MarkupSafe>=2.1.3,<3.0.0 11 | pyquery>=2.0.0 12 | requests>=2.31.0,<3.0.0 13 | urllib3>=2.0.0,<3.0.0 14 | Werkzeug>=2.3.0,<3.0.0 15 | PySocks>=1.7.1 16 | func-timeout>=4.3.5 17 | PyYAML>=6.0 18 | PyJWT>=2.8.0,<3.0.0 19 | Flask-Cors>=4.0.0 20 | psutil>=5.9.0 21 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # 前端目录 2 | 3 | 前端代码基于 Nuxt 3 + Vue 3 + TypeScript + Ant Design Vue 构建。 4 | 5 | ## 快速开始 6 | 7 | ### 生产模式(推荐) 8 | ```bash 9 | # 在项目根目录 10 | python main.py 11 | ``` 12 | 访问:http://localhost:5000/web 13 | 14 | ### 开发模式 15 | ```bash 16 | # 终端1 - 后端 17 | python main.py 18 | 19 | # 终端2 - 前端 20 | cd frontend/src 21 | npm run dev 22 | ``` 23 | 24 | ## 构建 25 | ```bash 26 | cd frontend/src 27 | npm run generate 28 | ``` 29 | 30 | 详细开发指南请参考:[frontend/src/README_NUXT3.md](src/README_NUXT3.md) 31 | -------------------------------------------------------------------------------- /frontend/src/middleware/auth.global.ts: -------------------------------------------------------------------------------- 1 | // 全局路由守卫 - 检查登录状态 2 | export default defineNuxtRouteMiddleware((to, from) => { 3 | // 在客户端运行 4 | if (process.client) { 5 | const token = localStorage.getItem('token') 6 | const isLoginPage = to.path === '/login' 7 | 8 | // 如果没有token且不是登录页,跳转到登录页 9 | if (!token && !isLoginPage) { 10 | return navigateTo('/login') 11 | } 12 | 13 | // 如果有token且是登录页,跳转到首页 14 | if (token && isLoginPage) { 15 | return navigateTo('/') 16 | } 17 | } 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /frontend/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "2.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "nuxt dev", 8 | "build": "nuxt build", 9 | "generate": "nuxt generate", 10 | "preview": "nuxt preview", 11 | "postinstall": "nuxt prepare" 12 | }, 13 | "dependencies": { 14 | "ant-design-vue": "^4.0.0", 15 | "axios": "^1.6.0", 16 | "moment": "^2.29.4", 17 | "nuxt": "^3.9.0", 18 | "vue": "^3.4.0", 19 | "vue-router": "^4.2.0" 20 | }, 21 | "devDependencies": { 22 | "@nuxt/devtools": "latest" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/types/app.d.ts: -------------------------------------------------------------------------------- 1 | // App-specific type declarations 2 | 3 | // Extend NuxtApp with custom $http plugin 4 | declare module '#app' { 5 | interface NuxtApp { 6 | $http: { 7 | get: (url: string, params?: any) => Promise 8 | post: (url: string, data?: any, params?: any) => Promise 9 | } 10 | } 11 | } 12 | 13 | declare module 'vue' { 14 | interface ComponentCustomProperties { 15 | $http: { 16 | get: (url: string, params?: any) => Promise 17 | post: (url: string, data?: any, params?: any) => Promise 18 | } 19 | } 20 | } 21 | 22 | export {} 23 | 24 | -------------------------------------------------------------------------------- /fetchers/README.md: -------------------------------------------------------------------------------- 1 | # 爬取器 2 | 3 | 所有的爬取器都在这个目录中,并且在`__init__.py`中进行了注册。 4 | 5 | ## 添加新的爬取器 6 | 7 | 本项目默认包含了数量不少的免费公开代理源,并且会持续更新,如果你发现有不错的免费代理源,欢迎通过Issues反馈给我们。 8 | 9 | 1. 编写爬取器代码 10 | 11 | 爬取器需要继承基类`BaseFetcher`,然后实现`fetch`函数。 12 | 13 | `fetch`函数没有输入参数,每次运行都返回一个列表,列表中包含本次爬取到的代理。返回的格式为(代理协议类型,代理IP,端口)。 14 | 15 | 示例: 16 | 17 | ```python 18 | class CustomFetcher(BaseFetcher): 19 | def fetch(self): 20 | return [('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)] 21 | ``` 22 | 23 | 2. 注册爬取器 24 | 25 | 编写好爬取器之后,还需要在`__init__.py`文件中进行注册,添加如下代码: 26 | 27 | **注意:爬取器的名称(name)一定不能重复。** 28 | 29 | ```python 30 | from .CustomFetcher import CustomFetcher 31 | 32 | fetchers = [ 33 | ... 34 | Fetcher(name='www.custom.com', fetcher=CustomFetcher), 35 | ... 36 | ] 37 | ``` 38 | 39 | 3. 重启 40 | 41 | 完成上述步骤之后,重启进程即可。代码会自动将新爬取器添加到数据库中,爬取进程也会自动运行新爬取器。 42 | -------------------------------------------------------------------------------- /data/api_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "/auth/login": true, 4 | "/auth/verify": true, 5 | "/auth/change_password": true 6 | }, 7 | "proxy": { 8 | "/fetch_random": true, 9 | "/fetch_all": true, 10 | "/fetch_http": true, 11 | "/fetch_https": true, 12 | "/fetch_socks4": true, 13 | "/fetch_socks5": true 14 | }, 15 | "clash": { 16 | "/clash": true, 17 | "/clash/proxies": true 18 | }, 19 | "v2ray": { 20 | "/v2ray": true 21 | }, 22 | "subscription": { 23 | "/generate_subscription_links": true, 24 | "/subscribe/clash": true, 25 | "/subscribe/v2ray": true, 26 | "/subscription_links": true 27 | }, 28 | "management": { 29 | "/proxies_status": true, 30 | "/fetchers_status": true, 31 | "/add_proxy": true, 32 | "/delete_proxy": true, 33 | "/fetcher_enable": true, 34 | "/ping": true 35 | } 36 | } -------------------------------------------------------------------------------- /frontend/src/types/nuxt.d.ts: -------------------------------------------------------------------------------- 1 | // Nuxt 3 Auto-imports type declarations 2 | import type { NuxtApp } from '#app' 3 | import type { RuntimeConfig } from 'nuxt/schema' 4 | 5 | declare global { 6 | const useNuxtApp: () => NuxtApp 7 | const useRuntimeConfig: () => RuntimeConfig 8 | const useRoute: typeof import('vue-router')['useRoute'] 9 | const useRouter: typeof import('vue-router')['useRouter'] 10 | const navigateTo: typeof import('#app')['navigateTo'] 11 | const onMounted: typeof import('vue')['onMounted'] 12 | const onUnmounted: typeof import('vue')['onUnmounted'] 13 | const ref: typeof import('vue')['ref'] 14 | const computed: typeof import('vue')['computed'] 15 | const watch: typeof import('vue')['watch'] 16 | const defineNuxtPlugin: typeof import('#app')['defineNuxtPlugin'] 17 | const defineNuxtConfig: typeof import('nuxt/config')['defineNuxtConfig'] 18 | } 19 | 20 | export {} 21 | 22 | -------------------------------------------------------------------------------- /db/init.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from data.config import DATABASE_PATH 4 | from .Proxy import Proxy 5 | from .Fetcher import Fetcher 6 | from fetchers import fetchers 7 | import sqlite3 8 | 9 | def init(): 10 | """ 11 | 初始化数据库 12 | """ 13 | 14 | conn = sqlite3.connect(DATABASE_PATH) 15 | 16 | create_tables = Proxy.ddls + Fetcher.ddls 17 | for sql in create_tables: 18 | conn.execute(sql) 19 | conn.commit() 20 | 21 | # 注册所有的爬取器 22 | c = conn.cursor() 23 | c.execute('BEGIN EXCLUSIVE TRANSACTION;') 24 | for item in fetchers: 25 | c.execute('SELECT * FROM fetchers WHERE name=?', (item.name,)) 26 | if c.fetchone() is None: 27 | f = Fetcher() 28 | f.name = item.name 29 | c.execute('INSERT INTO fetchers VALUES(?,?,?,?,?)', f.params()) 30 | c.close() 31 | conn.commit() 32 | 33 | conn.close() 34 | -------------------------------------------------------------------------------- /fetchers/ProxyScrapeFetcher.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import requests 4 | 5 | from .BaseFetcher import BaseFetcher 6 | 7 | 8 | class ProxyScrapeFetcher(BaseFetcher): 9 | """ 10 | https://api.proxyscrape.com/?request=displayproxies&proxytype={{ protocol }}&_t={{ timestamp }} 11 | """ 12 | 13 | def fetch(self): 14 | proxies = [] 15 | type_list = ['socks4', 'socks5', 'http', 'https'] 16 | for protocol in type_list: 17 | url = "https://api.proxyscrape.com/?request=displayproxies&proxytype=" + protocol + "&_t=" + str( 18 | time.time()) 19 | resp = requests.get(url).text 20 | for data in resp.split("\n"): 21 | flag_idx = data.find(":") 22 | ip = data[:flag_idx] 23 | port = data[flag_idx + 1:-1] 24 | proxies.append((protocol, ip, port)) 25 | 26 | return list(set(proxies)) 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | ENV/ 26 | env/ 27 | 28 | # Database 29 | #*.db 30 | *.db-journal 31 | *.db-shm 32 | *.db-wal 33 | 34 | # Data directory (contains sensitive files) 35 | data/ 36 | !data/.gitkeep 37 | 38 | # User data (sensitive) - moved to data/ 39 | # users.json 40 | 41 | # IDE 42 | .vscode/ 43 | .idea/ 44 | *.swp 45 | *.swo 46 | *~ 47 | 48 | # OS 49 | .DS_Store 50 | Thumbs.db 51 | 52 | # Logs 53 | *.log 54 | 55 | # Environment variables 56 | .env 57 | .env.local 58 | .env.*.local 59 | 60 | # Frontend 61 | node_modules/ 62 | .nuxt/ 63 | .output/ 64 | .cache/ 65 | 66 | # Testing 67 | .pytest_cache/ 68 | htmlcov/ 69 | .coverage 70 | -------------------------------------------------------------------------------- /fetchers/ProxyscanFetcher.py: -------------------------------------------------------------------------------- 1 | from .BaseFetcher import BaseFetcher 2 | import requests 3 | import time 4 | 5 | class ProxyscanFetcher(BaseFetcher): 6 | """ 7 | https://www.proxyscan.io/api/proxy?last_check=9800&uptime=50&limit=20&_t={{ timestamp }} 8 | """ 9 | 10 | def fetch(self): 11 | """ 12 | 执行一次爬取,返回一个数组,每个元素是(protocol, ip, port),portocol是协议名称,目前主要为http 13 | 返回示例:[('http', '127.0.0.1', 8080), ('http', '127.0.0.1', 1234)] 14 | """ 15 | proxies = [] 16 | # 此API为随机获取接口,获取策略为:重复取十次后去重 17 | for _ in range(10): 18 | url = "https://www.proxyscan.io/api/proxy?last_check=9800&uptime=50&limit=20&_t=" + str(time.time()) 19 | resp = requests.get(url).json() 20 | for data in resp: 21 | protocol = str.lower(data['Type'][0]) 22 | proxies.append((protocol, data['Ip'], data['Port'])) 23 | 24 | return list(set(proxies)) -------------------------------------------------------------------------------- /frontend/src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | '@nuxtjs', 12 | 'plugin:nuxt/recommended' 13 | ], 14 | plugins: [ 15 | ], 16 | // add your custom rules here 17 | rules: { 18 | semi: ['error', 'always'], 19 | indent: ['error', 4], 20 | 'vue/html-indent': ['error', 4], 21 | camelcase: 'off', 22 | 'no-return-await': 'off', 23 | 'vue/no-parsing-error': 'off', 24 | 'no-unused-vars': 'warn', 25 | 'vue/html-self-closing': 'off', 26 | 'prefer-const': 'warn', 27 | 'vue/singleline-html-element-content-newline': 'off', 28 | 'vue/no-unused-components': 'warn', 29 | 'import/no-named-as-default': 'off', 30 | 'vue/no-unused-vars': 'warn' 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /fetchers/BaseFetcher.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class BaseFetcher(object): 4 | """ 5 | 所有爬取器的基类 6 | """ 7 | 8 | def fetch(self): 9 | """ 10 | 执行一次爬取,返回一个数组 11 | 每个元素可以是以下格式之一: 12 | 1. (protocol, ip, port) - 最基本,只有代理信息 13 | 2. (protocol, ip, port, username, password) - 包含认证信息 14 | 3. (protocol, ip, port, username, password, country, address) - 包含完整信息 15 | 16 | protocol是协议名称,目前主要为http/https/socks5等 17 | 18 | 返回示例: 19 | [ 20 | ('http', '127.0.0.1', 8080), # 只有代理 21 | ('socks5', '1.2.3.4', 1080, 'user1', 'pass1'), # 有认证 22 | ('socks5', '1.2.3.4', 1080, 'user1', 'pass1', '美国', '洛杉矶'), # 完整信息 23 | ('http', '5.6.7.8', 8080, None, None, '日本', '东京'), # 有位置无认证 24 | ] 25 | 26 | 说明: 27 | - username/password 为 None 表示无需认证 28 | - country/address 为 None 表示未知,系统会在验证成功后自动获取 29 | - 如果爬取器能获取到这些信息,建议直接返回,减少后续查询 30 | """ 31 | raise NotImplementedError() 32 | -------------------------------------------------------------------------------- /frontend/deployment/public/_nuxt/1nlWnoXy.js: -------------------------------------------------------------------------------- 1 | import{b as o,I as c}from"#entry";var i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M848 359.3H627.7L825.8 109c4.1-5.3.4-13-6.3-13H436c-2.8 0-5.5 1.5-6.9 4L170 547.5c-3.1 5.3.7 12 6.9 12h174.4l-89.4 357.6c-1.9 7.8 7.5 13.3 13.3 7.7L853.5 373c5.2-4.9 1.7-13.7-5.5-13.7zM378.2 732.5l60.3-241H281.1l189.6-327.4h224.6L487 427.4h211L378.2 732.5z"}}]},name:"thunderbolt",theme:"outlined"};function l(r){for(var t=1;t= 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 | 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 | 135 | 136 | 141 | ``` 142 | 143 | 使用时无需导入(自动导入): 144 | 145 | ```vue 146 | 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 | 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 | --------------------------------------------------------------------------------