Sfoglia il codice sorgente

添加 mihomo get proxy 路由

mrh 1 anno fa
parent
commit
b061a92664

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

@@ -1,14 +1,16 @@
 from datetime import datetime
 from datetime import datetime
-from typing import List, Optional
+from typing import Dict, List, Optional
 from sqlmodel import SQLModel, Field,Session,select,Relationship
 from sqlmodel import SQLModel, Field,Session,select,Relationship
 from sqlalchemy.engine import Engine
 from sqlalchemy.engine import Engine
 from sqlalchemy.dialects.postgresql import JSON
 from sqlalchemy.dialects.postgresql import JSON
 from pydantic import BaseModel
 from pydantic import BaseModel
 import yaml
 import yaml
 from database.engine import get_session,create_engine,engine
 from database.engine import get_session,create_engine,engine
+from config.logu import logger
 
 
 class SubscriptFile(SQLModel, table=True):
 class SubscriptFile(SQLModel, table=True):
     id: Optional[int] = Field(default=None, primary_key=True)
     id: Optional[int] = Field(default=None, primary_key=True)
+    name: str = Field()
     url: str = Field(index=True)
     url: str = Field(index=True)
     file_path: str = Field()
     file_path: str = Field()
     updated_at: datetime = Field(default_factory=datetime.now)
     updated_at: datetime = Field(default_factory=datetime.now)
@@ -36,43 +38,66 @@ class SubscriptionManager:
     def __init__(self, db:Engine=None):
     def __init__(self, db:Engine=None):
         self.engine:Engine = db or engine
         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:
         with Session(self.engine) as session:
             exist_sub = session.exec(select(SubscriptFile).where(SubscriptFile.url == sub_model.url)).first()
             exist_sub = session.exec(select(SubscriptFile).where(SubscriptFile.url == sub_model.url)).first()
             if exist_sub and not overwrite:
             if exist_sub and not overwrite:
+                logger.info(f"{sub_model.url} already exist, skip add")
                 return exist_sub
                 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:
             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.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]:
     def get_subscription_meta(self) -> List[SubscriptFile]:
         with Session(self.engine) as session:
         with Session(self.engine) as session:
             return session.exec(select(SubscriptFile)).all()
             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:
         with open(sub_model.file_path, "r",encoding='utf-8') as f:
             sub_yaml = yaml.safe_load(f)
             sub_yaml = yaml.safe_load(f)
         groups = sub_yaml.get("proxy-groups", [])
         groups = sub_yaml.get("proxy-groups", [])
@@ -81,7 +106,17 @@ class SubscriptionManager:
         name = groups[0].get("name", "")
         name = groups[0].get("name", "")
         if not name:
         if not name:
             raise ValueError("subscription file is not valid")
             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(
             # ret.append(
         #     SubscriptionResponse(
         #     SubscriptionResponse(
         #         file_name=Path(sub.file_path).name,
         #         file_name=Path(sub.file_path).name,

+ 2 - 0
backend/main.py

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

+ 75 - 23
backend/routers/mihomo.py

@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
 from pydantic import BaseModel
 from pydantic import BaseModel
 from typing import Dict, List, Optional
 from typing import Dict, List, Optional
 import httpx
 import httpx
+from sqlmodel import Session, select
 import yaml
 import yaml
 from config.logu import logger, get_logger
 from config.logu import logger, get_logger
 from config.settings import settings
 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 routers.subscriptions import list_subscriptions,SubscriptionResponse
 from utils.mihomo_service import port_is_using,find_free_port
 from utils.mihomo_service import port_is_using,find_free_port
 from utils.sub import update_config
 from utils.sub import update_config
+from database.models.subscription import SubscriptionManager,SubscriptFile,MihomoMeta
+
 # 初始化全局变量来保存进程池
 # 初始化全局变量来保存进程池
 POOL = None
 POOL = None
 processes = []
 processes = []
@@ -34,6 +37,7 @@ class ProcessInfo(BaseModel):
 class MihomoResponse(MihomoBatchRequest):
 class MihomoResponse(MihomoBatchRequest):
     error: int = 0
     error: int = 0
     detail: Optional[Dict] = None
     detail: Optional[Dict] = None
+
 async def start_mihomo(bin_exe: str, config_yaml_path: str):
 async def start_mihomo(bin_exe: str, config_yaml_path: str):
     global POOL
     global POOL
     process = await asyncio.create_subprocess_exec(
     process = await asyncio.create_subprocess_exec(
@@ -50,27 +54,75 @@ async def stop_mihomo(process: asyncio.subprocess.Process):
     process.terminate()
     process.terminate()
     await process.wait()
     await process.wait()
     processes.remove(process)  
     processes.remove(process)  
-@mihomo_router.post("/")
+
+@mihomo_router.post("/start")
 async def post_start_mihomo(request: MihomoBatchRequest):
 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
 POST /subscriptions
 {
 {
@@ -67,99 +6,51 @@ POST /subscriptions
     "http://sub.sub.sub.subsub123456789.com/answer/land?token=a7cbdde987a58068b82e52d57ee5eecd",
     "http://sub.sub.sub.subsub123456789.com/answer/land?token=a7cbdde987a58068b82e52d57ee5eecd",
     "https://www.yfjc.xyz/api/v1/client/subscribe?token=b74f2207492053926f7511a8e474048f",
     "https://www.yfjc.xyz/api/v1/client/subscribe?token=b74f2207492053926f7511a8e474048f",
     "http://subscr.xpoti.com/v3/subscr?id=90bd6ff1da374a89b4ba763f354f20bf"
     "http://subscr.xpoti.com/v3/subscr?id=90bd6ff1da374a89b4ba763f354f20bf"
-  ]
+  ],
+  "overwrite": true,
 }
 }
 
 
 Response:
 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):
 class SubscriptionBatchRequest(BaseModel):
     urls: List[str]
     urls: List[str]
+    overwrite: bool = False
 
 
 class SubscriptionResponse(BaseModel):
 class SubscriptionResponse(BaseModel):
     file_name: str
     file_name: str
@@ -53,24 +54,41 @@ async def process_url(url: str, save_path) -> SubscriptFile:
 async def add_subscriptions(sub: SubscriptionBatchRequest) -> List[SubscriptFile]:
 async def add_subscriptions(sub: SubscriptionBatchRequest) -> List[SubscriptFile]:
     """批量更新订阅链接"""
     """批量更新订阅链接"""
     logger.info(f"开始批量更新订阅: {sub.urls}")
     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 = []
     tasks = []
     # 并发处理所有URL
     # 并发处理所有URL
     for url in sub.urls:
     for url in sub.urls:
         save_path = settings.PATH_SUBSCRIPTION_DIR / f"{hashlib.md5(url.encode()).hexdigest()[:8]}.yaml"
         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的异步任务添加到任务列表中
         # 将每个URL的异步任务添加到任务列表中
         tasks.append(process_url(url, save_path))
         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_results = []
-    db = SubscriptionManager()
     for result in results:
     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
     return db_results
 
 
 @router.get("/")
 @router.get("/")
-async def list_subscriptions() ->List[SubscriptionResponse]:
+async def list_subscriptions() ->List[SubscriptFile]:
     ret = []
     ret = []
     db = SubscriptionManager()
     db = SubscriptionManager()
     db_sub_models = db.get_subscription_meta()
     db_sub_models = db.get_subscription_meta()
+    return db_sub_models