ZHIWEI666 commited on
Commit
25f34c9
·
verified ·
1 Parent(s): d405cc0

Upload 3 files

Browse files
router_items.py CHANGED
@@ -7,6 +7,7 @@ import os
7
  import urllib.request
8
  import urllib.error
9
  import json
 
10
  import 数据库连接 as db
11
  from models import ItemCreate, ItemUpdate, RatingRequest
12
  from 安全认证 import require_auth, check_ownership
@@ -15,6 +16,13 @@ from db_utils import record_view, sort_cache
15
 
16
  router = APIRouter()
17
 
 
 
 
 
 
 
 
18
  def get_last_6_months():
19
  res = []
20
  today = datetime.date.today()
@@ -69,7 +77,7 @@ async def get_items(type: str = "tool", sort: str = "time", limit: int = 50): #
69
  for item in filtered_items:
70
  item["commentsData"] = comments_db.get(item["id"], [])
71
  item["comments"] = len(item["commentsData"])
72
- item["latest_version"] = versions_db.get(item["id"], "")
73
 
74
  # 🔴 【绝对核心防线】:在下发给前端前,强行在内存中抹除敏感信息!
75
  item["has_private_token"] = bool(item.get("github_token"))
@@ -331,7 +339,10 @@ async def update_item(item_id: str, update_data: ItemUpdate, current_user: str =
331
  if update_data.title is not None: item["title"] = update_data.title
332
  if update_data.shortDesc is not None: item["shortDesc"] = update_data.shortDesc
333
  if update_data.fullDesc is not None: item["fullDesc"] = update_data.fullDesc
 
334
  if update_data.link is not None: item["link"] = update_data.link
 
 
335
  if update_data.coverUrl is not None: item["coverUrl"] = update_data.coverUrl
336
  if update_data.imageUrls is not None: item["imageUrls"] = update_data.imageUrls # 🖼️ 效果展示图列表
337
 
@@ -383,6 +394,49 @@ async def update_item(item_id: str, update_data: ItemUpdate, current_user: str =
383
  sort_cache.invalidate("items:")
384
  invalidate_cache("items.json")
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  result = {"status": "success"}
387
  if result_holder.get("price_change_info"):
388
  price_change_info = result_holder["price_change_info"]
@@ -472,7 +526,7 @@ async def get_item_by_id(item_id: str):
472
  # 添加关联数据
473
  item["commentsData"] = comments_db.get(item_id, [])
474
  item["comments"] = len(item["commentsData"])
475
- item["latest_version"] = versions_db.get(item_id, "")
476
 
477
  # 🔴 【安全防线】:抹除敏感信息
478
  item["has_private_token"] = bool(item.get("github_token"))
@@ -488,7 +542,7 @@ async def get_item_by_id(item_id: str):
488
  async def get_item_version(item_id: str):
489
  """获取单个资源的最新版本号"""
490
  versions_db = db.load_data("versions.json", default_data={})
491
- return {"status": "success", "version": versions_db.get(item_id, "")}
492
 
493
  @router.delete("/api/items/{item_id}")
494
  async def delete_item(item_id: str, current_user: str = Depends(require_auth)):
 
7
  import urllib.request
8
  import urllib.error
9
  import json
10
+ import asyncio
11
  import 数据库连接 as db
12
  from models import ItemCreate, ItemUpdate, RatingRequest
13
  from 安全认证 import require_auth, check_ownership
 
16
 
17
  router = APIRouter()
18
 
19
+ def _get_version_str(versions_db: dict, item_id: str) -> str:
20
+ """兼容新旧格式获取版本hash字符串"""
21
+ val = versions_db.get(item_id, "")
22
+ if isinstance(val, dict):
23
+ return val.get("hash", "") or ""
24
+ return val or ""
25
+
26
  def get_last_6_months():
27
  res = []
28
  today = datetime.date.today()
 
77
  for item in filtered_items:
78
  item["commentsData"] = comments_db.get(item["id"], [])
79
  item["comments"] = len(item["commentsData"])
80
+ item["latest_version"] = _get_version_str(versions_db, item["id"])
81
 
82
  # 🔴 【绝对核心防线】:在下发给前端前,强行在内存中抹除敏感信息!
83
  item["has_private_token"] = bool(item.get("github_token"))
 
339
  if update_data.title is not None: item["title"] = update_data.title
340
  if update_data.shortDesc is not None: item["shortDesc"] = update_data.shortDesc
341
  if update_data.fullDesc is not None: item["fullDesc"] = update_data.fullDesc
342
+ old_link = item.get("link", "")
343
  if update_data.link is not None: item["link"] = update_data.link
344
+ result_holder["link_changed"] = old_link != item.get("link", "")
345
+ result_holder["new_link"] = item.get("link", "")
346
  if update_data.coverUrl is not None: item["coverUrl"] = update_data.coverUrl
347
  if update_data.imageUrls is not None: item["imageUrls"] = update_data.imageUrls # 🖼️ 效果展示图列表
348
 
 
394
  sort_cache.invalidate("items:")
395
  invalidate_cache("items.json")
396
 
397
+ # 🔄 [缓存刷新] 资源 link 变更时清除 ZIP 缓存元信息
398
+ if result_holder.get("link_changed"):
399
+ try:
400
+ versions_db = db.load_data("versions.json", default_data={})
401
+ entry = versions_db.get(item_id)
402
+ if entry is not None:
403
+ if isinstance(entry, str):
404
+ entry = {"hash": entry}
405
+ had_meta = "cached_at" in entry or "zip_size" in entry
406
+ entry.pop("cached_at", None)
407
+ entry.pop("zip_size", None)
408
+ if had_meta:
409
+ versions_db[item_id] = entry
410
+ db.save_data("versions.json", versions_db)
411
+ print(f"[缓存刷新] 已清除 item {item_id} 的 ZIP 缓存元信息")
412
+
413
+ # 可选:触发异步预缓存
414
+ new_link = result_holder.get("new_link", "")
415
+ if new_link and new_link.startswith("https://github.com/"):
416
+ try:
417
+ from 云端_定时版本检测引擎 import precache_github_zip
418
+ # 获取当前 item 的 token 和 version_hash
419
+ items_db = db.load_data("items.json", default_data=[])
420
+ token = None
421
+ for it in items_db:
422
+ if it["id"] == item_id:
423
+ token = it.get("github_token")
424
+ break
425
+
426
+ versions_db = db.load_data("versions.json", default_data={})
427
+ entry = versions_db.get(item_id, {})
428
+ version_hash = entry.get("hash", "") if isinstance(entry, dict) else entry
429
+
430
+ if version_hash:
431
+ asyncio.create_task(precache_github_zip(new_link, token, item_id, version_hash))
432
+ print(f"[缓存刷新] 已触发 item {item_id} 的异步预缓存")
433
+ except ImportError:
434
+ print(f"[缓存刷新] 预缓存模块导入失败,跳过异步预缓存")
435
+ except Exception as e:
436
+ print(f"[缓存刷新] 触发异步预缓存失败: {e}")
437
+ except Exception as e:
438
+ print(f"[缓存刷新] 缓存刷新失败(不影响主流程): {e}")
439
+
440
  result = {"status": "success"}
441
  if result_holder.get("price_change_info"):
442
  price_change_info = result_holder["price_change_info"]
 
526
  # 添加关联数据
527
  item["commentsData"] = comments_db.get(item_id, [])
528
  item["comments"] = len(item["commentsData"])
529
+ item["latest_version"] = _get_version_str(versions_db, item_id)
530
 
531
  # 🔴 【安全防线】:抹除敏感信息
532
  item["has_private_token"] = bool(item.get("github_token"))
 
542
  async def get_item_version(item_id: str):
543
  """获取单个资源的最新版本号"""
544
  versions_db = db.load_data("versions.json", default_data={})
545
+ return {"status": "success", "version": _get_version_str(versions_db, item_id)}
546
 
547
  @router.delete("/api/items/{item_id}")
548
  async def delete_item(item_id: str, current_user: str = Depends(require_auth)):
router_proxy.py CHANGED
@@ -57,6 +57,64 @@ async def proxy_github_zip(req_data: ProxyGithubZipRequest, db: Session = Depend
57
  if not owned:
58
  return JSONResponse(content={"error": "🚨 拒绝访问:未找到购买记录!"}, status_code=403)
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  # 2. 解析 GitHub 仓库信息
61
  repo_url = item.get("link", "").rstrip("/")
62
  if not repo_url.startswith("https://github.com/"):
 
57
  if not owned:
58
  return JSONResponse(content={"error": "🚨 拒绝访问:未找到购买记录!"}, status_code=403)
59
 
60
+ # ===== ZIP 缓存优先机制(鉴权后、GitHub请求前) =====
61
+ try:
62
+ versions_db = json_db.load_data("versions.json", default_data={})
63
+ item_version = versions_db.get(req_data.item_id)
64
+
65
+ # 兼容新格式:{"item_id": {"hash": "...", "cached_at": "...", "zip_size": ...}}
66
+ if isinstance(item_version, dict) and item_version.get("cached_at"):
67
+ cache_filename = f"zips/{req_data.item_id}_{item_version['hash']}.zip"
68
+ cache_url = f"https://huggingface.co/datasets/ZHIWEI666/ComfyUI-Ranking/resolve/main/{cache_filename}"
69
+
70
+ cache_client = httpx.AsyncClient(follow_redirects=True)
71
+ head_resp = None
72
+ try:
73
+ head_resp = await cache_client.head(cache_url, timeout=10, follow_redirects=True)
74
+ except Exception as head_err:
75
+ print(f"[ZIP缓存] HEAD验证失败: {head_err}")
76
+ await cache_client.aclose()
77
+ raise
78
+
79
+ if head_resp.status_code == 200:
80
+ print(f"[ZIP缓存] 命中: {req_data.item_id}, 大小: {item_version.get('zip_size', 'unknown')}")
81
+
82
+ content_length = head_resp.headers.get('content-length', '') or str(item_version.get('zip_size', ''))
83
+
84
+ try:
85
+ stream_resp = await cache_client.send(
86
+ cache_client.build_request("GET", cache_url),
87
+ stream=True
88
+ )
89
+ except Exception as stream_err:
90
+ print(f"[ZIP缓存] 流请求失败: {stream_err}")
91
+ await cache_client.aclose()
92
+ raise
93
+
94
+ cache_headers = {}
95
+ if content_length:
96
+ cache_headers["Content-Length"] = content_length
97
+
98
+ async def cache_stream_generator():
99
+ try:
100
+ if stream_resp.status_code != 200:
101
+ yield b"CACHE_DOWNLOAD_FAILED"
102
+ return
103
+ async for chunk in stream_resp.aiter_bytes():
104
+ yield chunk
105
+ except Exception:
106
+ yield b"CACHE_DOWNLOAD_FAILED"
107
+ finally:
108
+ await stream_resp.aclose()
109
+ await cache_client.aclose()
110
+
111
+ return StreamingResponse(cache_stream_generator(), media_type="application/zip", headers=cache_headers)
112
+
113
+ # 缓存未命中:关闭 client 后继续原有逻辑
114
+ await cache_client.aclose()
115
+ except Exception as e:
116
+ print(f"[ZIP缓存] 查询失败,fallback到GitHub直连: {e}")
117
+
118
  # 2. 解析 GitHub 仓库信息
119
  repo_url = item.get("link", "").rstrip("/")
120
  if not repo_url.startswith("https://github.com/"):
云端_定时版本检测引擎.py CHANGED
@@ -3,13 +3,46 @@ import asyncio
3
  import datetime
4
  import httpx
5
  import logging
 
 
 
6
  import 数据库连接 as db
7
  from notifications import add_notification
8
  from database_sql import SessionLocal
9
  from models_sql import Ownership
 
10
 
11
  logger = logging.getLogger("ComfyUI-Ranking.VersionCheck")
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  async def fetch_latest_github_hash(repo_url, token):
14
  """
15
  请求 GitHub API 获取最新版的 Commit Hash
@@ -151,6 +184,218 @@ async def trigger_update_notifications(updated_items: list):
151
  session.close()
152
 
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  async def daily_version_check_task():
155
  """每日 02:00 定时执行的守护进程"""
156
  while True:
@@ -168,6 +413,9 @@ async def daily_version_check_task():
168
  items_db = db.load_data("items.json", default_data=[])
169
  versions_db = db.load_data("versions.json", default_data={})
170
 
 
 
 
171
  updated_count = 0
172
  updated_items = [] # 记录有更新的插件信息
173
 
@@ -176,14 +424,23 @@ async def daily_version_check_task():
176
  if "github.com" in link:
177
  token = item.get("github_token")
178
  latest_hash = await fetch_latest_github_hash(link, token)
179
- if latest_hash and versions_db.get(item["id"]) != latest_hash:
180
- versions_db[item["id"]] = latest_hash
 
 
 
 
 
 
 
181
  updated_count += 1
182
- # 记录更新的插件信息
183
  updated_items.append({
184
  "id": item["id"],
185
  "title": item.get("title", "未知插件"),
186
- "version_hash": latest_hash
 
 
187
  })
188
 
189
  if updated_count > 0:
@@ -193,6 +450,26 @@ async def daily_version_check_task():
193
  await trigger_update_notifications(updated_items)
194
  else:
195
  print("✅ 版本检测任务完成,暂无任何新版本发现。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
 
198
  async def reset_daily_views_task():
 
3
  import datetime
4
  import httpx
5
  import logging
6
+ import os
7
+ import tempfile
8
+ import zipfile
9
  import 数据库连接 as db
10
  from notifications import add_notification
11
  from database_sql import SessionLocal
12
  from models_sql import Ownership
13
+ from huggingface_hub import upload_file, HfApi
14
 
15
  logger = logging.getLogger("ComfyUI-Ranking.VersionCheck")
16
 
17
+ # ==========================================
18
+ # 预缓存配置常量
19
+ # ==========================================
20
+ HF_REPO_ID = "ZHIWEI666/ComfyUI-Ranking"
21
+ ZIP_MAX_SIZE = 500 * 1024 * 1024 # 500MB
22
+ CACHE_EXPIRE_DAYS = 30
23
+ PRECACHE_BATCH_SIZE = 5
24
+ PRECACHE_INTERVAL = 10
25
+
26
+
27
+ def get_version_hash(versions_db: dict, item_id: str) -> str | None:
28
+ """兼容新旧格式获取 hash"""
29
+ val = versions_db.get(item_id)
30
+ if isinstance(val, str):
31
+ return val
32
+ elif isinstance(val, dict):
33
+ return val.get("hash")
34
+ return None
35
+
36
+
37
+ def _normalize_version_entry(versions_db: dict, item_id: str):
38
+ """将旧格式字符串转换为新格式字典(原地修改)"""
39
+ val = versions_db.get(item_id)
40
+ if isinstance(val, str):
41
+ versions_db[item_id] = {"hash": val}
42
+ elif not isinstance(val, dict):
43
+ versions_db[item_id] = {"hash": ""}
44
+
45
+
46
  async def fetch_latest_github_hash(repo_url, token):
47
  """
48
  请求 GitHub API 获取最新版的 Commit Hash
 
184
  session.close()
185
 
186
 
187
+ async def precache_github_zip(repo_url: str, token: str | None, item_id: str, version_hash: str):
188
+ """预下载 GitHub ZIP 并上传到 HF Dataset 缓存"""
189
+ try:
190
+ parts = repo_url.rstrip("/").split("/")
191
+ if len(parts) < 2:
192
+ logger.warning(f"[预缓存] 无效的仓库地址: {repo_url}")
193
+ return False
194
+ owner, repo = parts[-2], parts[-1].replace(".git", "")
195
+ zip_url = f"https://api.github.com/repos/{owner}/{repo}/zipball"
196
+
197
+ headers = {
198
+ "Accept": "application/vnd.github.v3+json",
199
+ "User-Agent": "ComfyUI-Hub"
200
+ }
201
+ if token:
202
+ headers["Authorization"] = f"token {token}"
203
+
204
+ hf_token = os.environ.get("HF_TOKEN")
205
+ if not hf_token:
206
+ logger.warning("[预缓存] HF_TOKEN 环境变量未设置,跳过预缓存")
207
+ return False
208
+
209
+ # 下载到临时文件(stream 模式,超时 300s)
210
+ temp_path = None
211
+ async with httpx.AsyncClient(follow_redirects=True, timeout=httpx.Timeout(300.0, connect=30.0)) as client:
212
+ logger.info(f"[预缓存] 开始下载 {item_id} ZIP: {zip_url}")
213
+ async with client.stream("GET", zip_url, headers=headers) as resp:
214
+ if resp.status_code != 200:
215
+ logger.warning(f"[预缓存] 下载 ZIP 失败,状态码: {resp.status_code}, URL: {zip_url}")
216
+ return False
217
+
218
+ fd, temp_path = tempfile.mkstemp(suffix=".zip", prefix=f"precache_{item_id}_")
219
+ try:
220
+ total_size = 0
221
+ oversized = False
222
+ with os.fdopen(fd, "wb") as f:
223
+ async for chunk in resp.aiter_bytes(chunk_size=8192):
224
+ f.write(chunk)
225
+ total_size += len(chunk)
226
+ if total_size > ZIP_MAX_SIZE:
227
+ logger.warning(
228
+ f"[预缓存] ZIP 超过 {ZIP_MAX_SIZE // (1024*1024)}MB 限制,跳过: "
229
+ f"{item_id} (已下载 {total_size} 字节)"
230
+ )
231
+ oversized = True
232
+ break
233
+
234
+ if oversized:
235
+ if temp_path and os.path.exists(temp_path):
236
+ try:
237
+ os.remove(temp_path)
238
+ except Exception:
239
+ pass
240
+ return False
241
+ except Exception:
242
+ # 清理临时文件
243
+ if temp_path and os.path.exists(temp_path):
244
+ try:
245
+ os.remove(temp_path)
246
+ except Exception:
247
+ pass
248
+ raise
249
+
250
+ if not temp_path or not os.path.exists(temp_path):
251
+ logger.warning(f"[预缓存] 临时文件未生成: {item_id}")
252
+ return False
253
+
254
+ # 验证 ZIP 完整性
255
+ if not zipfile.is_zipfile(temp_path):
256
+ logger.warning(f"[预缓存] ZIP 文件验证失败(非有效 ZIP): {item_id}")
257
+ os.remove(temp_path)
258
+ return False
259
+
260
+ zip_size = os.path.getsize(temp_path)
261
+ logger.info(f"[预缓存] ZIP 下载完成: {item_id}, 大小 {zip_size} 字节")
262
+
263
+ # 上传到 HF Dataset
264
+ path_in_repo = f"zips/{item_id}_{version_hash}.zip"
265
+ try:
266
+ upload_file(
267
+ path_or_fileobj=temp_path,
268
+ path_in_repo=path_in_repo,
269
+ repo_id=HF_REPO_ID,
270
+ repo_type="dataset",
271
+ token=hf_token,
272
+ )
273
+ logger.info(f"[预缓存] 上传成功: {path_in_repo}")
274
+ except Exception as e:
275
+ logger.warning(f"[预缓存] 上传 HF 失败 ({item_id}): {e}")
276
+ os.remove(temp_path)
277
+ return False
278
+
279
+ # 更新 versions.json 缓存元信息
280
+ versions_db = db.load_data("versions.json", default_data={})
281
+ entry = versions_db.get(item_id, {})
282
+ if isinstance(entry, str):
283
+ entry = {"hash": entry}
284
+ entry["cached_at"] = datetime.datetime.utcnow().isoformat() + "Z"
285
+ entry["zip_size"] = zip_size
286
+ versions_db[item_id] = entry
287
+ db.save_data("versions.json", versions_db)
288
+ logger.info(f"[预缓存] 缓存元信息已更新: {item_id}")
289
+
290
+ # 清理临时文件
291
+ os.remove(temp_path)
292
+ return True
293
+
294
+ except Exception as e:
295
+ logger.warning(f"[预缓存] 预缓存异常 ({item_id}): {e}")
296
+ return False
297
+
298
+
299
+ async def _delete_old_zip_cache(item_id: str, old_hash: str):
300
+ """删除指定 item_id + old_hash 对应的 HF Dataset 缓存 ZIP"""
301
+ try:
302
+ hf_token = os.environ.get("HF_TOKEN")
303
+ if not hf_token:
304
+ return
305
+ hf_api = HfApi(token=hf_token)
306
+ path_in_repo = f"zips/{item_id}_{old_hash}.zip"
307
+ try:
308
+ hf_api.delete_file(
309
+ path_in_repo=path_in_repo,
310
+ repo_id=HF_REPO_ID,
311
+ repo_type="dataset",
312
+ )
313
+ logger.info(f"[预缓存] 已删除旧版本缓存: {path_in_repo}")
314
+ except Exception as e:
315
+ error_msg = str(e)
316
+ if "404" in error_msg or "Not Found" in error_msg:
317
+ logger.info(f"[预缓存] 旧缓存文件已不存在: {path_in_repo}")
318
+ else:
319
+ logger.warning(f"[预缓存] 删除旧缓存失败: {path_in_repo}, {e}")
320
+ except Exception as e:
321
+ logger.warning(f"[预缓存] 删除旧缓存异常 ({item_id}): {e}")
322
+
323
+
324
+ async def cleanup_expired_cache(versions_db: dict):
325
+ """清理过期的 ZIP 缓存文件"""
326
+ try:
327
+ hf_token = os.environ.get("HF_TOKEN")
328
+ if not hf_token:
329
+ logger.warning("[预缓存] HF_TOKEN 未设置,跳过缓存清理")
330
+ return
331
+
332
+ hf_api = HfApi(token=hf_token)
333
+ now = datetime.datetime.utcnow()
334
+ expired_items = []
335
+
336
+ for item_id, val in list(versions_db.items()):
337
+ if isinstance(val, str):
338
+ continue
339
+ if not isinstance(val, dict):
340
+ continue
341
+
342
+ old_hash = val.get("hash")
343
+ cached_at_str = val.get("cached_at")
344
+
345
+ # 规则1: cached_at 超过 30 天的条目
346
+ if cached_at_str:
347
+ try:
348
+ cached_at = datetime.datetime.fromisoformat(cached_at_str.replace("Z", "+00:00"))
349
+ if (now - cached_at).days > CACHE_EXPIRE_DAYS:
350
+ expired_items.append((item_id, old_hash, "过期"))
351
+ continue
352
+ except ValueError:
353
+ pass
354
+
355
+ # 规则2: 版本更新时(hash 变化)——在 daily_version_check_task 中单独处理
356
+ # 本函数仅处理过期时间
357
+
358
+ if not expired_items:
359
+ return
360
+
361
+ deleted_count = 0
362
+ modified_local = False
363
+ for item_id, old_hash, reason in expired_items:
364
+ if old_hash:
365
+ path_in_repo = f"zips/{item_id}_{old_hash}.zip"
366
+ try:
367
+ hf_api.delete_file(
368
+ path_in_repo=path_in_repo,
369
+ repo_id=HF_REPO_ID,
370
+ repo_type="dataset",
371
+ )
372
+ logger.info(f"[预缓存] 已删除过期缓存 ({reason}): {path_in_repo}")
373
+ deleted_count += 1
374
+ except Exception as e:
375
+ error_msg = str(e)
376
+ if "404" in error_msg or "Not Found" in error_msg:
377
+ logger.info(f"[预缓存] 缓存文件已不存在: {path_in_repo}")
378
+ else:
379
+ logger.warning(f"[预缓存] 删除缓存失败: {path_in_repo}, {e}")
380
+
381
+ # 清除本地 cached_at 和 zip_size(保留 hash)
382
+ entry = versions_db.get(item_id, {})
383
+ if isinstance(entry, dict):
384
+ had_meta = "cached_at" in entry or "zip_size" in entry
385
+ entry.pop("cached_at", None)
386
+ entry.pop("zip_size", None)
387
+ versions_db[item_id] = entry
388
+ if had_meta:
389
+ modified_local = True
390
+
391
+ if deleted_count > 0 or modified_local:
392
+ db.save_data("versions.json", versions_db)
393
+ logger.info(f"[预缓存] 过期缓存清理完成,共删除 {deleted_count} 个文件")
394
+
395
+ except Exception as e:
396
+ logger.warning(f"[预缓存] 缓存清理异常: {e}")
397
+
398
+
399
  async def daily_version_check_task():
400
  """每日 02:00 定时执行的守护进程"""
401
  while True:
 
413
  items_db = db.load_data("items.json", default_data=[])
414
  versions_db = db.load_data("versions.json", default_data={})
415
 
416
+ # 先清理过期缓存(cached_at 超过 30 天)
417
+ await cleanup_expired_cache(versions_db)
418
+
419
  updated_count = 0
420
  updated_items = [] # 记录有更新的插件信息
421
 
 
424
  if "github.com" in link:
425
  token = item.get("github_token")
426
  latest_hash = await fetch_latest_github_hash(link, token)
427
+ old_hash = get_version_hash(versions_db, item["id"])
428
+ if latest_hash and old_hash != latest_hash:
429
+ # 版本更新时,删除旧 hash 对应的缓存文件
430
+ if old_hash:
431
+ await _delete_old_zip_cache(item["id"], old_hash)
432
+
433
+ # 统一转换为新格式并写入新 hash
434
+ _normalize_version_entry(versions_db, item["id"])
435
+ versions_db[item["id"]]["hash"] = latest_hash
436
  updated_count += 1
437
+ # 记录更新的插件信息(包含 link/token 供预缓存使用)
438
  updated_items.append({
439
  "id": item["id"],
440
  "title": item.get("title", "未知插件"),
441
+ "version_hash": latest_hash,
442
+ "link": link,
443
+ "github_token": token
444
  })
445
 
446
  if updated_count > 0:
 
450
  await trigger_update_notifications(updated_items)
451
  else:
452
  print("✅ 版本检测任务完成,暂无任何新版本发现。")
453
+
454
+ # 对更新的 items 执行预缓存(最多 5 个,每个间隔 10 秒)
455
+ if updated_items:
456
+ precache_count = 0
457
+ batch = updated_items[:PRECACHE_BATCH_SIZE]
458
+ for idx, item in enumerate(batch):
459
+ success = await precache_github_zip(
460
+ item["link"],
461
+ item.get("github_token"),
462
+ item["id"],
463
+ item["version_hash"]
464
+ )
465
+ if success:
466
+ precache_count += 1
467
+ # 间隔 10 秒(最后一个不需要等待)
468
+ if idx < len(batch) - 1:
469
+ await asyncio.sleep(PRECACHE_INTERVAL)
470
+ logger.info(
471
+ f"[预缓存] 本次预缓存完成: {precache_count}/{len(batch)} 成功"
472
+ )
473
 
474
 
475
  async def reset_daily_views_task():