Files
BeacherZ 2c61b727cb feat: 项目改名为 dailycheckin-plus,README 补 docker run 用法,通知标题带项目名
- 品牌字串全面更换:README / compose / Dockerfile / entrypoint / 面板页面 / CI。
  镜像名变为 ghcr.io/beacherz/dailycheckin-plus:latest;compose 里的服务名与
  container_name 保持 dailycheckin 不动 —— 改了会变成新旧两个容器同时跑,通知收两条。
- 通知标题带上项目名:NOTICE_TITLE 与 main.py 的标题都改用 PROJECT_NAME,
  实际发出来是「【dailycheckin-plus 每日签到 · 日期】」;企业微信卡片署名同步。
  这两处仍在 patches/0002 里,已重新生成并验证「重放后与工作区逐字节一致」。
- 面板:默认 PANEL_TITLE 改为 dailycheckin-plus,浏览器标签页与顶栏品牌跟着走
  (顺手把顶栏一直没被填充的面板版本/引擎状态补上);HTTP Server 头改为
  dailycheckin-plus-panel/2.1.0。
- crontab 托管标记改为 dailycheckin-plus managed,但**仍兼容老标记**并在保存时
  升级;crontab 里若存在裸的签到行,首次保存会整行注释留档后再接管,避免和新任务
  并存导致一天跑两遍、通知收两条。新增 tools/test_schedule.py(27 项断言)锁这条行为。
- README:快速开始拆成 docker compose / docker run 两种方式(含 Windows 路径写法和
  docker run 的升级步骤),更新通知样例、环境变量表、目录结构与常见问题。
- Dockerfile 增加「面板品牌自检」,装进镜像的网页必须是改名后的版本。
2026-10-15 20:54:00 -07:00

