Bladeren bron

前后端开启和关闭代理

mrh 1 jaar geleden
bovenliggende
commit
e87c13eedc

+ 2 - 2
ui/backend/config.yaml

@@ -7,11 +7,11 @@ sub:
   proxies:
     9660:
       file_path: g:\code\upwork\zhang_crawl_bio\download\proxy_pool\temp\9660.yaml
-      name: ""
+      name: "\U0001F1FA\U0001F1F8\u7F8E\u56FD\u5723\u4F55\u585E5"
       port: 9660
     9662:
       file_path: g:\code\upwork\zhang_crawl_bio\download\proxy_pool\temp\9662.yaml
-      name: ""
+      name: "\U0001F1EF\U0001F1F5\u4E9A\u9A6C\u900A\u65E5\u672C2"
       port: 9662
   start_port: 9660
   temp_dir: g:\code\upwork\zhang_crawl_bio\download\proxy_pool\temp

+ 16 - 1
ui/backend/main.py

@@ -1,10 +1,25 @@
+import asyncio
 from pathlib import Path
 import sys
 # 为了避免耦合,微服务,可能确实要将上级的上级目录作为一个单独的进程来处理,此目录作为一个单独的UI项目
 sys.path.append(str(Path(__file__).parent))
 from fastapi import FastAPI
-from routers.proxy import router
+from routers.proxy import router,health_check_proxy_task
 from fastapi.middleware.cors import CORSMiddleware
