Initial Qinglong script classification corpus

This commit is contained in:
Hermes Agent
2026-05-23 18:48:48 +00:00
commit 5b4854515c
32 changed files with 15496 additions and 0 deletions

764
collector.py Normal file
View File

@@ -0,0 +1,764 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
公开青龙脚本采集器(多来源首轮版)
目标:
- 从公开代码平台/公开网页入口搜索青龙/QL 脚本;
- 按 APP版/抓包、APP版/账密、web版/抓包、web版/账密、微信小程序/协议版、微信小程序/抓包版分类;
- 以代码内容 SHA256 去重;
- 同项目不同代码放入同一项目目录;
- 记录源仓库、源路径、源 URL、上传/提交时间。
说明:
- GitHub 搜索 API 未配置 token 时有限流;可设置 GITHUB_TOKEN 提升额度。
- “上传时间”优先取该文件最近一次 commit 的 commit.committer.date失败时退化为仓库 pushed_at。
"""
from __future__ import annotations
import base64
import csv
import hashlib
import json
import os
import re
import time
import unicodedata
import urllib.parse
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Tuple
try:
import requests
except ModuleNotFoundError: # 便于在新环境直接运行
import subprocess
import sys as _sys
subprocess.check_call([_sys.executable, "-m", "pip", "install", "requests", "-q", "--break-system-packages"])
import requests
ROOT = Path(__file__).resolve().parent
OUT_DIR = ROOT / "脚本库"
META_DIR = ROOT / "metadata"
INDEX_CSV = ROOT / "INDEX.csv"
INDEX_JSON = ROOT / "INDEX.json"
README = ROOT / "README.md"
CACHE = META_DIR / "collector_cache.json"
MAX_REPOS_PER_QUERY = int(os.getenv("MAX_REPOS_PER_QUERY", "12"))
MAX_FILES_TOTAL = int(os.getenv("MAX_FILES_TOTAL", "260"))
REQUEST_SLEEP = float(os.getenv("REQUEST_SLEEP", "0.4"))
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "").strip()
SESSION = requests.Session()
SESSION.headers.update({
"User-Agent": "ql-script-classifier/1.0",
"Accept": "application/vnd.github+json",
})
if GITHUB_TOKEN:
SESSION.headers["Authorization"] = f"Bearer {GITHUB_TOKEN}"
SEARCH_QUERIES = [
# 青龙/QL 常用关键词
"青龙 脚本 language:JavaScript",
"青龙 脚本 language:Python",
"qinglong script language:JavaScript",
"qinglong script language:Python",
"ql repo script language:JavaScript",
'"new Env" "cron" language:JavaScript',
'"new Env" "青龙" language:JavaScript',
'"ql repo" "new Env" language:JavaScript',
'"青龙面板" "new Env" language:JavaScript',
'"微信小程序" "青龙" language:Python',
'"wx_token" "wx_cloud" language:Python',
'"getMiniProgramCode" language:Python',
'"getMiniProgramCode" language:JavaScript',
]
GITEE_SEARCH_QUERIES = [
"青龙 脚本",
"new Env 青龙",
"wx_token wx_cloud",
"getMiniProgramCode",
]
# 其它公开网页入口:只处理公开可访问的代码页/raw 链接;不处理需要登录、私密分享或绕权的网盘。
WEB_SEARCH_QUERIES = [
'青龙 脚本 "new Env"',
'青龙脚本 "ql repo"',
'青龙脚本 "wx_token" "wx_cloud"',
'青龙脚本 "getMiniProgramCode"',
'青龙脚本 site:gitee.com',
'青龙脚本 site:gitcode.com',
'青龙脚本 site:agit.ai',
'青龙脚本 site:raw.githubusercontent.com',
]
VALID_SUFFIXES = {".js", ".py", ".ts", ".sh"}
SKIP_PATH_KEYWORDS = {
"node_modules", ".git", "dist/", "build/", "package-lock", "yarn.lock",
"readme", "license", "docker", "workflow", ".github", "icon", "image", "png", "jpg",
}
CATEGORY_ORDER = [
"APP版/抓包",
"APP版/账密",
"web版/抓包",
"web版/账密",
"微信小程序/协议版",
"微信小程序/抓包版",
"未分类",
]
@dataclass
class Record:
category: str
project: str
filename: str
sha256: str
source_platform: str
source_repo: str
source_path: str
source_url: str
raw_url: str
uploaded_at: str
saved_path: str
auth_type: str
platform_type: str
evidence: str
def safe_segment(text: str, fallback: str = "unknown", max_len: int = 80) -> str:
text = unicodedata.normalize("NFKC", text or "")
text = re.sub(r"[\\/:*?\"<>|\s]+", "_", text).strip("._-")
text = re.sub(r"_+", "_", text)
if not text:
text = fallback
return text[:max_len]
def normalized_hash(content: str) -> str:
# 去 BOM、统一换行、去末尾空白不删除注释避免误合并不同版本
norm = content.replace("\r\n", "\n").replace("\r", "\n").lstrip("\ufeff")
norm = "\n".join(line.rstrip() for line in norm.split("\n")).strip() + "\n"
return hashlib.sha256(norm.encode("utf-8", "ignore")).hexdigest()
def github_get(url: str, **kwargs):
time.sleep(REQUEST_SLEEP)
r = SESSION.get(url, timeout=30, **kwargs)
if r.status_code == 403 and "rate limit" in r.text.lower():
reset = r.headers.get("X-RateLimit-Reset")
print(f"[WARN] GitHub rate limited: {url} reset={reset}")
raise RuntimeError("github_rate_limited")
if r.status_code >= 400:
print(f"[WARN] HTTP {r.status_code}: {url} {r.text[:160]}")
return None
return r
def search_github_repos() -> List[dict]:
repos: Dict[str, dict] = {}
for q in SEARCH_QUERIES:
url = "https://api.github.com/search/repositories"
params = {"q": q, "sort": "updated", "order": "desc", "per_page": MAX_REPOS_PER_QUERY}
print(f"[SEARCH] {q}", flush=True)
r = github_get(url, params=params)
if not r:
continue
data = r.json()
for item in data.get("items", []):
full = item.get("full_name")
if full and full not in repos:
repos[full] = item
return list(repos.values())
def generic_get(url: str, **kwargs):
time.sleep(REQUEST_SLEEP)
headers = kwargs.pop("headers", {}) or {}
merged = {"User-Agent": "Mozilla/5.0 ql-script-classifier/1.0", **headers}
try:
r = SESSION.get(url, timeout=30, headers=merged, **kwargs)
if r.status_code >= 400:
print(f"[WARN] HTTP {r.status_code}: {url} {r.text[:120]}")
return None
return r
except Exception as e:
print(f"[WARN] GET failed: {url} {e}")
return None
def search_gitee_repos() -> List[dict]:
"""Gitee 公开搜索。API 搜索经常返回空,因此同时解析公开搜索页。"""
repos: Dict[str, dict] = {}
for q in GITEE_SEARCH_QUERIES:
print(f"[GITEE_SEARCH] {q}")
# API 尝试
api = "https://gitee.com/api/v5/search/repositories"
r = generic_get(api, params={"q": q, "page": 1, "per_page": MAX_REPOS_PER_QUERY})
if r:
try:
data = r.json()
if isinstance(data, list):
for item in data:
full = item.get("full_name") or item.get("path_with_namespace")
if full:
repos[full] = item
except Exception:
pass
# HTML 搜索页尝试
html_url = "https://gitee.com/search"
# HTML 搜索容易被前端页面拖慢/反爬,默认仅使用 API如需扩展可后续加入定向种子仓库。
return list(repos.values())
def list_gitee_tree(repo: dict) -> List[dict]:
full = repo.get("full_name") or repo.get("path_with_namespace")
if not full:
return []
branch = repo.get("default_branch") or "master"
for b in [branch, "master", "main"]:
api = f"https://gitee.com/api/v5/repos/{urllib.parse.quote(str(full), safe='')}/git/trees/{urllib.parse.quote(str(b))}"
r = generic_get(api, params={"recursive": 1})
if not r:
continue
try:
data = r.json()
tree = data.get("tree") if isinstance(data, dict) else None
if isinstance(tree, list):
repo["default_branch"] = b
return tree
except Exception:
continue
return []
def get_gitee_file_content(repo_full: str, path: str, branch: str) -> Tuple[Optional[str], str, str]:
# raw URL 不需要 token 的公开仓库通常可直接访问
raw_url = f"https://gitee.com/{repo_full}/raw/{urllib.parse.quote(branch)}/{urllib.parse.quote(path)}"
html_url = f"https://gitee.com/{repo_full}/blob/{urllib.parse.quote(branch)}/{urllib.parse.quote(path)}"
r = generic_get(raw_url)
if r and len(r.content) <= 1024 * 1024:
return r.content.decode("utf-8", "ignore"), raw_url, html_url
return None, raw_url, html_url
def get_gitee_file_uploaded_at(repo_full: str, path: str, fallback: str) -> str:
# Gitee commit API 兼容性不稳定;失败时退化到 repo pushed_at 或空。
encoded_repo = urllib.parse.quote(repo_full, safe="")
api = f"https://gitee.com/api/v5/repos/{encoded_repo}/commits"
r = generic_get(api, params={"path": path, "page": 1, "per_page": 1})
if r:
try:
data = r.json()
if isinstance(data, list) and data:
c = data[0]
return ((c.get("commit") or {}).get("committer") or {}).get("date") or c.get("created_at") or fallback or ""
except Exception:
pass
return fallback or ""
def discover_public_code_links() -> List[dict]:
"""通过公开搜索页发现 raw/js/py 链接。使用 DuckDuckGo HTML无登录。"""
links: Dict[str, dict] = {}
if os.getenv("ENABLE_WEB_SEARCH", "false").strip().lower() != "true":
print("[WEB_SEARCH] disabled; set ENABLE_WEB_SEARCH=true to enable public web search")
return []
for q in WEB_SEARCH_QUERIES:
print(f"[WEB_SEARCH] {q}")
url = "https://duckduckgo.com/html/"
r = generic_get(url, params={"q": q})
if not r:
continue
html = r.text
for m in re.finditer(r'href="([^"]+)"', html):
href = m.group(1)
href = href.replace("&amp;", "&")
if "uddg=" in href:
qs = urllib.parse.parse_qs(urllib.parse.urlparse(href).query)
href = qs.get("uddg", [href])[0]
if not re.search(r"\.(js|py|ts|sh)(\?|$)", href, flags=re.I):
continue
if not any(host in href for host in ["githubusercontent.com", "gitee.com", "gitcode", "agit.ai", "raw"]):
continue
links[href] = {"url": href}
return list(links.values())
def get_public_link_content(url: str) -> Tuple[Optional[str], str, str, str]:
raw_url = url
html_url = url
# GitHub blob -> raw
m = re.match(r"https://github\.com/([^/]+/[^/]+)/blob/([^/]+)/(.+)", url)
if m:
raw_url = f"https://raw.githubusercontent.com/{m.group(1)}/{m.group(2)}/{m.group(3)}"
# Gitee blob -> raw
m = re.match(r"https://gitee\.com/([^/]+/[^/]+)/blob/([^/]+)/(.+)", url)
if m:
raw_url = f"https://gitee.com/{m.group(1)}/raw/{m.group(2)}/{m.group(3)}"
r = generic_get(raw_url)
if r and len(r.content) <= 1024 * 1024:
return r.content.decode("utf-8", "ignore"), raw_url, html_url, ""
return None, raw_url, html_url, ""
def list_github_tree(repo: dict) -> List[dict]:
full = repo["full_name"]
branch = repo.get("default_branch") or "main"
url = f"https://api.github.com/repos/{full}/git/trees/{urllib.parse.quote(branch)}?recursive=1"
r = github_get(url)
if not r:
return []
data = r.json()
return data.get("tree", [])
def is_candidate_path(path: str) -> bool:
lower = path.lower()
if any(k in lower for k in SKIP_PATH_KEYWORDS):
return False
if Path(path).suffix.lower() not in VALID_SUFFIXES:
return False
# 常见脚本路径或文件内容关键词由后续判断;这里不过窄过滤
return True
def get_file_content(repo_full: str, path: str, branch: str) -> Tuple[Optional[str], str, str]:
encoded = urllib.parse.quote(path, safe="")
url = f"https://api.github.com/repos/{repo_full}/contents/{encoded}?ref={urllib.parse.quote(branch)}"
r = github_get(url)
if not r:
return None, "", ""
data = r.json()
raw_url = data.get("download_url", "")
html_url = data.get("html_url", "")
content = data.get("content")
if data.get("encoding") == "base64" and content:
try:
b = base64.b64decode(content)
if len(b) > 1024 * 1024:
return None, raw_url, html_url
text = b.decode("utf-8", "ignore")
return text, raw_url, html_url
except Exception:
return None, raw_url, html_url
if raw_url:
rr = github_get(raw_url)
if rr:
return rr.text, raw_url, html_url
return None, raw_url, html_url
def get_file_uploaded_at(repo_full: str, path: str, fallback: str) -> str:
encoded = urllib.parse.quote(path, safe="")
url = f"https://api.github.com/repos/{repo_full}/commits?path={encoded}&per_page=1"
r = github_get(url)
if not r:
return fallback or ""
arr = r.json()
if isinstance(arr, list) and arr:
commit = arr[0].get("commit", {})
return (commit.get("committer") or {}).get("date") or (commit.get("author") or {}).get("date") or fallback or ""
return fallback or ""
def looks_like_ql_script(content: str, path: str) -> bool:
lower = content.lower()
score = 0
keywords = [
"new env", "cron:", "ql repo", "青龙", "notify", "sendnotify", "process.env",
"requests.", "axios", "got(", "wx_token", "jd_cookie", "cookie", "authorization",
"getminiprogramcode", "wx_cloud", "wx.login", "小程序",
]
for k in keywords:
if k in lower:
score += 1
if Path(path).suffix.lower() in {".js", ".py", ".sh"}:
score += 1
return score >= 2
def extract_project(content: str, path: str) -> str:
patterns = [
r"new\s+Env\([\"']([^\"']{2,80})[\"']\)",
r"new\s+Env\s*\([`\"]([^`\"]{2,80})[`\"]\)",
r"#\s*new\s+Env\([\"']([^\"']{2,80})[\"']\)",
r"项目[:]\s*([^\n\r]{2,60})",
r"name[:]\s*([^\n\r]{2,60})",
]
for pat in patterns:
m = re.search(pat, content, flags=re.I)
if m:
name = m.group(1).strip()
name = re.sub(r"[【】\[\]()]", "", name).strip()
if name:
return safe_segment(name)
stem = Path(path).stem
stem = re.sub(r"^(ql_|jd_|jx_|ks_|wx_|applet_)", "", stem, flags=re.I)
return safe_segment(stem)
def classify(content: str, path: str) -> Tuple[str, str, str, str]:
text = content
lower = text.lower()
evidence = []
is_wx_protocol = any(k in lower for k in [
"getminiprogramcode", "wx_cloud", "wx_token", "miniprogramcode", "养鸡场", "wx.login", "code2session"
])
is_wx = is_wx_protocol or any(k in lower for k in [
"appid", "openid", "session_key", "encrypteddata", "小程序", "wechat/login", "wxappid"
])
has_pwd = any(k in lower for k in [
"password", "passwd", "pwd", "username", "user_name", "account", "账密", "手机号登录", "mobile", "smscode", "验证码"
]) and any(k in lower for k in ["login", "登录", "/auth", "/user/login"])
has_capture = any(k in lower for k in [
"cookie", "authorization", "bearer", "token", "x-token", "session", "抓包", "header", "headers", "ck"
])
web_hint = any(k in lower for k in [
"web", "h5", "pc", "browser", "chrome", "网页登录", "www.", "passport", "csrf"
])
app_hint = any(k in lower for k in [
"app", "android", "ios", "okhttp", "x-requested-with", "user-agent", "deviceid", "imei", "oaid"
])
if is_wx_protocol:
evidence.append("getMiniProgramCode/wx_cloud/wx_token/code协议链路")
return "微信小程序/协议版", "微信小程序", "协议版", "; ".join(evidence)
if is_wx:
evidence.append("appid/openid/session_key/encryptedData等小程序字段")
return "微信小程序/抓包版", "微信小程序", "抓包版", "; ".join(evidence)
if web_hint and has_pwd:
evidence.append("web/H5关键词 + username/password/login")
return "web版/账密", "web版", "账密", "; ".join(evidence)
if web_hint and has_capture:
evidence.append("web/H5关键词 + cookie/token/header")
return "web版/抓包", "web版", "抓包", "; ".join(evidence)
if has_pwd:
evidence.append("username/password/mobile/login")
return "APP版/账密", "APP版", "账密", "; ".join(evidence)
if has_capture:
evidence.append("cookie/token/authorization/header")
return "APP版/抓包", "APP版", "抓包", "; ".join(evidence)
if app_hint:
evidence.append("APP/Android/iOS/device关键词")
return "APP版/抓包", "APP版", "抓包", "; ".join(evidence)
return "未分类", "未分类", "未分类", "规则未命中"
def unique_filename(project_dir: Path, uploaded_at: str, original: str, sha: str) -> str:
date = (uploaded_at or "unknown")[:10] or "unknown"
stem = safe_segment(Path(original).stem, "script", 60)
suffix = Path(original).suffix or ".txt"
name = f"{date}_{stem}_{sha[:8]}{suffix}"
i = 2
while (project_dir / name).exists():
name = f"{date}_{stem}_{sha[:8]}_{i}{suffix}"
i += 1
return name
def write_record_file(record: Record, content: str):
out_path = ROOT / record.saved_path
out_path.parent.mkdir(parents=True, exist_ok=True)
header = (
f"# Source: {record.source_url}\n"
f"# Raw: {record.raw_url}\n"
f"# Repo: {record.source_repo}\n"
f"# Path: {record.source_path}\n"
f"# UploadedAt: {record.uploaded_at}\n"
f"# SHA256: {record.sha256}\n"
f"# Category: {record.category}\n"
f"# Evidence: {record.evidence}\n\n"
)
if out_path.suffix.lower() == ".py":
header = header
elif out_path.suffix.lower() in {".js", ".ts"}:
header = "// " + header.replace("\n", "\n// ").rstrip("// ") + "\n"
elif out_path.suffix.lower() == ".sh":
header = header
out_path.write_text(header + content, encoding="utf-8", errors="ignore")
def load_seen_hashes() -> set:
if INDEX_JSON.exists():
try:
arr = json.loads(INDEX_JSON.read_text(encoding="utf-8"))
return {x.get("sha256") for x in arr if x.get("sha256")}
except Exception:
return set()
return set()
def save_indexes(records: List[Record]):
existing = []
if INDEX_JSON.exists():
try:
existing = json.loads(INDEX_JSON.read_text(encoding="utf-8"))
except Exception:
existing = []
by_sha = {x.get("sha256"): x for x in existing if x.get("sha256")}
for r in records:
by_sha[r.sha256] = asdict(r)
arr = sorted(by_sha.values(), key=lambda x: (x.get("category", ""), x.get("project", ""), x.get("uploaded_at", "")))
INDEX_JSON.write_text(json.dumps(arr, ensure_ascii=False, indent=2), encoding="utf-8")
with INDEX_CSV.open("w", newline="", encoding="utf-8-sig") as f:
fields = list(asdict(records[0]).keys()) if records else [
"category", "project", "filename", "sha256", "source_platform", "source_repo", "source_path",
"source_url", "raw_url", "uploaded_at", "saved_path", "auth_type", "platform_type", "evidence"
]
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
for item in arr:
w.writerow(item)
def update_readme():
arr = []
if INDEX_JSON.exists():
arr = json.loads(INDEX_JSON.read_text(encoding="utf-8"))
counts = {c: 0 for c in CATEGORY_ORDER}
projects = {}
for x in arr:
counts[x.get("category", "未分类")] = counts.get(x.get("category", "未分类"), 0) + 1
projects.setdefault(x.get("category", "未分类"), set()).add(x.get("project", "unknown"))
lines = [
"# 青龙脚本分类库\n",
"\n",
"本仓库用于归档公开青龙脚本,并按运行端与凭证方式分类。\n",
"\n",
"## 分类\n",
"\n",
]
for c in CATEGORY_ORDER:
lines.append(f"- `{c}`{counts.get(c,0)} 个脚本,{len(projects.get(c,set()))} 个项目\n")
lines += [
"\n",
"## 目录规则\n",
"\n",
"```text\n",
"脚本库/APP版/抓包/<项目>/YYYY-MM-DD_原文件名_<sha8>.ext\n",
"脚本库/APP版/账密/<项目>/YYYY-MM-DD_原文件名_<sha8>.ext\n",
"脚本库/web版/抓包/<项目>/YYYY-MM-DD_原文件名_<sha8>.ext\n",
"脚本库/web版/账密/<项目>/YYYY-MM-DD_原文件名_<sha8>.ext\n",
"脚本库/微信小程序/协议版/<项目>/YYYY-MM-DD_原文件名_<sha8>.ext\n",
"脚本库/微信小程序/抓包版/<项目>/YYYY-MM-DD_原文件名_<sha8>.ext\n",
"```\n",
"\n",
"## 元数据\n",
"\n",
"- `INDEX.csv`:脚本索引,含分类、项目、源仓库、源路径、源 URL、上传时间、SHA256。\n",
"- `INDEX.json`同上JSON 格式。\n",
"- 文件名中的日期优先使用源文件最近一次 commit 时间。\n",
"- 相同代码以归一化 SHA256 去重。\n",
"\n",
"## 采集器\n",
"\n",
"运行:\n",
"\n",
"```bash\n",
"python3 collector.py\n",
"```\n",
"\n",
"可选环境变量:\n",
"\n",
"```bash\n",
"export GITHUB_TOKEN=你的GitHubToken # 可提升 GitHub API 限额\n",
"export MAX_REPOS_PER_QUERY=20\n",
"export MAX_FILES_TOTAL=500\n",
"```\n",
]
README.write_text("".join(lines), encoding="utf-8")
def add_content_record(
records: List[Record],
seen: set,
content: str,
source_platform: str,
source_repo: str,
source_path: str,
source_url: str,
raw_url: str,
uploaded_at: str,
) -> bool:
if not content or not looks_like_ql_script(content, source_path):
return False
sha = normalized_hash(content)
if sha in seen:
return False
category, platform_type, auth_type, evidence = classify(content, source_path)
project = extract_project(content, source_path)
project_dir = OUT_DIR / category / project
fname = unique_filename(project_dir, uploaded_at, Path(source_path).name, sha)
saved_path = str((project_dir / fname).relative_to(ROOT))
rec = Record(
category=category,
project=project,
filename=fname,
sha256=sha,
source_platform=source_platform,
source_repo=source_repo,
source_path=source_path,
source_url=source_url,
raw_url=raw_url,
uploaded_at=uploaded_at,
saved_path=saved_path,
auth_type=auth_type,
platform_type=platform_type,
evidence=evidence,
)
write_record_file(rec, content)
records.append(rec)
seen.add(sha)
print(f" [ADD] {category} / {project} / {fname}")
return True
def collect_github(records: List[Record], seen: set, files_done: int) -> int:
repos = search_github_repos()
print(f"[INFO] github_repos={len(repos)}")
for repo in repos:
if files_done >= MAX_FILES_TOTAL:
break
full = repo.get("full_name")
if not full:
continue
full = str(full)
branch = str(repo.get("default_branch") or "main")
print(f"[GITHUB_REPO] {full}")
tree = list_github_tree(repo)
candidates = [x for x in tree if x.get("type") == "blob" and is_candidate_path(x.get("path", ""))]
candidates.sort(key=lambda x: (
0 if re.search(r"(?i)(script|scripts|ql|青龙|jd_|wx_|applet|env)", x.get("path", "")) else 1,
len(x.get("path", ""))
))
for item in candidates[:80]:
if files_done >= MAX_FILES_TOTAL:
break
path = item.get("path", "")
content, raw_url, html_url = get_file_content(full, path, branch)
uploaded_at = get_file_uploaded_at(full, path, repo.get("pushed_at", "")) if content else ""
if add_content_record(records, seen, content or "", "GitHub", full, path, html_url, raw_url, uploaded_at):
files_done += 1
return files_done
def collect_gitee(records: List[Record], seen: set, files_done: int) -> int:
repos = search_gitee_repos()
print(f"[INFO] gitee_repos={len(repos)}")
for repo in repos:
if files_done >= MAX_FILES_TOTAL:
break
full = repo.get("full_name") or repo.get("path_with_namespace")
if not full:
continue
full = str(full)
branch = str(repo.get("default_branch") or "master")
print(f"[GITEE_REPO] {full}")
tree = list_gitee_tree(repo)
branch = str(repo.get("default_branch") or branch)
candidates = [x for x in tree if x.get("type") in ("blob", "file") and is_candidate_path(x.get("path", ""))]
candidates.sort(key=lambda x: (
0 if re.search(r"(?i)(script|scripts|ql|青龙|jd_|wx_|applet|env)", x.get("path", "")) else 1,
len(x.get("path", ""))
))
for item in candidates[:80]:
if files_done >= MAX_FILES_TOTAL:
break
path = item.get("path", "")
content, raw_url, html_url = get_gitee_file_content(full, path, branch)
uploaded_at = get_gitee_file_uploaded_at(full, path, repo.get("pushed_at", "")) if content else ""
if add_content_record(records, seen, content or "", "Gitee", full, path, html_url, raw_url, uploaded_at):
files_done += 1
return files_done
def collect_public_links(records: List[Record], seen: set, files_done: int) -> int:
links = discover_public_code_links()
print(f"[INFO] public_links={len(links)}")
for item in links:
if files_done >= MAX_FILES_TOTAL:
break
url = item.get("url", "")
content, raw_url, html_url, uploaded_at = get_public_link_content(url)
source_repo = urllib.parse.urlparse(url).netloc
source_path = urllib.parse.urlparse(url).path.strip("/") or Path(url).name
if add_content_record(records, seen, content or "", "PublicWeb", source_repo, source_path, html_url, raw_url, uploaded_at):
files_done += 1
return files_done
def rebuild_index_from_files():
"""当采集被限流/中断后,可从已落盘脚本头部重建 INDEX。"""
records = []
for path in OUT_DIR.rglob("*"):
if not path.is_file() or path.suffix.lower() not in VALID_SUFFIXES:
continue
text = path.read_text(encoding="utf-8", errors="ignore")
head = "\n".join(text.splitlines()[:12])
def pick(name):
m = re.search(rf"{name}:\s*(.+)", head)
return m.group(1).strip().lstrip("// ").strip() if m else ""
rel = str(path.relative_to(ROOT))
parts = path.relative_to(OUT_DIR).parts
if len(parts) < 4:
continue
category = f"{parts[0]}/{parts[1]}" if parts[0] in {"APP版", "web版", "微信小程序"} else parts[0]
project = parts[2] if len(parts) >= 4 else "unknown"
rec = Record(
category=category,
project=project,
filename=path.name,
sha256=pick("SHA256") or normalized_hash(text),
source_platform="GitHub",
source_repo=pick("Repo"),
source_path=pick("Path"),
source_url=pick("Source"),
raw_url=pick("Raw"),
uploaded_at=pick("UploadedAt"),
saved_path=rel,
auth_type=category.split("/")[-1] if "/" in category else "未分类",
platform_type=category.split("/")[0] if "/" in category else "未分类",
evidence=pick("Evidence"),
)
records.append(rec)
save_indexes(records)
update_readme()
print(f"[REBUILD] records={len(records)}")
def main():
OUT_DIR.mkdir(parents=True, exist_ok=True)
META_DIR.mkdir(parents=True, exist_ok=True)
seen = load_seen_hashes()
records: List[Record] = []
files_done = 0
try:
files_done = collect_github(records, seen, files_done)
except RuntimeError as e:
if str(e) == "github_rate_limited":
print("[WARN] GitHub 限流,跳过 GitHub 剩余采集,继续其它来源")
else:
raise
if files_done < MAX_FILES_TOTAL:
files_done = collect_gitee(records, seen, files_done)
if files_done < MAX_FILES_TOTAL:
files_done = collect_public_links(records, seen, files_done)
if records:
save_indexes(records)
update_readme()
elif any(OUT_DIR.rglob("*")):
rebuild_index_from_files()
else:
save_indexes(records)
update_readme()
print(f"[DONE] added={len(records)} total_files_done={files_done}")
if __name__ == "__main__":
main()