ソースを参照

添加 mihomo get proxy 路由

mrh 1 年間 前
コミット
b061a92664

+ 61 - 26
backend/database/models/subscription.py

@@ -1,14 +1,16 @@
 from datetime import datetime
-from typing import List, Optional
+from typing import Dict, List, Optional
 from sqlmodel import SQLModel, Field,Session,select,Relationship
 from sqlalchemy.engine import Engine
 from sqlalchemy.dialects.postgresql import JSON
 from pydantic import BaseModel
 import yaml
 from database.engine import get_session,create_engine,engine
+from config.logu import logger
 
 class SubscriptFile(SQLModel, table=True):
     id: Optional[int] = Field(default=None, primary_key=True)
+    name: str = Field()
     url: str = Field(index=True)
     file_path: str = Field()
     updated_at: datetime = Field(default_factory=datetime.now)
@@ -36,43 +38,66 @@ class SubscriptionManager:
     def __init__(self, db:Engine=None):
         self.engine:Engine = db or engine
 
-    def add_subscription_meta(self, sub_model: SubscriptFile, overwrite:bool=False):
+    def add_subscription_meta(self, sub_model: SubscriptFile, proxies, overwrite:bool=False):
         with Session(self.engine) as session:
             exist_sub = session.exec(select(SubscriptFile).where(SubscriptFile.url == sub_model.url)).first()
             if exist_sub and not overwrite:
+                logger.info(f"{sub_model.url} already exist, skip add")
                 return exist_sub
 
-            name, groups = self._check_valid(sub_model)
-            
-            # 如果存在且需要覆盖,则删除旧的 MihomoMeta 记录
+            logger.info(f"exist_sub {exist_sub} overwrite {overwrite} proxies {proxies}")
             if exist_sub and overwrite:
-                session.exec(select(MihomoMeta).where(MihomoMeta.subscript_file_id == exist_sub.id)).delete()
-                session.delete(exist_sub)
-                session.commit()
-
-            # 添加新的 SubscriptFile
-            session.add(sub_model)
-            session.commit()
-            session.refresh(sub_model)
-
-            # 添加 MihomoMeta 记录
-            for group in groups:
-                miho = MihomoMeta(
-                    provider_name=name,
-                    proxy_name=group["name"],
-                    subscript_file_id=sub_model.id  # 使用 sub_model.id
-                )
-                session.add(miho)
+                # 删除与 exist_sub 相关的 MihomoMeta 记录
+                session.exec(select(MihomoMeta).where(MihomoMeta.subscript_file_id == exist_sub.id)).all()
+                for proxy in session.exec(select(MihomoMeta).where(MihomoMeta.subscript_file_id == exist_sub.id)).all():
+                    session.delete(proxy)
             
+            # 删除旧的 SubscriptFile 记录
+            session.delete(exist_sub)
             session.commit()
-            session.refresh(sub_model)
-            return sub_model
+
+        # 添加新的 SubscriptFile
+        session.add(sub_model)
+        session.commit()
+        session.refresh(sub_model)
+
+        # 添加 MihomoMeta 记录
+        for proxy in proxies:
+            miho = MihomoMeta(
+                provider_name=sub_model.name,
+                proxy_name=proxy["name"],
+                subscript_file_id=sub_model.id  # 使用 sub_model.id
+            )
+            logger.info(f"miho {miho}")
+            session.add(miho)
+        
+        session.commit()
+        session.refresh(sub_model)
+        return sub_model
         
     def get_subscription_meta(self) -> List[SubscriptFile]:
         with Session(self.engine) as session:
             return session.exec(select(SubscriptFile)).all()
     
