Skip to content

Commit d3ec5d0

Browse files
author
QtineNiko
committed
feat(market): mirror speedtest + plugin detail, source empty when unavailable
1 parent be86896 commit d3ec5d0

4 files changed

Lines changed: 491 additions & 19 deletions

File tree

config.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ logging:
2929
plugins:
3030
autoload: []
3131
dir: ./plugins
32-
marketplace_mirrors:
33-
- https://mirror.qtine.dev/api
34-
marketplace_url: https://qtine-market.example.com/api
32+
marketplace_mirrors: []
33+
marketplace_url: http://xingruo.chat:2456/api
3534
security:
3635
blacklist:
3736
enabled: true

plugins/pu-sr/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# PU-SR
2+
3+
PU-CN-Wiki 搜索与反馈插件,基于 Qtine 平台。
4+
5+
## 功能
6+
7+
通过 Wikit GraphQL API 搜索 pu-cn-wiki 页面信息,并提供反馈/建议系统。
8+
9+
## 命令
10+
11+
### /sr - 搜索
12+
13+
- `/sr` — 显示帮助
14+
- `/sr <页面名>` — 按页面名搜索,返回首个匹配(标题/评分/作者/标签/讨论/链接)
15+
- `/sr <页面名> #tag <标签> [标签...]` — 页面名 + 标签检索(全包含)
16+
- `/sr <页面名> <作者名>` — 页面名 + 作者组合检索
17+
- `/sr #au <作者名>` — 查看作者在本站所有页面(QQ 合并转发消息)
18+
19+
### /au - 作者
20+
21+
- `/au <作者名>` — 显示作者排行与总分,格式 `#排名 作者名 总分:值`
22+
23+
### /putc - 反馈
24+
25+
- `/putc` — 反馈/建议菜单
26+
- `/putc <内容>` — 提交反馈
27+
- `/putc me` — 查看自己的反馈受理状态
28+
- `/putc <编号>` — 查看指定编号反馈
29+
- `/putc list` — 列出所有反馈(管理员)
30+
- `/putc add <编号>` — 受理反馈(管理员)
31+
- `/putc kill <编号>` — 拒绝反馈(管理员)
32+
33+
## 配置
34+
35+
管理员 QQ 通过插件配置项 `admins` 设置(WebUI 可改),控制 `/putc list/add/kill` 权限。
36+
37+
## 数据来源
38+
39+
Wikit GraphQL API: `https://wikit.unitreaty.org/apiv1/graphql`
40+
41+
## 版本
42+
43+
1.0.1

qtine/core/app.py

Lines changed: 187 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@
3434
MAX_UPLOAD_MB = 50
3535
ALLOWED_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

Comments
 (0)