Explorar o código

feat(发布代理): 将城市API配置替换为神龙产品Key

- 前端设置界面将“城市IP列表API”字段改为“神龙产品Key”
- 后端路由验证、服务层配置存储和获取逻辑相应调整
- Python代理解析器支持通过产品Key调用神龙官方API,并增强IP提取兼容性
- 移除通过API动态获取城市列表的逻辑,改为从内置CSV文件生成城市列表
Ethanfly hai 2 días
pai
achega
7baea60d17

+ 6 - 6
client/src/views/Settings/index.vue

@@ -29,9 +29,9 @@
       <el-tab-pane label="发布代理" name="publish-proxy">
         <div class="page-card">
           <el-form :model="publishProxy" label-width="150px">
-            <el-form-item label="城市IP列表API">
-              <el-input v-model="publishProxy.cityApiUrl" placeholder="填写返回城市+代理列表的 API 地址" style="width: 100%" />
-              <span class="form-tip">发布时按该 API 动态生成城市列表与代理IP,不再额外配置</span>
+            <el-form-item label="神龙产品Key">
+              <el-input v-model="publishProxy.productKey" placeholder="填写神龙产品Key" style="width: 100%" />
+              <span class="form-tip">发布时使用该 Key 调用神龙代理获取对应地区的IP</span>
             </el-form-item>
 
             <el-form-item>
@@ -127,7 +127,7 @@ const settings = reactive({
 });
 
 const publishProxy = reactive({
-  cityApiUrl: '',
+  productKey: '',
 });
 
 const users = ref<User[]>([]);