-    def _check_valid(self, sub_model:SubscriptFile):
+    def get_proxies(self) -> List[MihomoMeta]:
+        with Session(self.engine) as session:
+            return session.exec(select(MihomoMeta)).all()
+    def get_proxies_by_provider(self) -> Dict[str, List[MihomoMeta]]:
+        """
+        返回一个字典,键是 provider_name,值是该 provider_name 对应的所有 MihomoMeta 记录列表。
+        """
+        with Session(self.engine) as session:
+            all_proxies = session.exec(select(MihomoMeta)).all()
+            
+            # 使用字典来组织数据
+            proxies_by_provider = {}
+            for proxy in all_proxies:
+                if proxy.provider_name not in proxies_by_provider:
+                    proxies_by_provider[proxy.provider_name] = []
+                proxies_by_provider[proxy.provider_name].append(proxy)
+            
+            return proxies_by_provider
+    def check_valid(self, sub_model:SubscriptFile):
         with open(sub_model.file_path, "r",encoding='utf-8') as f:
             sub_yaml = yaml.safe_load(f)
         groups = sub_yaml.get("proxy-groups", [])
@@ -81,7 +106,17 @@ class SubscriptionManager:
         name = groups[0].get("name", "")
         if not name:
             raise ValueError("subscription file is not valid")
-        return name, groups
+        proxies = sub_yaml.get("proxies", [])
+        if not proxies:
+            raise ValueError("subscription file is not valid")
+        fileter_proxies = []
+        fileter_proxies = []
+        keywords = ['流量', '套餐', '剩余', '测试']
+
+        for proxy in proxies:
+            if not any(keyword in proxy.get("name", "") for keyword in keywords):
+                fileter_proxies.append(proxy)
+        return name, groups,proxies
             # ret.append(
         #     SubscriptionResponse(
         #         file_name=Path(sub.file_path).name,

+ 2 - 0
backend/main.py

@@ -10,6 +10,7 @@ import asyncio
 import httpx
 from config.settings import settings
 from routers.subscriptions import router
+from routers.mihomo import mihomo_router
 from utils.mihomo_service import download_mihomo
 from database.engine import create_db_and_tables
 from aiomultiprocess import Pool
@@ -28,6 +29,7 @@ app = FastAPI(lifespan=lifespan)
 
 # 注册路由
 app.include_router(router, prefix="/subscriptions", tags=["订阅管理"])
+app.include_router(mihomo_router, prefix="/mihomo", tags=["mihomo管理"])
 
 # 添加健康检查路由
 @app.get("/health")

+ 75 - 23
backend/routers/mihomo.py

@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
 from pydantic import BaseModel
 from typing import Dict, List, Optional
 import httpx
+from sqlmodel import Session, select
 import yaml
 from config.logu import logger, get_logger
 from config.settings import settings
@@ -12,6 +13,8 @@ from config.app_yaml import app_yaml, Subscription
 from routers.subscriptions import list_subscriptions,SubscriptionResponse
 from utils.mihomo_service import port_is_using,find_free_port
 from utils.sub import update_config
+from database.models.subscription import SubscriptionManager,SubscriptFile,MihomoMeta
+
 # 初始化全局变量来保存进程池
 POOL = None
 processes = []
@@ -34,6 +37,7 @@ class ProcessInfo(BaseModel):
 class MihomoResponse(MihomoBatchRequest):
     error: int = 0
     detail: Optional[Dict] = None