+from contextlib import asynccontextmanager
+from utils.process_mgr import process_manager
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    """应用生命周期管理"""
+    health_check_task_instance = asyncio.create_task(health_check_proxy_task(interval=90))
+    yield
+    health_check_task_instance.cancel()  # 取消任务
+    try:
+        await health_check_task_instance  # 等待任务结束
+    except asyncio.CancelledError:
+        pass
+    await process_manager.cleanup()
+
 # 创建 FastAPI 应用实例
 app = FastAPI(
     description="",

+ 85 - 22
ui/backend/routers/proxy.py

@@ -4,7 +4,7 @@ import hashlib
 from pathlib import Path
 import random
 from typing import Dict, List, Optional
-from fastapi import APIRouter
+from fastapi import APIRouter, HTTPException
 from pydantic import BaseModel
 from cachetools import TTLCache
 from utils.win import get_proxy_settings
@@ -17,7 +17,7 @@ from src.services.subscription_manager import SubscriptionManager
 sub_mgr = SubscriptionManager(config=config)
 proxy_lock = asyncio.Lock()  # 全局异步锁
 router = APIRouter()
-cache = TTLCache(maxsize=100, ttl=160)
+cache = TTLCache(maxsize=100, ttl=360)
 
 
 class SysProxyResponse(BaseModel):
@@ -39,16 +39,17 @@ class ProxyResponse(BaseModel):
     file_path: str
     mgr_url: str
     process_info: Optional[Dict] = None
+    ping: Optional[Dict[str, int]] = None
 
-async def get_proxy_response(port: int):
+async def get_proxy_response(port: int, use_cache: bool = True):
+    cache_key = f"get_proxy_response_{port}"
     porxy_model = sub_mgr.sub.proxies.get(port)
     proxy_mgr = sub_mgr.list_proxies_mgr.get(port)
     mgr_url = proxy_mgr.get_management_url()
-    logger.info(f"checking port {port}")
     reachable = await port_is_using(port, timeout=0.5)
-    logger.info(f"checking port {port} result: {reachable}")
     name = porxy_model.name or ''
     process_info = None
+    ping = {}
     if reachable:
         process_info = proxy_mgr.get_process_info()
         response = await proxy_mgr.get_now_selected_proxy()
@@ -60,24 +61,42 @@ async def get_proxy_response(port: int):
                 sub_mgr.save_config()
         else:
             name = porxy_model.name or ''
-        logger.info(f"{response}")
-    return ProxyResponse(
+        ping = await proxy_mgr.ping_proxies()
+        # logger.info(f"{response}")
+    result = ProxyResponse(
         name=name, 
         port=porxy_model.port,
         reachable=reachable,
         file_path=porxy_model.file_path,
         mgr_url=mgr_url,
         process_info=process_info,
-        )
+        ping=ping
+    )
+    return result
 
-async def get_all_proxy_response():
+async def get_all_proxy_response(use_cache: bool = True):
     global sub_mgr
     ret = []
     tasks = []
     for port,porxy_model in sub_mgr.sub.proxies.items():
-        tasks.append(get_proxy_response(port))
+        tasks.append(get_proxy_response(port, use_cache))
     ret = await asyncio.gather(*tasks)
     return ret
+async def health_check_proxy_task(interval: int = 80):
+    """定时检查所有代理的健康状态"""
+    while True:
+        try:
+            logger.info("Running health check...")
+            proxies = await get_all_proxy_response(use_cache=True)
+            # if not proxies:
+            #     logger.error("Health check failed: No proxies available")
+            # else:
+            #     logger.info(f"Health check succeeded: {len(proxies)} proxies are healthy")
+        except Exception as e:
+            logger.error(f"Health check failed: {e}")
+        
+        # 等待指定的时间间隔
+        await asyncio.sleep(interval)
 @router.get("/ping")
 async def ping_proxies() -> Dict[str, int]:
     global sub_mgr,cache
@@ -89,19 +108,32 @@ async def ping_proxies() -> Dict[str, int]:
         except Exception as e:
             logger.error(f"ping_proxies error: {e}")
             return {"err": 1, "msg": str(e)}
+    else:
+        logger.info(f"use cache: {cache_key}")
     return cache[cache_key]
 
+@router.get("/proxies/{port}")
 @router.get("/proxies")
-async def get_proxies():
-    ret = await get_all_proxy_response()
-    logger.info(f"{ret}")
-    return ret
+async def get_proxies(port: int = None):
+    if port:
+        proxy_mgr = sub_mgr.get_proxy_manager(port)
+        if not proxy_mgr:
+            raise HTTPException(status_code=404, detail=f"Proxy with port {port} not found")
+        return await get_proxy_response(port)
+    else:
+        ret = await get_all_proxy_response()
+        logger.info(f"{ret}")
+        return ret
 
 class ProxyPost(BaseModel):
     name: Optional[str] = None
     port: Optional[int] = None
-    auto: Optional[bool] = True
+    auto: Optional[bool] = False
 
+class ProxyPostResponse(BaseModel):
+    err: int = 1
+    msg: str = ''
+    data: ProxyResponse
 async def auto_select_proxy(port: int):
     global sub_mgr
     ping_res = await ping_proxies()
@@ -109,29 +141,60 @@ async def auto_select_proxy(port: int):
     # sub_mgr.list_proxies_mgr.get(port).get_management_url()
     await sub_mgr.select_proxy(port, name)
 
+@router.delete("/proxies/{port}")
+async def delete_proxy(port: int):
+    global sub_mgr
+    try:
+        proxy_mgr = sub_mgr.get_proxy_manager(port)
+        if not proxy_mgr:
+            raise HTTPException(status_code=404, detail=f"Proxy with port {port} not found")
+            
+        await sub_mgr.stop_proxy(port)
+        if port in sub_mgr.sub.proxies:
+            del sub_mgr.sub.proxies[port]
+            sub_mgr.save_config()
+            
+        return await get_all_proxy_response()
+    except Exception as e:
+        logger.error(f"Failed to delete proxy {port}: {str(e)}")
+        raise HTTPException(status_code=500, detail=str(e))
+
 @router.post("/proxies")
 async def create_proxy(request:ProxyPost):
     global sub_mgr,proxy_lock
     logger.info(f"request: {request}")
+    proxy_mgr = None
     async with proxy_lock:
         if request.auto:
             porxy_port = await find_free_port((sub_mgr.sub.start_port, sub_mgr.sub.start_port + 10000))
             controler_port = await find_free_port((porxy_port + 1, porxy_port + 10001))
-        else:
+        elif request.port:
             porxy_port = request.port
-            porxy_port_is_using = await port_is_using(porxy_port)
             proxy_mgr = sub_mgr.get_proxy_manager(porxy_port)
-            if porxy_port_is_using and proxy_mgr.running:
-                return {'err': 0, "msg": f"已开启,跳过 {porxy_port} "}
+            if proxy_mgr and proxy_mgr.running:
+                # return {'err': 0, "msg": f"已开启,跳过 {porxy_port} ", "data": await get_proxy_response(porxy_port)}
+                return ProxyPostResponse(err=0, msg=f"已开启,跳过 {porxy_port} ", data=await get_proxy_response(porxy_port))
+            porxy_port_is_using = await port_is_using(porxy_port)
             controler_port = request.port + 1
             if porxy_port_is_using:
-                return {"err": 1, "msg": f"porxy_port={porxy_port} 端口已被占用"}
+                # return ProxyPostResponse(err=1, msg=f"porxy_port={porxy_port} 端口已被占用")
+                raise HTTPException(status_code=400, detail=ProxyPostResponse(err=1, msg=f"porxy_port={porxy_port} 端口已被占用"))
             if await port_is_using(controler_port):
-                return {"err": 1, "msg": f"controler_port={controler_port} 端口已被占用"}
+                # return {"err": 1, "msg": f"controler_port={controler_port} 端口已被占用"}
+                # return ProxyPostResponse(err=1, msg=f"controler_port={controler_port} 端口已被占用")
+                raise HTTPException(status_code=400, detail=ProxyPostResponse(err=1, msg=f"controler_port={controler_port} 端口已被占用"))
+        else:
+            # return ProxyPostResponse(err=1, msg="port 或 auto 必须有一个")
+            raise HTTPException(status_code=400, detail=ProxyPostResponse(err=1, msg="port 或 auto 必须有一个"))
         await sub_mgr.create_custom_config(porxy_port, controler_port)
         await sub_mgr.start_proxy(porxy_port)
         await auto_select_proxy(porxy_port)
-    return {"err": 0, "msg": sub_mgr.sub}
+        # return {"err": 0, "msg": "ok", "data": await get_proxy_response(porxy_port)}
+        res = ProxyPostResponse(err=0, msg="ok", data=await get_proxy_response(porxy_port))
+        logger.info(f"{res}")
+        return res
+    # return ProxyPostResponse(err=1, msg="proxy_lock error", data=sub_mgr.sub)
+    return HTTPException(status_code=500, detail=ProxyPostResponse(err=1, msg="proxy_lock error", data=sub_mgr.sub))
 
 @router.get("/subs")
 async def get_subscriptions():

+ 6 - 1
ui/backend/src/services/proxy_manager.py

@@ -76,7 +76,12 @@ class ProxyManager:
     
     def get_process_info(self):
         """获取代理进程的运行信息"""
-        return self.process_manager.processes.get(self.process_name)
+        info = self.process_manager.processes.get(self.process_name)
+        return {
+                    "log_file": info.get("log_file"),
+                    "pid": info.get("pid"),
+                    "start_time": info.get("start_time"),
+                }
 
     async def request_providers_proxies(self):
         """请求所有代理"""

+ 3 - 1
ui/backend/src/services/subscription_manager.py

@@ -42,10 +42,12 @@ class SubscriptionManager:
         """
         Path(self.sub.file).unlink(missing_ok=True)
         
-    async def create_custom_config(self, port: int, controler_port:int = None) -> Path:
+    async def create_custom_config(self, port: int, controler_port:int = None, skip_exist:bool=True) -> Path:
         """
         基于源订阅文件,创建自定义配置文件
         """
+        if skip_exist and self.sub.proxies.get(port):
+            return self.config
         config = {}
         controler_port = controler_port or port + 1
         temp_path = Path(self.sub.temp_dir) / f"{port}.yaml"

+ 2 - 1
ui/backend/utils/process_mgr.py

@@ -127,7 +127,8 @@ class ProcessManager:
                     "process": process,
                     "log_file": log_file,
                     "start_time": time.time(),
-                    "log_fd": log_fd
+                    "log_fd": log_fd,
+                    "pid": process.pid
                 }
 
                 logger.info(f"Started process {name} (PID: {process.pid})")

+ 4 - 5
ui/fontend/src/App.vue

@@ -1,17 +1,16 @@
-<script setup lang="ts">
-import Proxy from './components/Proxy.vue'
-</script>
-
 <template>
     <div class="common-layout">
     <el-container>
-      <el-header></el-header>
+        <el-aside width="200px"><Menu /></el-aside>
       <el-main >    
         <Proxy />
       </el-main>
     </el-container>
   </div>
 </template>
+<script setup lang="ts">
+import Menu from './components/Menu.vue';
+</script>
 
 <style scoped>
 </style>

+ 21 - 0
ui/fontend/src/api-types.ts

@@ -0,0 +1,21 @@
+export interface ProxyResponse {
+    name: string
+    port: number
+    reachable: boolean
+    file_path: string
+    mgr_url: string
+    process_info?: {
+        pid?: number
+        start_time?: number
+        log_file?: string
+    }
+    ping?: {
+        [key: string]: number
+    }
+}
+
+export interface ProxyPostResponse {
+    err: number
+    msg: string
+    data: ProxyResponse
+}

+ 0 - 2
ui/fontend/src/auto-imports.d.ts

@@ -63,8 +63,6 @@ declare global {
   const watchEffect: typeof import('vue')['watchEffect']
   const watchPostEffect: typeof import('vue')['watchPostEffect']
   const watchSyncEffect: typeof import('vue')['watchSyncEffect']
-
-  
 }
 // for type re-export
 declare global {

+ 6 - 0
ui/fontend/src/components.d.ts

@@ -8,6 +8,7 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    ElAside: typeof import('element-plus/es')['ElAside']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
@@ -17,12 +18,17 @@ declare module 'vue' {
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElMain: typeof import('element-plus/es')['ElMain']
+    ElMenu: typeof import('element-plus/es')['ElMenu']
+    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
     ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
+    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     ElTag: typeof import('element-plus/es')['ElTag']
+    Menu: typeof import('./components/Menu.vue')['default']
     Proxy: typeof import('./components/Proxy.vue')['default']
     ProxyPool: typeof import('./components/ProxyPool.vue')['default']
   }

+ 52 - 0
ui/fontend/src/components/Menu.vue

@@ -0,0 +1,52 @@
+<template>
+    <div>
+        <el-radio-group v-model="isCollapse" style="margin-bottom: 20px">
+      <el-radio-button :value="false">expand</el-radio-button>
+      <el-radio-button :value="true">collapse</el-radio-button>
+    </el-radio-group>
+    <el-menu
+      default-active="2"
+      class="el-menu-vertical-demo"
+      :collapse="isCollapse"
+      @open="handleOpen"
+      @close="handleClose"
+    >
+      <el-menu-item index="1">
+          <el-icon><HomeFilled /></el-icon>
+        <template #title>主页</template>
+      </el-menu-item>
+      <el-menu-item index="2">
+        <el-icon><Connection /></el-icon>
+        <template #title>代理</template>
+      </el-menu-item>
+      <el-menu-item index="3" disabled>
+        <el-icon><document /></el-icon>
+        <template #title>Navigator Three</template>
+      </el-menu-item>
+      <el-menu-item index="4">
+        <el-icon><setting /></el-icon>
+        <template #title>Navigator Four</template>
+      </el-menu-item>
+    </el-menu>
+    </div>
+  </template>
+  
+<script lang="ts" setup>
+import { ref } from 'vue'
+import Proxy from '@/components/Proxy.vue'
+
+const isCollapse = ref(false)
+const handleOpen = (key: string, keyPath: string[]) => {
+console.log(key, keyPath)
+}
+const handleClose = (key: string, keyPath: string[]) => {
+console.log(key, keyPath)
+}
+</script>
+
+<style>
+.el-menu-vertical-demo:not(.el-menu--collapse) {
+width: 200px;
+min-height: 400px;
+}
+</style>

+ 73 - 25
ui/fontend/src/components/ProxyPool.vue

@@ -9,14 +9,18 @@
         <el-table-column prop="port" label="端口" width="100" align="center" />
         <el-table-column label="状态" width="120" align="center">
           <template #default="{ row }">
-            <el-tag :type="row.reachable ? 'success' : 'danger'" effect="dark">
+            <el-tag
+              :type="row.reachable ? 'success' : 'danger'"
+              effect="dark"
+              @click="row.reachable ? showDetail(row) : null"
+              :style="{ cursor: row.reachable ? 'pointer' : 'default' }">
               {{ row.reachable ? '在线' : '离线' }}
             </el-tag>
           </template>
         </el-table-column>
         <el-table-column label="管理地址">
           <template #default="{ row }">
-            <el-link v-if="row.reachable" :href="row.mgr_url" target="_blank">
+            <el-link v-if="row.reachable && row.mgr_url" :href="row.mgr_url || ''" target="_blank">
                 <el-icon><Link /></el-icon>访问
             </el-link>
             
@@ -24,38 +28,50 @@
         </el-table-column>
         <el-table-column label="操作" width="220" align="center">
           <template #default="{ row }">
-            <el-button 
-              type="" 
-              size="small" 
-              @click="openProxy(row)"
-              :disabled="row.reachable">
-              开启
+            <el-button
+              :type="row.reachable ? 'danger' : 'primary'"
+              size="small"
+              @click="row.reachable ? closeProxy(row) : openProxy(row)">
+              {{ row.reachable ? '关闭' : '开启' }}
             </el-button>
-            <el-button 
-              type="warning" 
-              size="small" 
-              @click="showDetail(row)"
-              :disabled="row.reachable">
-              详情
+            <el-button
+              type="danger"
+              size="small"
+              @click="deleteProxy(row)">
+              删除
             </el-button>
           </template>
         </el-table-column>
       </el-table>
   
       <!-- 详情对话框 -->
-      <el-dialog v-model="detailVisible" :title="`${selectedProxy?.name} 详情`" width="30%">
-        <el-descriptions :column="1" border>
-          <el-descriptions-item label="进程信息">
-            <pre>{{ selectedProxy?.process_info || '无进程信息' }}</pre>
-          </el-descriptions-item>
-        </el-descriptions>
-      </el-dialog>
+<el-dialog v-model="detailVisible" :title="selectedProxy ? `${selectedProxy.name} 详情` : '代理详情'" width="80%">
+  <el-descriptions :column="1" border>
+    <el-descriptions-item label="进程信息">
+      <div v-if="selectedProxy && selectedProxy.process_info" class="process-info">
+        <div class="info-item">
+          <span class="info-label">日志文件:</span>
+          <span class="info-value">{{ selectedProxy.process_info.log_file }}</span>
+        </div>
+        <div class="info-item">
+          <span class="info-label">进程ID:</span>
+          <span class="info-value">{{ selectedProxy.process_info.pid }}</span>
+        </div>
+        <div class="info-item">
+          <span class="info-label">启动时间:</span>
+          <span class="info-value">{{ formatTime(selectedProxy.process_info.start_time) }}</span>
+        </div>
+      </div>
+      <span v-else>无进程信息</span>
+    </el-descriptions-item>
+  </el-descriptions>
+</el-dialog>
     </div>
   </template>
   
 <script setup lang="ts">
 import { ref } from 'vue'
-import type { ProxyResponse } from '@/types'  // 根据你的类型定义调整路径
+import type { ProxyResponse, ProxyPostResponse } from '../api-types'
 
 const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || ''
 const proxyList = ref<ProxyResponse[]>([])
@@ -85,18 +101,50 @@ detailVisible.value = true
 
 const openProxy = async (proxy: ProxyResponse) => {
 try {
-    const response = await fetch(`${apiBaseUrl}/proxy/open`, {
+    const response = await fetch(`${apiBaseUrl}/proxy/proxies`, {
     method: 'POST',
     headers: {
         'Content-Type': 'application/json',
     },
-    body: JSON.stringify({ name: proxy.name }),
+    body: JSON.stringify({ port: proxy.port }),
     }) 
+    if (!response.ok) throw new Error('开启代理失败')
+    const result: ProxyPostResponse = await response.json()
+    proxy.reachable = true
+    proxy.mgr_url = result.data.mgr_url
 } catch (error) {
     console.error(error)
 }
 
 }
+const closeProxy = async (proxy: ProxyResponse) => {
+    try {
+        const response = await fetch(`${apiBaseUrl}/proxy/proxies/${proxy.port}`, {
+            method: 'DELETE'
+        })
+        if (!response.ok) throw new Error('关闭代理失败')
+        proxy.reachable = false
+    } catch (error) {
+        console.error(error)
+    }
+}
+
+const deleteProxy = async (proxy: ProxyResponse) => {
+    try {
+        const response = await fetch(`${apiBaseUrl}/proxy/proxies/${proxy.port}`, {
+            method: 'DELETE'
+        })
+        if (!response.ok) throw new Error('删除代理失败')
+        await fetchProxyList()
+    } catch (error) {
+        console.error(error)
+    }
+}
+const formatTime = (timestamp: number| undefined): string => {
+  if (!timestamp) return '';
+  const date = new Date(timestamp * 1000); // 转换为毫秒
+  return date.toLocaleString(); // 根据本地格式返回日期和时间
+};
 // 初始化时自动加载一次
 fetchProxyList()
 </script>
@@ -115,4 +163,4 @@ max-height: 200px;
 overflow-y: auto;
 margin: 0;
 }
-</style>
+</style>

+ 0 - 11
ui/fontend/src/types.ts

@@ -1,11 +0,0 @@
-export interface ProxyResponse {
-    name: string
-    port: number
-    reachable: boolean
-    file_path: string
-    mgr_url: string
-    process_info?: {
-      pid?: number
-      status?: string
-    }
-  }

+ 9 - 2
ui/fontend/tsconfig.app.json

@@ -4,11 +4,18 @@
     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 
     /* Linting */
+    "module": "ESNext",
+    "target": "ESNext", // 目标也设置为 esnext
+    "moduleResolution": "node", // 模块解析策略
     "strict": true,
+    "esModuleInterop": true, // 允许 CommonJS 和 ES 模块互操作
+    "skipLibCheck": true, // 跳过库文件的类型检查
     "noUnusedLocals": true,
     "noUnusedParameters": true,
     "noFallthroughCasesInSwitch": true,
-    "noUncheckedSideEffectImports": true
+    "noUncheckedSideEffectImports": true,
+    "types": ["vite/client"] // 添加 Vite 的类型支持
   },
-  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "exclude": ["node_modules"] // 排除的文件
 }

+ 5 - 1
ui/fontend/tsconfig.json

@@ -3,5 +3,9 @@
   "references": [
     { "path": "./tsconfig.app.json" },
     { "path": "./tsconfig.node.json" }
-  ]
+  ],
+  "compilerOptions": {
+    "module": "ESNext",
+    "moduleResolution": "bundler"
+  }
 }