@@ -172,7 +172,7 @@ async function loadSettings() {
 async function loadPublishProxy() {
   try {
     const config = await request.get('/api/system/publish-proxy');
-    publishProxy.cityApiUrl = String(config.cityApiUrl || '');
+    publishProxy.productKey = String(config.productKey || '');
   } catch {
     // 错误已处理
   }
@@ -181,7 +181,7 @@ async function loadPublishProxy() {
 async function savePublishProxy() {
   try {
     await request.put('/api/system/publish-proxy', {
-      cityApiUrl: publishProxy.cityApiUrl,
+      productKey: publishProxy.productKey,
     });
     ElMessage.success('发布代理配置已保存');
     loadPublishProxy();

BIN=BIN
server/python/__pycache__/app.cpython-313.pyc


+ 41 - 14
server/python/app.py

@@ -148,18 +148,34 @@ def _test_proxy_connectivity(test_url: str, host: str, port: int, username: str
 
 
 def _resolve_shenlong_proxy(proxy_payload: dict) -> dict:
-    api_url = str(proxy_payload.get('apiUrl') or '').strip()
     test_url = 'http://myip.ipip.net'
     city = str(proxy_payload.get('city') or '').strip()
+    region_code = str(proxy_payload.get('regionCode') or '').strip()
+    api_url = str(proxy_payload.get('apiUrl') or '').strip()
+    product_key = str(proxy_payload.get('productKey') or '').strip()
 
-    if not api_url:
-        raise Exception('缺少城市IP列表API地址')
-
+    request_url = api_url
     params = {}
-    if city:
-        params['city'] = city
 
-    resp = requests.get(api_url, params=params, timeout=15)
+    if request_url:
+        if city:
+            params['city'] = city
+    else:
+        if not product_key:
+            raise Exception('缺少神龙产品Key')
+        request_url = 'https://api.shenlongip.com/getip'
+        params = {
+            'key': product_key,
+            'count': 1,
+            'protocol': 'http',
+        }
+        if city:
+            params['city'] = city
+        if region_code:
+            params['city_code'] = region_code
+            params['region_code'] = region_code
+
+    resp = requests.get(request_url, params=params, timeout=15)
     if resp.status_code >= 400:
         raise Exception(f"代理提取失败: HTTP {resp.status_code}")
 
@@ -185,18 +201,29 @@ def _resolve_shenlong_proxy(proxy_payload: dict) -> dict:
             item_city = str(item.get('city') or item.get('area') or '').strip()
             if city_filter and item_city and item_city != city_filter:
                 continue
-            ip = str(item.get('ip') or '').strip()
-            port = str(item.get('port') or '').strip()
+            ip = str(item.get('ip') or item.get('host') or item.get('proxy_ip') or '').strip()
+            port = str(item.get('port') or item.get('proxy_port') or '').strip()
             if ip and port:
                 ips.append(f"{ip}:{port}")
+            proxy = str(item.get('proxy') or item.get('ip_port') or '').strip()
+            if proxy:
+                for ip_port in _extract_ip_ports(proxy):
+                    ips.append(ip_port)
         return ips
 
     ip_ports = []
     if payload is not None:
-        if isinstance(payload, dict) and isinstance(payload.get('data'), list):
-            ip_ports = collect_ip_ports(payload.get('data'), city)
-            if city and not ip_ports:
-                ip_ports = collect_ip_ports(payload.get('data'), '')
+        if isinstance(payload, dict):
+            if isinstance(payload.get('data'), list):
+                ip_ports = collect_ip_ports(payload.get('data'), city)
+                if city and not ip_ports:
+                    ip_ports = collect_ip_ports(payload.get('data'), '')
+            elif isinstance(payload.get('list'), list):
+                ip_ports = collect_ip_ports(payload.get('list'), city)
+                if city and not ip_ports:
+                    ip_ports = collect_ip_ports(payload.get('list'), '')
+            elif payload.get('ip') and payload.get('port'):
+                ip_ports = collect_ip_ports([payload], city)
         elif isinstance(payload, list):
             ip_ports = collect_ip_ports(payload, city)
             if city and not ip_ports:
@@ -210,7 +237,7 @@ def _resolve_shenlong_proxy(proxy_payload: dict) -> dict:
     random.shuffle(ip_ports)
     candidates = ip_ports[: min(10, len(ip_ports))]
 
-    print(f"[Proxy] shenlong resolved: city={city or '-'} candidates={len(candidates)}/{len(ip_ports)}", flush=True)
+    print(f"[Proxy] shenlong resolved: city={city or '-'} regionCode={region_code or '-'} candidates={len(candidates)}/{len(ip_ports)}", flush=True)
 
     for ip_port in candidates:
         try:

+ 1 - 1
server/src/routes/system.ts

@@ -66,7 +66,7 @@ router.put(
   authenticate,
   authorize('admin'),
   [
-    body('cityApiUrl').optional().isString(),
+    body('productKey').optional().isString(),
     validateRequest,
   ],
   asyncHandler(async (req, res) => {

+ 5 - 5
server/src/services/PublishService.ts

@@ -718,7 +718,7 @@ export class PublishService {
     enabled: boolean;
     provider: 'shenlong';
     city: string;
-    apiUrl: string;
+    productKey: string;
     regionCode?: string;
     regionName?: string;
   }> {
@@ -728,10 +728,10 @@ export class PublishService {
     if (provider !== 'shenlong') return null;
 
     const rows = await this.systemConfigRepository.find({
-      where: { configKey: 'publish_proxy_city_api_url' },
+      where: { configKey: 'publish_proxy_shenlong_product_key' },
     });
-    const cityApiUrl = String(rows?.[0]?.configValue || '').trim();
-    if (!cityApiUrl) {
+    const productKey = String(rows?.[0]?.configValue || '').trim();
+    if (!productKey) {
       return null;
     }
 
@@ -739,7 +739,7 @@ export class PublishService {
       enabled: true,
       provider: 'shenlong',
       city: String(publishProxy.city || '').trim(),
-      apiUrl: cityApiUrl,
+      productKey,
       regionCode: publishProxy.regionCode ? String(publishProxy.regionCode).trim() : undefined,
       regionName: publishProxy.regionName ? String(publishProxy.regionName).trim() : undefined,
     };

+ 17 - 67
server/src/services/SystemService.ts

@@ -11,11 +11,11 @@ interface UpdateConfigParams {
 
 export interface PublishProxyAdminConfig {
   enabled: boolean;
-  cityApiUrl: string;
+  productKey: string;
 }
 
 export interface UpdatePublishProxyAdminConfig {
-  cityApiUrl?: string;
+  productKey?: string;
 }
 
 interface SystemStatus {
@@ -38,7 +38,7 @@ export class SystemService {
   async getPublicConfig(): Promise<SystemConfigType> {
     const configs = await this.configRepository.find();
     const configMap = new Map(configs.map(c => [c.configKey, c.configValue]));
-    const cityApiUrl = (configMap.get('publish_proxy_city_api_url') || '').trim();
+    const productKey = (configMap.get('publish_proxy_shenlong_product_key') || '').trim();
 
     return {
       allowRegistration: configMap.get('allow_registration') === 'true',
@@ -46,7 +46,7 @@ export class SystemService {
       maxUploadSize: 4096 * 1024 * 1024, // 4GB
       supportedPlatforms: AVAILABLE_PLATFORM_TYPES,
       publishProxy: {
-        enabled: Boolean(cityApiUrl),
+        enabled: Boolean(productKey),
         provider: 'shenlong',
         cities: [],
       },
@@ -66,42 +66,30 @@ export class SystemService {
     const configs = await this.configRepository.find();
     const configMap = new Map(configs.map(c => [c.configKey, c.configValue]));
 
-    const cityApiUrl = (configMap.get('publish_proxy_city_api_url') || '').trim();
+    const productKey = (configMap.get('publish_proxy_shenlong_product_key') || '').trim();
 
     return {
-      enabled: Boolean(cityApiUrl),
-      cityApiUrl,
+      enabled: Boolean(productKey),
+      productKey,
     };
   }
 
   async updatePublishProxyAdminConfig(params: UpdatePublishProxyAdminConfig): Promise<void> {
-    if (params.cityApiUrl !== undefined) {
-      await this.setConfig('publish_proxy_city_api_url', String(params.cityApiUrl || '').trim());
+    if (params.productKey !== undefined) {
+      await this.setConfig('publish_proxy_shenlong_product_key', String(params.productKey || '').trim());
     }
   }
 
   async getPublishProxyCitiesFromApi(): Promise<string[]> {
-    const configs = await this.configRepository.find({
-      where: { configKey: 'publish_proxy_city_api_url' },
-    });
-    const cityApiUrl = (configs?.[0]?.configValue || '').trim();
-    if (!cityApiUrl) return [];
-
-    let responseText = '';
-    try {
-      const response = await fetch(cityApiUrl, { signal: AbortSignal.timeout(15000) });
-      responseText = await response.text();
-    } catch {
-      return [];
-    }
-
-    try {
-      const parsed = JSON.parse(responseText);
-      const cities = this.extractCitiesFromProxyApiResponse(parsed);
-      return Array.from(new Set(cities)).filter(Boolean).sort((a, b) => a.localeCompare(b, 'zh-CN'));
-    } catch {
-      return [];
+    const regions = await this.regionService.getChinaRegionsFromCsv();
+    const citySet = new Set<string>();
+    for (const province of regions) {
+      for (const city of province.children || []) {
+        const name = String(city.label || '').trim();
+        if (name) citySet.add(name);
+      }
     }
+    return Array.from(citySet).sort((a, b) => a.localeCompare(b, 'zh-CN'));
   }
 
   async getPublishProxyRegionsFromCsv(): Promise<ChinaRegionOption[]> {
@@ -134,42 +122,4 @@ export class SystemService {
       await this.configRepository.save({ configKey: key, configValue: value });
     }
   }
-
-  private extractCitiesFromProxyApiResponse(payload: any): string[] {
-    const result: string[] = [];
-
-    const pushCity = (value: any) => {
-      const city = String(value || '').trim();
-      if (city) result.push(city);
-    };
-
-    if (Array.isArray(payload)) {
-      for (const item of payload) {
-        if (typeof item === 'string') continue;
-        if (item && typeof item === 'object') {
-          pushCity((item as any).city ?? (item as any).name ?? (item as any).fullname);
-        }
-      }
-      return result;
-    }
-
-    if (!payload || typeof payload !== 'object') return result;
-
-    if (Array.isArray((payload as any).cities)) {
-      for (const c of (payload as any).cities) pushCity(c);
-      return result;
-    }
-
-    const data = (payload as any).data;
-    if (Array.isArray(data)) {
-      for (const item of data) {
-        if (typeof item === 'string') continue;
-        if (item && typeof item === 'object') {
-          pushCity((item as any).city ?? (item as any).name ?? (item as any).fullname);
-        }
-      }
-    }
-
-    return result;
-  }
 }