3434MAX_UPLOAD_MB = 50
3535ALLOWED_EXTENSIONS = {"zip" }
3636
37+ # 默认 GitHub 加速镜像(10 个)
38+ DEFAULT_GITHUB_MIRRORS = [
39+ {"name" : "GitHub 官方" , "url" : "https://github.com" },
40+ {"name" : "ghproxy" , "url" : "https://ghproxy.com" },
41+ {"name" : "99988866" , "url" : "https://gh.api.99988866.xyz" },
42+ {"name" : "mirror.ghproxy" , "url" : "https://mirror.ghproxy.com" },
43+ {"name" : "gh-proxy" , "url" : "https://gh-proxy.com" },
44+ {"name" : "xcxgw" , "url" : "https://gh.xcxgw.com" },
45+ {"name" : "ghps" , "url" : "https://ghps.cc" },
46+ {"name" : "d-ai workers" , "url" : "https://gh.d-ai.workers.dev" },
47+ {"name" : "llkk" , "url" : "https://gh.llkk.cc" },
48+ {"name" : "gitmirror" , "url" : "https://hub.gitmirror.com" },
49+ ]
50+
3751# Built-in marketplace demo entries. Used as fallback when no remote
3852# marketplace source is configured (or the configured source is unreachable),
3953# so the WebUI can still render the plugin market page out of the box.
@@ -1144,9 +1158,7 @@ def api_market_plugins():
11441158 continue
11451159
11461160 if using_fallback :
1147- # Deep copy the built-in demo list so callers can't mutate
1148- # the module-level constant via the response.
1149- plugins = [dict (p ) for p in BUILTIN_MARKET_PLUGINS ]
1161+ plugins = []
11501162
11511163 # Annotate installed state.
11521164 for p in plugins :
@@ -1241,6 +1253,17 @@ def api_market_install(name):
12411253 "name" : name ,
12421254 }), 400
12431255
1256+ # Apply mirror if configured and download_url is from GitHub
1257+ mirror_url = self .storage .get ("market_mirror" , "" ) or ""
1258+ if (
1259+ mirror_url
1260+ and "github.com" in download_url
1261+ and mirror_url != "https://github.com"
1262+ ):
1263+ download_url = download_url .replace (
1264+ "https://github.com" , mirror_url .rstrip ("/" )
1265+ )
1266+
12441267 # Download to data/uploads/<name>.zip
12451268 try :
12461269 import urllib .request
@@ -1264,6 +1287,167 @@ def api_market_install(name):
12641287 {"success" : False , "error" : "Plugin import failed" }
12651288 ), 400
12661289
1290+ # ── marketplace: mirrors / speedtest / readme ──────────────
1291+
1292+ @app .route ("/api/market/mirrors" )
1293+ def api_market_mirrors ():
1294+ """返回 GitHub 加速镜像列表,含用户自定义的。"""
1295+ custom = self .storage .get ("market_custom_mirrors" , []) or []
1296+ current = self .storage .get (
1297+ "market_mirror" , "https://github.com"
1298+ ) or "https://github.com"
1299+ all_mirrors = list (DEFAULT_GITHUB_MIRRORS ) + [
1300+ {"name" : m .get ("name" , "自定义" ), "url" : m .get ("url" , "" )}
1301+ for m in custom
1302+ if m .get ("url" )
1303+ ]
1304+ return jsonify ({
1305+ "mirrors" : all_mirrors ,
1306+ "current" : current ,
1307+ })
1308+
1309+ @app .route ("/api/market/mirrors/set" , methods = ["POST" ])
1310+ def api_market_mirrors_set ():
1311+ """设置当前使用的加速源。"""
1312+ data = request .get_json (silent = True ) or {}
1313+ url = (data .get ("url" ) or "" ).strip ()
1314+ if not url :
1315+ return jsonify (
1316+ {"success" : False , "error" : "url is required" }
1317+ ), 400
1318+ self .storage .set ("market_mirror" , url )
1319+ return jsonify ({"success" : True , "current" : url })
1320+
1321+ @app .route ("/api/market/mirrors/speedtest" )
1322+ def api_market_mirrors_speedtest ():
1323+ """对所有镜像进行测速,返回最快的。"""
1324+ import urllib .request
1325+
1326+ mirrors = DEFAULT_GITHUB_MIRRORS [:]
1327+ custom = self .storage .get ("market_custom_mirrors" , []) or []
1328+ for m in custom :
1329+ if m .get ("url" ):
1330+ mirrors .append (
1331+ {"name" : m .get ("name" , "自定义" ), "url" : m ["url" ]}
1332+ )
1333+
1334+ results = []
1335+ test_path = "/QtineNiko/Qtine"
1336+ for m in mirrors :
1337+ url = m ["url" ].rstrip ("/" ) + test_path
1338+ start = time .time ()
1339+ try :
1340+ req = urllib .request .Request (url , method = "HEAD" )
1341+ with urllib .request .urlopen (req , timeout = 5 ) as r :
1342+ latency = (time .time () - start ) * 1000
1343+ results .append ({
1344+ "name" : m ["name" ],
1345+ "url" : m ["url" ],
1346+ "latency" : round (latency , 0 ),
1347+ })
1348+ except Exception :
1349+ results .append ({
1350+ "name" : m ["name" ],
1351+ "url" : m ["url" ],
1352+ "latency" : 99999 ,
1353+ })
1354+
1355+ results .sort (key = lambda x : x ["latency" ])
1356+ fastest = (
1357+ results [0 ]["url" ]
1358+ if results and results [0 ]["latency" ] < 99999
1359+ else None
1360+ )
1361+ return jsonify ({"results" : results , "fastest" : fastest })
1362+
1363+ @app .route ("/api/market/mirrors/custom" , methods = ["POST" ])
1364+ def api_market_mirrors_custom_add ():
1365+ """添加自定义加速源。"""
1366+ data = request .get_json (silent = True ) or {}
1367+ name = (data .get ("name" ) or "" ).strip ()
1368+ url = (data .get ("url" ) or "" ).strip ()
1369+ if not url :
1370+ return jsonify (
1371+ {"success" : False , "error" : "url is required" }
1372+ ), 400
1373+ if not name :
1374+ name = url [:20 ]
1375+ custom = self .storage .get ("market_custom_mirrors" , []) or []
1376+ custom .append ({"name" : name , "url" : url })
1377+ self .storage .set ("market_custom_mirrors" , custom )
1378+ return jsonify ({"success" : True , "mirrors" : custom })
1379+
1380+ @app .route (
1381+ "/api/market/mirrors/custom/<int:idx>" , methods = ["DELETE" ]
1382+ )
1383+ def api_market_mirrors_custom_remove (idx ):
1384+ """删除自定义加速源。"""
1385+ custom = self .storage .get ("market_custom_mirrors" , []) or []
1386+ if idx < 0 or idx >= len (custom ):
1387+ return jsonify (
1388+ {"success" : False , "error" : "index out of range" }
1389+ ), 400
1390+ removed = custom .pop (idx )
1391+ self .storage .set ("market_custom_mirrors" , custom )
1392+ return jsonify ({"success" : True , "removed" : removed })
1393+
1394+ @app .route ("/api/market/plugins/<name>/readme" )
1395+ def api_market_plugin_readme (name ):
1396+ """获取插件 README。
1397+
1398+ 优先从市场源拉取;源不可用时返回空字符串。
1399+ """
1400+ source_url = (
1401+ self .config .get ("plugins.marketplace_url" , "" ) or ""
1402+ ).strip ()
1403+ readme = ""
1404+ if source_url :
1405+ try :
1406+ import urllib .request
1407+ import json as _json
1408+
1409+ url = (
1410+ source_url .rstrip ("/" )
1411+ + f"/plugins/{ name } /readme"
1412+ )
1413+ with urllib .request .urlopen (
1414+ url , timeout = 10
1415+ ) as r :
1416+ d = _json .loads (
1417+ r .read ().decode ("utf-8" , "ignore" )
1418+ )
1419+ readme = d .get ("readme" , "" ) or ""
1420+ except Exception as e :
1421+ self .logger .warning (
1422+ f"Fetch readme for { name } failed: { e } "
1423+ )
1424+ return jsonify ({"name" : name , "readme" : readme })
1425+
1426+ @app .route ("/api/market/plugins/<name>/detail" )
1427+ def api_market_plugin_detail (name ):
1428+ """获取插件详情(含 readme)。"""
1429+ source_url = (
1430+ self .config .get ("plugins.marketplace_url" , "" ) or ""
1431+ ).strip ()
1432+ if source_url :
1433+ try :
1434+ import urllib .request
1435+ import json as _json
1436+
1437+ url = source_url .rstrip ("/" ) + f"/plugins/{ name } "
1438+ with urllib .request .urlopen (
1439+ url , timeout = 10
1440+ ) as r :
1441+ d = _json .loads (
1442+ r .read ().decode ("utf-8" , "ignore" )
1443+ )
1444+ return jsonify (d )
1445+ except Exception as e :
1446+ self .logger .warning (
1447+ f"Fetch detail for { name } failed: { e } "
1448+ )
1449+ return jsonify ({"error" : "not found" }), 404
1450+
12671451 # ── adapters CRUD ──────────────────────────────────────────
12681452
12691453 @app .route ("/api/adapters" )
0 commit comments