645 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""跟随上游更新签到引擎。
本项目的签到引擎是把 https://github.com/Sitoi/dailycheckin 的源码**整份内置**在
`dailycheckin/` 目录里(而不是从 PyPI 装),好处是构建可复现、离线能编译、补丁可控;
代价是上游更新不会自己流进来,必须显式同步一次 —— 这个脚本就是干这个的。
为什么不能手工把上游 `dailycheckin/` 目录整个盖过来?
因为本项目的引擎上有 **2 个本地补丁**(见 `patches/`),直接覆盖会把它们悄悄抹掉:
* 0001-bilibili-newlist B 站旧接口下线,改 newlist + 空值兜底
* 0002-notice-single-message 通知恒定只发一条(标题带项目名、分隔行、项目地址)
本脚本会在替换引擎源码之后**自动重放这些补丁**,任何一个补丁打不上就直接回滚,
绝不留下一个「改了一半」的引擎。
用法::
python tools/sync_upstream.py --check # 只查上游有没有更新,不联网写文件
python tools/sync_upstream.py # 同步到 UPSTREAM.json 记录的 ref(默认 main)
python tools/sync_upstream.py --ref 25.12.9 # 同步到指定 tag / 分支 / commit
python tools/sync_upstream.py --ref main --dry-run # 走完整流程但不落地
退出码:0 = 成功(或已是最新);1 = 失败(补丁冲突、网络异常等);2 = `--check` 发现上游有更新。
"""
from __future__ import annotations
import argparse
import hashlib
import io
import json
import os
import re
import shutil
import sys
import tarfile
import tempfile
import time
import urllib.error
import urllib.request
# --------------------------------------------------------------------------- #
# 常量
# --------------------------------------------------------------------------- #
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ENGINE_DIR = os.path.join(ROOT, "dailycheckin")
PATCH_DIR = os.path.join(ROOT, "patches")
STATE_PATH = os.path.join(ROOT, "UPSTREAM.json")
CATALOG_PATH = os.path.join(ROOT, "panel", "catalog.py")
DEFAULT_REPO = "Sitoi/dailycheckin"
DEFAULT_REF = "main"
# 非引擎文件:我们改了就不自动覆盖,只提示;没改过就跟着上游走
TRACKED_FILES = ["requirements.txt", "pyproject.toml", "setup.py", "LICENSE"]
PROTECTED_FILES = {"setup.py"} # 本地有意改动过(find_packages 排除 panel)
SKIP_NAMES = {"__pycache__", ".DS_Store"}
# --------------------------------------------------------------------------- #
# 小工具
# --------------------------------------------------------------------------- #
def log(msg: str = "") -> None:
print(msg, flush=True)
def step(msg: str) -> None:
log("\n==> " + msg)
def sha256(path: str) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def api_get(path: str, repo: str = DEFAULT_REPO):
url = "https://api.github.com/repos/%s%s" % (repo, path)
req = urllib.request.Request(url, headers={
"Accept": "application/vnd.github+json",
"User-Agent": "dailycheckin-plus-sync",
})
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
if token:
req.add_header("Authorization", "Bearer " + token)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode("utf-8"))
def download_tarball(ref: str, repo: str = DEFAULT_REPO) -> bytes:
url = "https://api.github.com/repos/%s/tarball/%s" % (repo, ref)
req = urllib.request.Request(url, headers={"User-Agent": "dailycheckin-plus-sync"})
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
if token:
req.add_header("Authorization", "Bearer " + token)
with urllib.request.urlopen(req, timeout=600) as resp:
return resp.read()
def extract(tar_bytes: bytes, dest: str) -> str:
"""解包,返回包内顶层目录的绝对路径。"""
os.makedirs(dest, exist_ok=True)
with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:gz") as tf:
names = tf.getnames()
if not names:
raise RuntimeError("源码包是空的")
top = names[0].split("/")[0]
try:
tf.extractall(dest, filter="data") # Python 3.12+
except TypeError:
tf.extractall(dest)
return os.path.join(dest, top)
def _chmod(path: str, mode: int) -> None:
try:
os.chmod(path, mode)
except OSError:
pass # Windows 上权限位语义有限,忽略
def copy_tree(src: str, dst: str, file_mode: int = 0o644, dir_mode: int = 0o755) -> int:
"""把 src 里的内容复制进 dst(不保留旧的残留文件),并归一化权限位。
权限位必须显式归一化:上游 tarball 里有一部分文件带可执行位,原样拷过来的话,
在 Linux 上 `git add` 会记录一堆「内容一个字没改、只有模式从 100644 变 100755」
的假改动(Windows 上看不出来,因为 NTFS 没有 exec 位)。本项目的引擎文件
一律 0644。
"""
if os.path.exists(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst, ignore=shutil.ignore_patterns(*SKIP_NAMES, "*.pyc"))
count = 0
for root, dirs, files in os.walk(dst):
for name in dirs:
_chmod(os.path.join(root, name), dir_mode)
for name in files:
_chmod(os.path.join(root, name), file_mode)
count += 1
return count
# --------------------------------------------------------------------------- #
# 站点目录探测
# --------------------------------------------------------------------------- #
def site_dirs(engine_root: str) -> dict:
"""从引擎源码目录里找出所有站点模块,返回 {大写KEY: 中文名}。
站点模块 = `dailycheckin/<name>/main.py`,中文名取 `name = "..."`。
这与引擎自身的 `pkgutil.walk_packages` 自动发现逻辑一致。
"""
out = {}
if not os.path.isdir(engine_root):
return out
for name in sorted(os.listdir(engine_root)):
if name.startswith(".") or name in SKIP_NAMES or name == "utils":
continue
main_py = os.path.join(engine_root, name, "main.py")
if not os.path.isfile(main_py):
continue
label = ""
try:
with open(main_py, encoding="utf-8", errors="replace") as f:
m = re.search(r'^\s{4}name\s*=\s*["\'](.+?)["\']', f.read(), re.M)
if m:
label = m.group(1)
except OSError:
pass
out[name.upper()] = label
return out
def site_dirs_from_api(sha: str, repo: str = DEFAULT_REPO) -> dict:
"""不下载源码,用 trees 接口列出站点模块(--check 用,秒回)。"""
data = api_get("/git/trees/%s?recursive=1" % sha, repo)
out = {}
pat = re.compile(r"^dailycheckin/([^/]+)/main\.py$")
for item in data.get("tree", []):
m = pat.match(item.get("path", ""))
if m and m.group(1) != "utils":
out[m.group(1).upper()] = ""
return out
def catalog_keys() -> set:
"""读 panel/catalog.py 里已经写了元数据的站点(避免 import 引入依赖)。"""
if not os.path.isfile(CATALOG_PATH):
return set()
with open(CATALOG_PATH, encoding="utf-8") as f:
text = f.read()
m = re.search(r"^SITE_META[^=]*=\s*\{(.*?)^\}", text, re.S | re.M)
if not m:
return set()
return set(re.findall(r'^\s{4}"([A-Za-z0-9_]+)"\s*:', m.group(1), re.M))
# --------------------------------------------------------------------------- #
# unified diff 应用(自带实现,不依赖 git / patch 命令)
# --------------------------------------------------------------------------- #
HUNK_RE = re.compile(r"^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@")
def _strip_ab(path: str) -> str:
path = path.strip().split("\t")[0].strip()
for prefix in ("a/", "b/"):
if path.startswith(prefix):
return path[len(prefix):]
return path
def parse_udiff(text: str):
"""解析 unified diff,返回 [(相对路径, [(起始行, 旧行, 新行), ...]), ...]。"""
lines = text.splitlines()
files, cur = [], None
i = 0
while i < len(lines):
line = lines[i]
if line.startswith("--- "):
# 正常情况下一行是 +++,但补丁被人手工编辑过时可能不是,
# 所以只在确实看到 +++ 时才跳两行,避免把紧随其后的 hunk 头吞掉。
if i + 1 < len(lines) and lines[i + 1].startswith("+++ "):
rel = _strip_ab(lines[i + 1][4:])
i += 2
else:
rel = _strip_ab(line[4:])
i += 1
cur = (rel, [])
files.append(cur)
continue
if line.startswith("@@ ") and cur is not None:
m = HUNK_RE.match(line)
if not m:
i += 1
continue
start = int(m.group(1))
i += 1
old, new = [], []
while i < len(lines) and not lines[i].startswith(("@@ ", "--- ")):
raw = lines[i]
if raw.startswith("+"):
new.append(raw[1:])
elif raw.startswith("-"):
old.append(raw[1:])
elif raw.startswith(" "):
old.append(raw[1:])
new.append(raw[1:])
elif raw.startswith("\\"):
pass
elif raw == "":
old.append("")
new.append("")
else:
break
i += 1
cur[1].append((start, old, new))
continue
i += 1
return [(p, h) for p, h in files if h]
def _match(lines, old, pos) -> bool:
if pos < 0 or pos + len(old) > len(lines):
return False
return all(lines[pos + k] == old[k] for k in range(len(old)))
def _match_loose(lines, old, pos) -> bool:
"""忽略行尾空白的宽松匹配,用来吸收上游的格式化改动。"""
if pos < 0 or pos + len(old) > len(lines):
return False
return all(lines[pos + k].rstrip() == old[k].rstrip() for k in range(len(old)))
def _locate(lines, old, hint: int) -> int:
"""定位 hunk。先试补丁给的行号,再前后扫描,最后放宽行尾空白。"""
for matcher in (_match, _match_loose):
if matcher(lines, old, hint):
return hint
for offset in range(1, len(lines) + 1):
for pos in (hint + offset, hint - offset):
if matcher(lines, old, pos):
return pos
return -1
def apply_udiff_file(path: str, hunks) -> None:
"""把 hunks 应用到 path 上,失败抛 RuntimeError。"""
with open(path, encoding="utf-8") as f:
text = f.read()
had_newline = text.endswith("\n")
lines = text.splitlines()
offset = 0
for start, old, new in hunks:
pos = _locate(lines, old, max(0, start - 1 + offset))
if pos < 0:
preview = "\n".join(" " + l for l in old[:6])
raise RuntimeError(
"补丁无法应用:%s\n 补丁期望的原文找不到(上游这段代码被改过了),片段:\n%s"
% (os.path.relpath(path, ROOT), preview)
)
lines[pos:pos + len(old)] = new
offset += len(new) - len(old)
out = "\n".join(lines) + ("\n" if had_newline else "")
with open(path, "w", encoding="utf-8", newline="") as f:
f.write(out)
def apply_patches(root: str, patch_dir: str = PATCH_DIR) -> list:
"""按文件名顺序重放 patches/*.patch,返回已应用的补丁名列表。"""
applied = []
if not os.path.isdir(patch_dir):
return applied
names = sorted(n for n in os.listdir(patch_dir) if n.endswith(".patch"))
for name in names:
with open(os.path.join(patch_dir, name), encoding="utf-8") as f:
text = f.read()
for rel, hunks in parse_udiff(text):
target = os.path.join(root, rel)
if not os.path.isfile(target):
raise RuntimeError("补丁 %s 的目标文件不存在:%s" % (name, rel))
apply_udiff_file(target, hunks)
applied.append(name)
log(" 已重放 %s" % name)
return applied
# --------------------------------------------------------------------------- #
# 状态文件
# --------------------------------------------------------------------------- #
def read_state() -> dict:
if not os.path.isfile(STATE_PATH):
return {}
try:
with open(STATE_PATH, encoding="utf-8") as f:
return json.load(f)
except (OSError, ValueError):
return {}
def write_state(state: dict) -> None:
tmp = STATE_PATH + ".tmp"
with open(tmp, "w", encoding="utf-8", newline="\n") as f:
json.dump(state, f, ensure_ascii=False, indent=2)
f.write("\n")
os.replace(tmp, STATE_PATH)
def engine_version(engine_dir: str) -> str:
path = os.path.join(engine_dir, "__version__.py")
try:
with open(path, encoding="utf-8") as f:
m = re.search(r'__version__\s*=\s*["\']([^"\']+)', f.read())
return m.group(1) if m else "?"
except OSError:
return "?"
def _same_except_time(old: dict, new: dict) -> bool:
"""除 synced_at 外,两份溯源信息是否一致。"""
if not old:
return False
drop = lambda d: {k: v for k, v in d.items() if k != "synced_at"} # noqa: E731
return drop(old) == drop(new)
# --------------------------------------------------------------------------- #
# 报告
# --------------------------------------------------------------------------- #
def report_sites(old: dict, new: dict) -> bool:
"""打印站点目录变化,返回是否有变化。"""
added = sorted(set(new) - set(old))
removed = sorted(set(old) - set(new))
if not added and not removed:
log(" 站点目录没有变化(共 %d 个)" % len(new))
return False
if added:
log(" 上游新增站点 %d 个:" % len(added))
for key in added:
log(" + %-12s %s" % (key, new.get(key) or ""))
if removed:
log(" 上游移除站点 %d 个:" % len(removed))
for key in removed:
log(" - %-12s %s" % (key, old.get(key) or ""))
return True
def report_meta_gaps(sites: dict) -> list:
"""哪些站点还没有 panel/catalog.py 元数据(会退回通用配置)。"""
known = catalog_keys()
gaps = sorted(k for k in sites if k not in known)
if gaps:
log(" ⚠ 以下 %d 个站点在 panel/catalog.py 里没有元数据:" % len(gaps))
for key in gaps:
log(" %-12s %s" % (key, sites.get(key) or ""))
log(" 也能正常签到,只是面板不知道它的必填 cookie 字段、没有在线校验、")
log(" LOGO 会退回 emoji。建议照着文件里已有的写法补一段。")
else:
log(" 所有站点都有 panel/catalog.py 元数据")
return gaps
# --------------------------------------------------------------------------- #
# --check
# --------------------------------------------------------------------------- #
def do_check(args) -> int:
state = read_state()
ref = args.ref or state.get("ref") or DEFAULT_REF
repo = args.repo or state.get("repo") or DEFAULT_REPO
step("检查上游 %s 的 %s" % (repo, ref))
try:
info = api_get("/commits/" + ref, repo)
except urllib.error.HTTPError as exc:
log(" 查询失败:HTTP %s(ref %s 不存在?)" % (exc.code, ref))
return 1
except Exception as exc: # noqa: BLE001
log(" 查询失败:%s: %s" % (type(exc).__name__, exc))
return 1
sha = info["sha"]
date = info["commit"]["committer"]["date"]
recorded = state.get("commit") or ""
log(" 上游最新提交 : %s (%s)" % (sha[:10], date))
log(" 本项目记录于 : %s" % (recorded[:10] or "(未记录)"))
log(" 内置引擎版本 : %s" % (state.get("version") or "?"))
upstream_sites = site_dirs_from_api(sha, repo)
local_sites = site_dirs(ENGINE_DIR)
log(" 上游站点模块 : %d 个" % len(upstream_sites))
changed = report_sites(local_sites, upstream_sites)
if sha == recorded and not changed:
log("\n结论:已是最新,无需同步。")
return 0
if sha == recorded:
log("\n结论:引擎提交没变,但站点目录有差异(本地可能有手工调整)。")
else:
log("\n结论:上游有更新,可以同步:")
log(" python tools/sync_upstream.py --ref %s" % ref)
return 2
# --------------------------------------------------------------------------- #
# 同步
# --------------------------------------------------------------------------- #
def do_sync(args) -> int:
state = read_state()
ref = args.ref or state.get("ref") or DEFAULT_REF
repo = args.repo or state.get("repo") or DEFAULT_REPO
step("同步 %s@%s" % (repo, ref))
try:
info = api_get("/commits/" + ref, repo)
sha = info["sha"]
commit_date = info["commit"]["committer"]["date"]
except Exception as exc: # noqa: BLE001
log(" 读取提交信息失败:%s: %s" % (type(exc).__name__, exc))
return 1
log(" 提交 %s (%s)" % (sha[:10], commit_date))
old_sites = site_dirs(ENGINE_DIR)
old_version = engine_version(ENGINE_DIR)
log(" 当前内置引擎版本 %s,站点 %d 个" % (old_version, len(old_sites)))
step("下载上游源码")
t0 = time.time()
try:
blob = download_tarball(sha, repo)
except Exception as exc: # noqa: BLE001
log(" 下载失败:%s: %s" % (type(exc).__name__, exc))
return 1
log(" %.1f KB,用时 %.1f 秒" % (len(blob) / 1024, time.time() - t0))
with tempfile.TemporaryDirectory(prefix="dc-sync-") as tmp:
top = extract(blob, tmp)
up_engine = os.path.join(top, "dailycheckin")
if not os.path.isfile(os.path.join(up_engine, "main.py")):
log(" 源码包里找不到 dailycheckin/main.py,可能上游改了目录结构")
return 1
new_sites = site_dirs(up_engine)
new_version = engine_version(up_engine)
log(" 上游引擎版本 %s,站点 %d 个" % (new_version, len(new_sites)))
if args.dry_run:
step("--dry-run:只比对,不落地")
report_sites(old_sites, new_sites)
report_meta_gaps(new_sites)
_report_tracked(top, state)
log("\n(没有写入任何文件)")
return 0
step("替换引擎源码")
backup = os.path.join(tmp, "_backup_engine")
if os.path.isdir(ENGINE_DIR):
shutil.copytree(ENGINE_DIR, backup, ignore=shutil.ignore_patterns(*SKIP_NAMES, "*.pyc"))
count = copy_tree(up_engine, ENGINE_DIR)
log(" 已写入 %d 个文件到 dailycheckin/" % count)
step("重放本地补丁")
try:
if args.no_patch:
log(" --no-patch:跳过(引擎将不带本项目补丁,仅供调试)")
applied = []
else:
# 补丁里的路径是仓库相对的(dailycheckin/...),所以根目录传 ROOT 而不是 ENGINE_DIR
applied = apply_patches(ROOT)
if not applied:
log(" 没有找到补丁文件,引擎为上游原样")
except RuntimeError as exc:
log(" ✗ " + str(exc))
log("\n 正在回滚到同步前的引擎……")
copy_tree(backup, ENGINE_DIR)
log(" 已回滚,dailycheckin/ 未被改动。")
log("\n 处理办法:打开上面的补丁文件,按上游新代码重新对一遍,")
log(" 改好 patches/ 里的 diff 后重跑本脚本。")
return 1
step("核对非引擎文件")
file_info = _sync_tracked_files(top, state)
step("写入 UPSTREAM.json")
patches = sorted(n for n in os.listdir(PATCH_DIR) if n.endswith(".patch")) if os.path.isdir(PATCH_DIR) else []
synced_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
new_state = {
"repo": repo,
"ref": ref,
"commit": sha,
"commit_date": commit_date,
"version": new_version,
"synced_at": synced_at,
"sync_tool": "tools/sync_upstream.py",
"patches": patches,
"engine_sites": sorted(new_sites),
"tracked_files": file_info,
"note": "引擎源码来自上游,patches/ 里的补丁在同步后自动重放。不要手工覆盖 dailycheckin/。",
}
# 上游没动、补丁也没变的时候,只刷新 synced_at 会白造一次提交
# (CI 是靠 git diff 判断「有没有变化」的),所以这里沿用上一次的时间戳。
old_state = read_state()
if _same_except_time(old_state, new_state):
new_state["synced_at"] = old_state.get("synced_at") or synced_at
log(" 上游内容与上次同步一致,保留原有的同步时间戳")
write_state(new_state)
log(" 已写入 %s" % os.path.relpath(STATE_PATH, ROOT).replace("\\", "/"))
step("结果")
report_sites(old_sites, new_sites)
gaps = report_meta_gaps(site_dirs(ENGINE_DIR))
log("")
log("引擎已更新:%s → %s" % (old_version, new_version))
log("下一步:")
log(" 1) 看一眼改了什么:git diff --stat")
if gaps:
log(" 2) 给上面列出的新站点补 panel/catalog.py 元数据(可选,但不补体验会打折)")
log(" 3) git add -A && git commit -m \"chore: 同步上游引擎 %s\" && git push" % new_version)
else:
log(" 2) git add -A && git commit -m \"chore: 同步上游引擎 %s\" && git push" % new_version)
log(" 推送后 CI 会自动重建并发布 ghcr.io 镜像。")
return 0
def _report_tracked(top: str, state: dict) -> None:
for rel in TRACKED_FILES:
src = os.path.join(top, rel)
if not os.path.isfile(src):
continue
up_hash = sha256(src)
local = os.path.join(ROOT, rel)
local_hash = sha256(local) if os.path.isfile(local) else ""
recorded = (state.get("tracked_files") or {}).get(rel, {}).get("upstream_sha256")
flag = []
if recorded and recorded != up_hash:
flag.append("上游已改动")
if up_hash != local_hash:
flag.append("本地与上游不同")
log(" %-18s %s" % (rel, "、".join(flag) or "一致"))
def _sync_tracked_files(top: str, state: dict) -> dict:
"""上游改过的非引擎文件:本地没定制过就跟着更新,定制过的只提示。"""
info = {}
for rel in TRACKED_FILES:
src = os.path.join(top, rel)
if not os.path.isfile(src):
continue
up_hash = sha256(src)
local = os.path.join(ROOT, rel)
local_hash = sha256(local) if os.path.isfile(local) else ""
recorded = (state.get("tracked_files") or {}).get(rel, {}).get("upstream_sha256")
entry = {"upstream_sha256": up_hash}
if rel in PROTECTED_FILES:
entry["local"] = "modified"
if recorded and recorded != up_hash:
log(" ⚠ %s 上游也改了,但本地有定制(%s),没有自动覆盖,请人工合并"
% (rel, "、".join(PROTECTED_FILES_REASON.get(rel, ["本地改动"]))))
else:
log(" %-18s 保留本地版本(有意改动)" % rel)
elif up_hash == local_hash:
entry["local"] = "same"
log(" %-18s 与上游一致" % rel)
else:
shutil.copyfile(src, local)
entry["local"] = "synced"
log(" %-18s 已更新为上游版本" % rel)
info[rel] = entry
return info
PROTECTED_FILES_REASON = {
"setup.py": ["find_packages 里排除了 panel,避免把面板打进 wheel"],
}
# --------------------------------------------------------------------------- #
def main() -> int:
parser = argparse.ArgumentParser(
description="同步上游 Sitoi/dailycheckin 引擎,并自动重放本项目补丁",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--check", action="store_true", help="只检查上游有没有更新,不下载不改文件")
parser.add_argument("--ref", default=None, help="上游 tag / 分支 / commit(默认沿用 UPSTREAM.json 里的,再默认 main)")
parser.add_argument("--repo", default=None, help="上游仓库,默认 Sitoi/dailycheckin")
parser.add_argument("--dry-run", action="store_true", help="下载并比对,但不写入任何文件")
parser.add_argument("--no-patch", action="store_true", help="跳过补丁重放(调试用,引擎将不带本项目补丁)")
args = parser.parse_args()
if not os.path.isdir(ENGINE_DIR):
log("找不到引擎目录:%s(请在仓库根目录运行本脚本)" % ENGINE_DIR)
return 1
if args.check:
return do_check(args)
return do_sync(args)
if __name__ == "__main__":
sys.exit(main())