+
 async def start_mihomo(bin_exe: str, config_yaml_path: str):
     global POOL
     process = await asyncio.create_subprocess_exec(
@@ -50,27 +54,75 @@ async def stop_mihomo(process: asyncio.subprocess.Process):
     process.terminate()
     await process.wait()
     processes.remove(process)  
-@mihomo_router.post("/")
+
+@mihomo_router.post("/start")
 async def post_start_mihomo(request: MihomoBatchRequest):
-    res_list:List[SubscriptionResponse] = await list_subscriptions()
-    provider = next((item for item in res_list if item.provider_name == request.provider_name), None)
-    if provider is None:
-        return MihomoResponse(
-            **request.model_dump(),
-            error=1,
-            detail={"msg": "provider not found"},
-        )
-    if not port_is_using(request.port):
-        return MihomoResponse(
-            **request.model_dump(),
-            error=1,
-            detail={"msg": "port is using"},
-        )
-    if mihomo_running_status.get(provider.file_name, {}).get(request.proxy_name, {}):
-        return MihomoResponse(
-            **request.model_dump(),
-            error=1,
-            detail={"msg": "mihomo is running"},
-        ) 
-    logger.info(f"start: {provider}")
-    settings.MIHOMO_BIN_PATH
+    db = SubscriptionManager()
+    
+    # 获取对应的订阅文件
+    with Session(db.engine) as session:
+        # 查找对应的订阅文件
+        sub_file = session.exec(
+            select(SubscriptFile)
+            .where(SubscriptFile.name == request.provider_name)
+        ).first()
+        
+        if not sub_file:
+            raise HTTPException(status_code=404, detail="Provider not found")
+            
+        # 查找对应的代理配置
+        proxy = session.exec(
+            select(MihomoMeta)
+            .where(MihomoMeta.provider_name == request.provider_name)
+            .where(MihomoMeta.proxy_name == request.proxy_name)
+        ).first()
+        
+        if not proxy:
+            raise HTTPException(status_code=404, detail="Proxy not found")
+            
+        # 如果端口未指定,查找可用端口
+        if not request.port:
+            request.port = find_free_port()
+        config = {}
+        # 保存临时配置文件
+        temp_path = settings.MIHOMO_TEMP_PATH / f"{request.provider_name}_{request.proxy_name}.yaml"
+        # 更新端口配置
+        config['mixed-port'] = request.port
+        config['external-controller'] = f'127.0.0.1:{request.port}'
+        
+        update_config(sub_file.file_path, config, temp_path)
+            
+        # 启动进程
+        try:
+            process = await start_mihomo(settings.MIHOMO_BIN_PATH, temp_path)
+            
+            # 更新数据库记录
+            proxy.mixed_port = request.port
+            proxy.external_controller = f'127.0.0.1:{request.port}'
+            proxy.temp_file_path = str(temp_path)
+            proxy.pid = process.pid
+            proxy.running = True
+            proxy.updated_at = datetime.now()
+            
+            session.add(proxy)
+            session.commit()
+            
+            return {
+                "provider_name": request.provider_name,
+                "proxy_name": request.proxy_name,
+                "port": request.port,
+                "pid": process.pid,
+                "status": "running"
+            }
+            
+        except Exception as e:
+            logger.error(f"Failed to start mihomo: {str(e)}")
+            raise HTTPException(status_code=500, detail=str(e))
+
+@mihomo_router.get("/")
+async def get_mihomo_running_status() -> Dict[str, List[MihomoMeta]]:
+    db = SubscriptionManager()
+    with Session(db.engine) as session:
+        session.exec(select(MihomoMeta)).all()
+
+    return db.get_proxies_by_provider()

+ 42 - 151
backend/routers/readme.md

@@ -1,65 +1,4 @@
 
-
-
-
-实际上,这些链接会调用以下参考函数去访问获得对应的 yaml 配置文件:
-```python
-import httpx
-import yaml
-from pathlib import Path
-from typing import Optional, Dict, Any
-from fastapi import HTTPException
-
-sub_url = 'https://www.yfjc.xyz/api/v1/client/subscribe?token=b74f2207492053926f7511a8e474048f'
-OUTPUT = Path(__file__).parent.parent.absolute() / "output"
-
-def get_sub(sub_url: str, save_path: str) -> Path:
-    """获取订阅文件"""
-    headers = {'User-Agent': 'clash-verge/v1.7.5'}
-    try:
-        resp = httpx.get(sub_url, headers=headers, follow_redirects=True, timeout=10)
-        resp.raise_for_status()
-    except httpx.HTTPError as e:
-        raise HTTPException(status_code=500, detail=f"订阅获取失败: {str(e)}")
-    
-    save_path = Path(save_path)
-    save_path.parent.mkdir(parents=True, exist_ok=True)
-    
-    with open(save_path, 'w', encoding='utf-8') as f:
-        f.write(resp.text)
-    return save_path
-
-def update_config(
-    read_path: Path,
-    config_update: dict,
-    save_as: Optional[Path] = None,
-) -> Path:
-    """更新配置文件"""
-    config: Dict[str, Any] = {}
-    if read_path.exists():
-        with open(read_path, 'r', encoding='utf-8') as f:
-            config = yaml.safe_load(f) or {}
-    
-    config.update(config_update)
-    
-    save_as = save_as or read_path
-    save_as.parent.mkdir(parents=True, exist_ok=True)
-    
-    with open(save_as, 'w', encoding='utf-8') as f:
-        yaml.dump(
-            config,
-            f,
-            Dumper=yaml.SafeDumper,
-            allow_unicode=True,
-            indent=2,
-            sort_keys=False
-        )
-    return save_as
-
-```
-
-因此, POST /subscriptions 应该是实现 get_sub 、 save_path 的操作。 
-
 ```
 POST /subscriptions
 {
@@ -67,99 +6,51 @@ POST /subscriptions
     "http://sub.sub.sub.subsub123456789.com/answer/land?token=a7cbdde987a58068b82e52d57ee5eecd",
     "https://www.yfjc.xyz/api/v1/client/subscribe?token=b74f2207492053926f7511a8e474048f",
     "http://subscr.xpoti.com/v3/subscr?id=90bd6ff1da374a89b4ba763f354f20bf"
-  ]
+  ],
+  "overwrite": true,
 }
 
 Response:
 
 ```
-
-具体 get_sub 得到的yaml 文件类似如下,当我调用API接口时, GET /subscriptions 应该可以返回所有 yaml 文件的 json 格式:
-```yaml
-mixed-port: 9360
-allow-lan: false
-bind-address: '*'
-mode: rule
-log-level: info
-external-controller: 127.0.0.1:9361
-dns:
-  enable: true
-  ipv6: false
-  default-nameserver: [223.5.5.5, 119.29.29.29]
-  enhanced-mode: fake-ip
-  fake-ip-range: 198.18.0.1/16
-proxies:
-    - { name: '剩余流量:855.2 GB', server: 163.123.192.140, port: 54000, ports: 54000-54060, mport: 54000-54060, udp: true, skip-cert-verify: true, sni: www.bing.com, type: hysteria2, password: 9ecf3874-29dc-482f-a935-91d1d7c6a82a }
-    - { name: 套餐到期:长期有效, server: 163.123.192.140, port: 54000, ports: 54000-54060, mport: 54000-54060, udp: true, skip-cert-verify: true, sni: www.bing.com, type: hysteria2, password: 9ecf3874-29dc-482f-a935-91d1d7c6a82a }
-    - { name: 🇺🇸美国凤凰城专线1, server: 163.123.192.140, port: 54000, ports: 54000-54060, mport: 54000-54060, udp: true, skip-cert-verify: true, sni: www.bing.com, type: hysteria2, password: 9ecf3874-29dc-482f-a935-91d1d7c6a82a }
-proxy-groups:
-    - { name: 一分机场, type: select, proxies: [自动选择, 故障转移, '剩余流量:855.2 GB', 套餐到期:长期有效, 🇺🇸美国凤凰城专线1, 🇺🇸美国凤凰城专线2, 🇦🇺澳大利亚悉尼, 🇦🇺澳大利亚悉尼2, 🇳🇱荷兰专线, 🇯🇵日本专线, 🇯🇵日本专线3, 🇯🇵日本专线4, 🇸🇬新加坡专线, 🇸🇬新加坡专线2, 🇺🇸美国洛杉矶专线, 🇺🇸美国洛杉矶专线2, 🇺🇸美国洛杉矶专线3, 🇺🇸美国洛杉矶专线4, 🇰🇷韩国三网专线, 🇰🇷韩国三网专线2, 🇰🇷韩国三网专线3, 🇰🇷韩国专线2, 🇧🇷巴西专线, 🇸🇬亚马逊新加坡, 🇮🇳印度孟买专线2, 🇺🇸美国高速01, 🇺🇸美国高速02, '🇳🇱荷兰Eygelshoven | BT下载-0.1倍率', 🇭🇰香港1号, 🇭🇰香港2号, 🇭🇰香港3号, 🇭🇰香港4号, 🇺🇸美国1号-0.1倍, 🇺🇸美国2号-0.1倍, '🇳🇱荷兰Eygelshoven | BT下载-0.1倍', 🇹🇼台湾, 🇫🇷法国巴黎, 🇫🇷法国巴黎2, 🇫🇷法国马赛, 🇳🇱荷兰阿姆斯特丹, 🇩🇪德国法兰克福2, 🇬🇧英国伦敦, 🇬🇧英国伦敦2, 🇮🇳印度海得拉巴, 🇯🇵日本, 🇯🇵日本2] }
-    - { name: 自动选择, type: url-test, proxies: ['剩余流量:855.2 GB', 套餐到期:长期有效, 🇺🇸美国凤凰城专线1, 🇺🇸美国凤凰城专线2, 🇦🇺澳大利亚悉尼, 🇦🇺澳大利亚悉尼2, 🇳🇱荷兰专线, 🇯🇵日本专线, 🇯🇵日本专线3, 🇯🇵日本专线4, 🇸🇬新加坡专线, 🇸🇬新加坡专线2, 🇺🇸美国洛杉矶专线, 🇺🇸美国洛杉矶专线2, 🇺🇸美国洛杉矶专线3, 🇺🇸美国洛杉矶专线4, 🇰🇷韩国三网专线, 🇰🇷韩国三网专线2, 🇰🇷韩国三网专线3, 🇰🇷韩国专线2, 🇧🇷巴西专线, 🇸🇬亚马逊新加坡, 🇮🇳印度孟买专线2, 🇺🇸美国高速01, 🇺🇸美国高速02, '🇳🇱荷兰Eygelshoven | BT下载-0.1倍率', 🇭🇰香港1号, 🇭🇰香港2号, 🇭🇰香港3号, 🇭🇰香港4号, 🇺🇸美国1号-0.1倍, 🇺🇸美国2号-0.1倍, '🇳🇱荷兰Eygelshoven | BT下载-0.1倍', 🇹🇼台湾, 🇫🇷法国巴黎, 🇫🇷法国巴黎2, 🇫🇷法国马赛, 🇳🇱荷兰阿姆斯特丹, 🇩🇪德国法兰克福2, 🇬🇧英国伦敦, 🇬🇧英国伦敦2, 🇮🇳印度海得拉巴, 🇯🇵日本, 🇯🇵日本2], url: 'http://www.gstatic.com/generate_204', interval: 86400 }
-    - { name: 故障转移, type: fallback, proxies: ['剩余流量:855.2 GB', 套餐到期:长期有效, 🇺🇸美国凤凰城专线1, 🇺🇸美国凤凰城专线2, 🇦🇺澳大利亚悉尼, 🇦🇺澳大利亚悉尼2, 🇳🇱荷兰专线, 🇯🇵日本专线, 🇯🇵日本专线3, 🇯🇵日本专线4, 🇸🇬新加坡专线, 🇸🇬新加坡专线2, 🇺🇸美国洛杉矶专线, 🇺🇸美国洛杉矶专线2, 🇺🇸美国洛杉矶专线3, 🇺🇸美国洛杉矶专线4, 🇰🇷韩国三网专线, 🇰🇷韩国三网专线2, 🇰🇷韩国三网专线3, 🇰🇷韩国专线2, 🇧🇷巴西专线, 🇸🇬亚马逊新加坡, 🇮🇳印度孟买专线2, 🇺🇸美国高速01, 🇺🇸美国高速02, '🇳🇱荷兰Eygelshoven | BT下载-0.1倍率', 🇭🇰香港1号, 🇭🇰香港2号, 🇭🇰香港3号, 🇭🇰香港4号, 🇺🇸美国1号-0.1倍, 🇺🇸美国2号-0.1倍, '🇳🇱荷兰Eygelshoven | BT下载-0.1倍', 🇹🇼台湾, 🇫🇷法国巴黎, 🇫🇷法国巴黎2, 🇫🇷法国马赛, 🇳🇱荷兰阿姆斯特丹, 🇩🇪德国法兰克福2, 🇬🇧英国伦敦, 🇬🇧英国伦敦2, 🇮🇳印度海得拉巴, 🇯🇵日本, 🇯🇵日本2], url: 'http://www.gstatic.com/generate_204', interval: 7200 }
-rules:
-    - 'DOMAIN-SUFFIX,services.googleapis.cn,一分机场'
-    - 'DOMAIN-SUFFIX,xn--ngstr-lra8j.com,一分机场'
-    - 'DOMAIN,safebrowsing.urlsec.qq.com,DIRECT'
-
+GET /mihomo
+```
+{
+  "FSCloud": [
+    {
+      "proxy_name": "美国003 - hysteria2",
+      "external_controller": null,
+      "pid": null,
+      "updated_at": "2025-01-30T10:04:02.399640",
+      "subscript_file_id": 10,
+      "provider_name": "FSCloud",
+      "mixed_port": null,
+      "id": 30,
+      "temp_file_path": null,
+      "running": false
+    }
+  ],
+  "一分机场": [
+    {
+      "proxy_name": "自动选择",
+      "external_controller": null,
+      "pid": null,
+      "updated_at": "2025-01-30T09:28:33.180280",
+      "subscript_file_id": null,
+      "provider_name": "一分机场",
+      "mixed_port": null,
+      "id": 5,
+      "temp_file_path": null,
+      "running": false
+    },
+    ...
+  ],
+}
+```
+/POST /mihomo/start
+```
+{
+  "provider_name": "FSCloud",
+  "proxy_name": "美国003 - hysteria2",
+}
 ```
-PUT /subscriptions 应该是实现 update if not exists 
-GET /subscriptions 应该是实现这些链接对应的 yaml 文件的读取。
-GET /subscriptions?update=true 应该是再次调用 get_sub 、 save_path ,然后返回新的 yaml 文件
-
-
-routers\subscriptions.py  中  SUBSCRIPTIONS_FILE = settings.PATH_CONFIG_DIR / "app.yaml" 并未能恰当的保存链接的元数据,每次请求,都会提示链接存在,但是实际 output\subscriptions 目录下的 yaml 并不存在。并且 app.yaml 中也没有任何元数据信息。
-
-我提供判断链接是否存在的方法:首先要从文件层面读取 app.yaml ,得到记录的元数据再判断。或者写成一个接口函数,每次读写都要有一个完整的过程。
-
-app.yaml 存在元数据后,根据元数据的信息,判断 output\subscriptions 目录下的 yaml 是否存在,最终才能确定链接及其配置文件是否保存。最终才能返回 API 。
-
-其次,每次请求多个 POST /subscriptions "urls" 都会因为某个订阅链接失败而导致下一个链接无法保存,这是不被允许的,错误的链接你也应该通过接口告知。成功的链接也需要返回其元数据。即:urls 有3个订阅链接,你都要返回3个的最终信息。
-
-现在Response返回第2个结果是错的,它没有保存到backend\\output\\subscriptions  .yaml 文件中
-[
-  {
-    "url": "http://sub.sub.sub.subsub123456789.com/answer/land?token=a7cbdde987a58068b82e52d57ee5eecd",
-    "file_path": "g:\\code\\upwork\\zhang_crawl_bio\\local_proxy_pool\\backend\\output\\subscriptions\\subscription_d92ef962dad83e2caa45b0082db318b6.yaml",
-    "created_at": "2025-01-26T05:08:24.654698",
-    "updated_at": "2025-01-27T04:23:02.069518",
-    "last_check": "2025-01-27T04:23:02.069518",
-    "status": "inactive",
-    "traffic_used": 0,
-    "traffic_total": 0,
-    "group": null,
-    "file_exists": true,
-    "last_error": null,
-    "retry_count": 0,
-    "next_retry": null
-  },
-  {
-    "url": "https://www.yfjc.xyz/api/v1/client/subscribe?token=b74f2207492053926f7511a8e474048f",
-    "file_path": "g:\\code\\upwork\\zhang_crawl_bio\\local_proxy_pool\\backend\\output\\subscriptions\\string.yaml",
-    "created_at": "2025-01-26T21:47:39.144325",
-    "updated_at": "2025-01-26T21:47:39.144325",
-    "last_check": null,
-    "status": "active",
-    "traffic_used": 0,
-    "traffic_total": 0,
-    "group": null,
-    "file_exists": false,
-    "last_error": null,
-    "retry_count": 0,
-    "next_retry": null
-  },
-  {
-    "url": "http://subscr.xpoti.com/v3/subscr?id=90bd6ff1da374a89b4ba763f354f20bf",
-    "file_path": "",
-    "created_at": "2025-01-27T05:00:59.463719",
-    "updated_at": "2025-01-27T05:00:59.463719",
-    "last_check": null,
-    "status": "error",
-    "traffic_used": 0,
-    "traffic_total": 0,
-    "group": null,
-    "file_exists": false,
-    "last_error": "",
-    "retry_count": 0,
-    "next_retry": null
-  }
-]

+ 24 - 6
backend/routers/subscriptions.py

@@ -18,6 +18,7 @@ router = APIRouter()
 
 class SubscriptionBatchRequest(BaseModel):
     urls: List[str]
+    overwrite: bool = False
 
 class SubscriptionResponse(BaseModel):
     file_name: str
@@ -53,24 +54,41 @@ async def process_url(url: str, save_path) -> SubscriptFile:
 async def add_subscriptions(sub: SubscriptionBatchRequest) -> List[SubscriptFile]:
     """批量更新订阅链接"""
     logger.info(f"开始批量更新订阅: {sub.urls}")
+    db = SubscriptionManager()
+    subscription_meta_list = db.get_subscription_meta()
+    urls = [subscription_meta.url for subscription_meta in subscription_meta_list if subscription_meta.url ]
+    logger.info(f"exist urls {urls}")
     # 初始化任务列表
     tasks = []
     # 并发处理所有URL
     for url in sub.urls:
         save_path = settings.PATH_SUBSCRIPTION_DIR / f"{hashlib.md5(url.encode()).hexdigest()[:8]}.yaml"
+        if url in urls and not sub.overwrite:
+            logger.info(f"{url} already exist, skip add")
+            continue
         # 将每个URL的异步任务添加到任务列表中
         tasks.append(process_url(url, save_path))
-    results = await asyncio.gather(*tasks)
+    if not tasks:
+        return subscription_meta_list
+    
+    results:List[SubscriptFile] = await asyncio.gather(*tasks)
+    logger.info(f"批量更新订阅完成: {results}")
     db_results = []
-    db = SubscriptionManager()
     for result in results:
-         db_results.append(
-             db.add_subscription_meta(result)
-         )
+        try:
+            name,groups,proxies = db.check_valid(result)
+            result.name = name
+            db_results.append(
+                db.add_subscription_meta(result, proxies, overwrite=sub.overwrite)
+            )
+        except Exception as e:
+            result.error = 1
+            result.detail = {"valid": str(e)}
     return db_results
 
 @router.get("/")
-async def list_subscriptions() ->List[SubscriptionResponse]:
+async def list_subscriptions() ->List[SubscriptFile]:
     ret = []
     db = SubscriptionManager()
     db_sub_models = db.get_subscription_meta()
+    return db_sub_models