feat: v1.2.0 — Gitea 地址可更改(默认 http://127.0.0.1:3000)

- 设置页新增「Gitea 地址」输入:同步引擎的 API/克隆/拉取全部改用该地址,
  适配 Gitea 换了端口或部署在别处的场景
- 地址规范化:自动补 http://、去尾部斜杠,校验 scheme/主机名,非法值明确拒绝
- 保存前探测连通性(/api/v1/version),连不上拒绝保存并提示
- 改地址后自动把已有镜像仓库的 origin 重指到新地址,无需重新克隆
- 「打开 Gitea」按钮随设置端口 + 访问面板的主机名自适应(不再写死 3000)
- 改地址/用户名不再误清 Gitea 密码(密码仅在密码行保存时提交)
- 启动日志记录当前 Gitea 地址,便于排障
- manifest 1.1.2 → 1.2.0
This commit is contained in:
cbb
2026-10-13 19:59:45 +08:00
parent 7260f402d3
commit 870612ff93
3 changed files with 150 additions and 34 deletions
+142 -31
View File
@@ -23,7 +23,7 @@ import uuid
from datetime import datetime from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from urllib.parse import quote from urllib.parse import quote, urlsplit
APP_DIR = Path(__file__).resolve().parent.parent APP_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = Path(os.environ.get("GHS_DATA_DIR", "/vol1/@appdata/GiteaHubSync")) DATA_DIR = Path(os.environ.get("GHS_DATA_DIR", "/vol1/@appdata/GiteaHubSync"))
@@ -41,8 +41,8 @@ SSH_KEY = APP_DIR / "ssh" / "id_ed25519"
if (DATA_DIR / "id_ed25519").exists(): if (DATA_DIR / "id_ed25519").exists():
SSH_KEY = DATA_DIR / "id_ed25519" SSH_KEY = DATA_DIR / "id_ed25519"
KNOWN_HOSTS = DATA_DIR / "known_hosts" KNOWN_HOSTS = DATA_DIR / "known_hosts"
GITEA_API = "http://127.0.0.1:3000/api/v1" GITEA_URL_ENV = os.environ.get("GITEA_URL") # “打开 Gitea”按钮地址覆盖,一般不设
GITEA_UI = os.environ.get("GITEA_URL", "http://192.168.31.10:3000") DEFAULT_GITEA_HOST = "http://127.0.0.1:3000" # 同步引擎访问的 Gitea 地址(设置页可改)
DEFAULT_REPOS = ["SMB_Android", "DCU_APP", "GD32E50X_BOOT", "DCU_DesktopController", DEFAULT_REPOS = ["SMB_Android", "DCU_APP", "GD32E50X_BOOT", "DCU_DesktopController",
"Motor_v2", "DCU_ECAT_Master", "GD32_BOOT"] "Motor_v2", "DCU_ECAT_Master", "GD32_BOOT"]
LEGACY_GH_USER = "cbb2625274797" # 旧版硬编码账号,仅用于首次运行的迁移 LEGACY_GH_USER = "cbb2625274797" # 旧版硬编码账号,仅用于首次运行的迁移
@@ -62,7 +62,46 @@ ENV["GIT_TERMINAL_PROMPT"] = "0"
# ---------------- 应用设置(settings.json) ---------------- # ---------------- 应用设置(settings.json) ----------------
cfg = {"gitea_user": LEGACY_GITEA_USER, "gitea_pass": "", "auto_create": True, cfg = {"gitea_user": LEGACY_GITEA_USER, "gitea_pass": "", "auto_create": True,
"interval": DEFAULT_INTERVAL} "interval": DEFAULT_INTERVAL, "gitea_host": DEFAULT_GITEA_HOST}
def norm_gitea_host(h):
"""规范化 Gitea 地址:补 http://、去尾部斜杠;不合法返回 None"""
h = str(h or "").strip().rstrip("/")
if not h:
return None
if "://" not in h:
h = "http://" + h
try:
p = urlsplit(h)
host = p.hostname or ""
if p.scheme in ("http", "https") and host \
and not p.path.strip("/") and not p.query and not p.fragment \
and re.fullmatch(r"[A-Za-z0-9.\-:\[\]]+", host):
return h
except Exception:
pass
return None
def gitea_host():
return cfg.get("gitea_host") or DEFAULT_GITEA_HOST
def gitea_ui(host_header=""):
"""「打开 Gitea」按钮地址:优先环境变量;否则用设置里的地址,
但把 127.0.0.1/localhost 替换成访问面板的主机名(浏览器打不开本机回环)"""
if GITEA_URL_ENV:
return GITEA_URL_ENV
base = gitea_host()
try:
p = urlsplit(base)
host = (host_header or "").split(":")[0].strip()
if host and p.hostname in ("127.0.0.1", "localhost", "0.0.0.0", "::1", ""):
return f"{p.scheme}://{host}" + (f":{p.port}" if p.port else "")
except Exception:
pass
return base
def sync_interval(): def sync_interval():
@@ -90,6 +129,9 @@ def load_settings():
cfg["auto_create"] = bool(d.get("auto_create", True)) cfg["auto_create"] = bool(d.get("auto_create", True))
if valid_interval(d.get("interval")): if valid_interval(d.get("interval")):
cfg["interval"] = int(d["interval"]) cfg["interval"] = int(d["interval"])
h = norm_gitea_host(d.get("gitea_host"))
if h:
cfg["gitea_host"] = h
except Exception: except Exception:
pass pass
write_gitcreds() write_gitcreds()
@@ -107,8 +149,8 @@ def save_settings():
def gitea_base(): def gitea_base():
"""Gitea 上当前用户的仓库前缀""" """Gitea 上当前用户的仓库前缀(地址可在设置页修改)"""
return f"http://127.0.0.1:3000/{cfg['gitea_user']}" return f"{gitea_host()}/{cfg['gitea_user']}"
# ---------------- GitHub 多账号(accounts.json) ---------------- # ---------------- GitHub 多账号(accounts.json) ----------------
@@ -217,9 +259,11 @@ def write_gitcreds():
这样镜像仓库的 remote 地址里不保存密码。清除密码时删除该文件。""" 这样镜像仓库的 remote 地址里不保存密码。清除密码时删除该文件。"""
try: try:
if cfg.get("gitea_pass"): if cfg.get("gitea_pass"):
p = urlsplit(gitea_host())
netloc = p.hostname + (f":{p.port}" if p.port else "")
user = quote(str(cfg["gitea_user"]), safe="") user = quote(str(cfg["gitea_user"]), safe="")
pw = quote(str(cfg["gitea_pass"]), safe="") pw = quote(str(cfg["gitea_pass"]), safe="")
GITCREDS_FILE.write_text(f"http://{user}:{pw}@127.0.0.1:3000\n", GITCREDS_FILE.write_text(f"{p.scheme}://{user}:{pw}@{netloc}\n",
encoding="utf-8") encoding="utf-8")
os.chmod(GITCREDS_FILE, 0o600) os.chmod(GITCREDS_FILE, 0o600)
elif GITCREDS_FILE.exists(): elif GITCREDS_FILE.exists():
@@ -228,6 +272,20 @@ def write_gitcreds():
pass pass
def repoint_mirror_remotes():
"""Gitea 地址变更后,把已有镜像仓库的 origin 指向新地址"""
n = 0
try:
for repo in REPO_DIR.glob("*.git"):
r = git(["-C", str(repo), "remote", "set-url", "origin",
f"{gitea_base()}/{repo.name[:-4]}.git"], timeout=20)
if r.returncode == 0:
n += 1
except Exception as e:
log(f"更新镜像 remote 失败: {e}")
return n
def git_creds_args(): def git_creds_args():
if cfg.get("gitea_pass") and GITCREDS_FILE.exists(): if cfg.get("gitea_pass") and GITCREDS_FILE.exists():
return ["-c", f"credential.helper=store --file={GITCREDS_FILE}"] return ["-c", f"credential.helper=store --file={GITCREDS_FILE}"]
@@ -286,13 +344,33 @@ def gitea_api(path):
cmd = ["curl", "-sS", "-m", "10"] cmd = ["curl", "-sS", "-m", "10"]
if cfg.get("gitea_pass"): if cfg.get("gitea_pass"):
cmd += ["-u", f"{cfg['gitea_user']}:{cfg['gitea_pass']}"] cmd += ["-u", f"{cfg['gitea_user']}:{cfg['gitea_pass']}"]
cmd.append(f"{GITEA_API}{path}") cmd.append(f"{gitea_host()}/api/v1{path}")
r = subprocess.run(cmd, capture_output=True, text=True, timeout=15) r = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
return r.returncode == 0, json.loads(r.stdout or "{}") return r.returncode == 0, json.loads(r.stdout or "{}")
except Exception: except Exception:
return False, {} return False, {}
def gitea_probe(host=None):
"""探测 Gitea 地址连通性,返回 (ok, msg)"""
host = host or gitea_host()
try:
r = subprocess.run(["curl", "-sS", "-m", "8", "-w", "\n%{http_code}",
f"{host}/api/v1/version"],
capture_output=True, text=True, timeout=12)
out, _, code = r.stdout.rpartition("\n")
st = int(code) if code.strip().isdigit() else 0
if 200 <= st < 300:
try:
ver = json.loads(out or "{}").get("version", "?")
except Exception:
ver = "?"
return True, f"连接成功,Gitea 版本 {ver}"
return False, f"地址连不上(HTTP {st or '超时'}),请检查后重试"
except Exception as e:
return False, f"地址连不上:{e}"
def gitea_repo_exists(name): def gitea_repo_exists(name):
ok, d = gitea_api(f"/repos/{cfg['gitea_user']}/{name}") ok, d = gitea_api(f"/repos/{cfg['gitea_user']}/{name}")
return ok and isinstance(d, dict) and d.get("full_name") == f"{cfg['gitea_user']}/{name}" return ok and isinstance(d, dict) and d.get("full_name") == f"{cfg['gitea_user']}/{name}"
@@ -559,7 +637,7 @@ def account_public(acc):
"method": "Token(HTTPS)" if acc.get("token") else "SSH 私钥"} "method": "Token(HTTPS)" if acc.get("token") else "SSH 私钥"}
def global_status(): def global_status(ui_url=None):
repos = load_repos() repos = load_repos()
details = [repo_detail(r) for r in repos] details = [repo_detail(r) for r in repos]
now = time.time() now = time.time()
@@ -582,7 +660,7 @@ def global_status():
"synced_count": sum(1 for x in details if x["status"] == "已同步"), "synced_count": sum(1 for x in details if x["status"] == "已同步"),
"total_size_mb": round(sum(x["size_mb"] for x in details), 1), "total_size_mb": round(sum(x["size_mb"] for x in details), 1),
"repos": details, "repos": details,
"gitea_url": GITEA_UI, "gitea_url": ui_url or gitea_ui(),
"gitea_user": cfg["gitea_user"], "gitea_user": cfg["gitea_user"],
"gitea_online": bool(gitea_all), "gitea_online": bool(gitea_all),
"available": [r for r in gitea_all if r not in repos], "available": [r for r in gitea_all if r not in repos],
@@ -759,7 +837,7 @@ PAGE_HOME = """
</div> </div>
<div class="btns"> <div class="btns">
<button class="b-sync" id="btnSync" onclick="syncNow()">⚡ 立即同步一次</button> <button class="b-sync" id="btnSync" onclick="syncNow()">⚡ 立即同步一次</button>
<a class="btn b-gitea" href="__GITEA__" target="_blank">🗂️ 打开 Gitea(3000端口)</a> <a class="btn b-gitea" href="__GITEA__" target="_blank">🗂️ 打开 Gitea</a>
<a class="btn b-gh" id="btnGh" href="__GH__" target="_blank">🐙 GitHub 仓库列表</a> <a class="btn b-gh" id="btnGh" href="__GH__" target="_blank">🐙 GitHub 仓库列表</a>
</div> </div>
</div> </div>
@@ -961,17 +1039,22 @@ PAGE_SETTINGS = """
</div> </div>
<div class="card"> <div class="card">
<h2>Gitea 与同步设置</h2> <h2>Gitea 与同步设置</h2>
<label class="flabel">Gitea 地址<span class="sub">同步引擎访问的地址,默认 http://127.0.0.1:3000</span></label>
<div class="addrow">
<input id="giteaHost" placeholder="http://127.0.0.1:3000">
<button class="b-mini" id="btnGitea" onclick="saveGitea(false)">保存</button>
</div>
<label class="flabel">Gitea 用户名<span class="sub">面板按此用户名列出你可同步的仓库</span></label> <label class="flabel">Gitea 用户名<span class="sub">面板按此用户名列出你可同步的仓库</span></label>
<div class="addrow"> <div class="addrow">
<input id="giteaUser" placeholder="cbb"> <input id="giteaUser" placeholder="cbb">
<button class="b-mini" id="btnGitea" onclick="saveGitea()">保存</button> <button class="b-mini" onclick="saveGitea(false)">保存</button>
</div> </div>
<label class="flabel">Gitea 密码<span class="sub">仅仓库为私有时需要;留空保存可清除</span></label> <label class="flabel">Gitea 密码<span class="sub">仅仓库为私有时需要;留空保存可清除</span></label>
<div class="addrow"> <div class="addrow">
<input id="giteaPass" type="password" placeholder="未设置"> <input id="giteaPass" type="password" placeholder="未设置">
<button class="b-mini" onclick="saveGitea()">保存</button> <button class="b-mini" onclick="saveGitea(true)">保存</button>
</div> </div>
<div class="tip">本地 Gitea 地址 __GITEAURL__;密码只保存在 NAS 本地,仅用于读取私有仓库。</div> <div class="tip" id="giteaUrlTip"></div>
<div class="optbox"> <div class="optbox">
<div class="optrow"> <div class="optrow">
<label class="switch"><input type="checkbox" id="swAuto" onchange="saveAuto()"><span class="slider"></span></label> <label class="switch"><input type="checkbox" id="swAuto" onchange="saveAuto()"><span class="slider"></span></label>
@@ -998,9 +1081,11 @@ function showMsg(id,t,ok){const m=document.getElementById(id);m.textContent=t;m.
async function refresh(){ async function refresh(){
try{ try{
const s=await jget('api/settings'); const s=await jget('api/settings');
document.getElementById('giteaHost').value=s.gitea_host||'http://127.0.0.1:3000';
document.getElementById('giteaUser').value=s.gitea_user||''; document.getElementById('giteaUser').value=s.gitea_user||'';
document.getElementById('giteaPass').placeholder=s.gitea_pass_set document.getElementById('giteaPass').placeholder=s.gitea_pass_set
?'Gitea 密码(已设置;留空保存可清除)':'Gitea 密码(私有仓库必填)'; ?'Gitea 密码(已设置;留空保存可清除)':'Gitea 密码(私有仓库必填)';
document.getElementById('giteaUrlTip').textContent='「打开 Gitea」按钮地址:'+(s.gitea_url||'—')+'(随访问地址自动适配);密码只保存在 NAS 本地,仅用于读取私有仓库。';
document.getElementById('swAuto').checked=!!s.auto_create; document.getElementById('swAuto').checked=!!s.auto_create;
const iv=document.getElementById('syncInterval'); const iv=document.getElementById('syncInterval');
iv.value=s.interval||10;iv.min=s.min_interval||5;iv.max=s.max_interval||86400; iv.value=s.interval||10;iv.min=s.min_interval||5;iv.max=s.max_interval||86400;
@@ -1073,12 +1158,14 @@ async function delAcc(id){
const r=await (await fetch('api/accounts/remove',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})})).json(); const r=await (await fetch('api/accounts/remove',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})})).json();
showMsg('msgAcc',r.msg||JSON.stringify(r),!!r.ok);refresh(); showMsg('msgAcc',r.msg||JSON.stringify(r),!!r.ok);refresh();
} }
async function saveGitea(){ async function saveGitea(withPass){
const gitea_user=document.getElementById('giteaUser').value.trim(); // withPass=false 时不发送密码字段,避免改地址/用户名时误清密码
const gitea_pass=document.getElementById('giteaPass').value; const body={gitea_host:document.getElementById('giteaHost').value.trim(),
const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({gitea_user,gitea_pass})})).json(); gitea_user:document.getElementById('giteaUser').value.trim()};
if(withPass)body.gitea_pass=document.getElementById('giteaPass').value;
const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok); showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok);
document.getElementById('giteaPass').value=''; if(withPass)document.getElementById('giteaPass').value='';
refresh(); refresh();
} }
async function saveAuto(){ async function saveAuto(){
@@ -1138,7 +1225,7 @@ class Handler(BaseHTTPRequestHandler):
path = self.path.split("?")[0].rstrip("/") or "/" path = self.path.split("?")[0].rstrip("/") or "/"
if path == "/": if path == "/":
acc = active_account() acc = active_account()
body = (PAGE_HOME.replace("__GITEA__", GITEA_UI) body = (PAGE_HOME.replace("__GITEA__", gitea_ui(self.headers.get("Host")))
.replace("__GH__", f"https://github.com/{acc['gh_user']}?tab=repositories" if acc else "") .replace("__GH__", f"https://github.com/{acc['gh_user']}?tab=repositories" if acc else "")
.replace("__LOGPATH__", str(LOG_FILE))) .replace("__LOGPATH__", str(LOG_FILE)))
self._html(page("控制面板", body, "home")) self._html(page("控制面板", body, "home"))
@@ -1147,8 +1234,7 @@ class Handler(BaseHTTPRequestHandler):
elif path == "/manage": elif path == "/manage":
self._html(page("项目管理", PAGE_MANAGE, "manage")) self._html(page("项目管理", PAGE_MANAGE, "manage"))
elif path == "/settings": elif path == "/settings":
body = PAGE_SETTINGS.replace("__GITEAURL__", GITEA_UI) self._html(page("设置", PAGE_SETTINGS, "settings"))
self._html(page("设置", body, "settings"))
elif path == "/api/state": elif path == "/api/state":
repos = load_repos() repos = load_repos()
acc = active_account() acc = active_account()
@@ -1171,7 +1257,8 @@ class Handler(BaseHTTPRequestHandler):
"interval": sync_interval(), "interval": sync_interval(),
"min_interval": MIN_INTERVAL, "min_interval": MIN_INTERVAL,
"max_interval": MAX_INTERVAL, "max_interval": MAX_INTERVAL,
"gitea_url": GITEA_UI, "gitea_host": gitea_host(),
"gitea_url": gitea_ui(self.headers.get("Host")),
"active": active_id, "active": active_id,
"accounts": [account_public(a) for a in accounts]}) "accounts": [account_public(a) for a in accounts]})
elif path == "/api/log": elif path == "/api/log":
@@ -1191,7 +1278,7 @@ class Handler(BaseHTTPRequestHandler):
ok, msg = run_sync() ok, msg = run_sync()
self._json({"ok": ok, "msg": msg}) self._json({"ok": ok, "msg": msg})
elif path == "/api/settings/save": elif path == "/api/settings/save":
new_user = new_pass = None new_user = new_pass = new_host = None
if "gitea_user" in data: if "gitea_user" in data:
u = (data.get("gitea_user") or "").strip() u = (data.get("gitea_user") or "").strip()
if not valid_name(u): if not valid_name(u):
@@ -1200,22 +1287,35 @@ class Handler(BaseHTTPRequestHandler):
new_user = u new_user = u
if "gitea_pass" in data: if "gitea_pass" in data:
new_pass = str(data.get("gitea_pass") or "") # 空串 = 清除密码 new_pass = str(data.get("gitea_pass") or "") # 空串 = 清除密码
if "gitea_host" in data:
h = norm_gitea_host(data.get("gitea_host"))
if not h:
self._json({"ok": False, "msg": "Gitea 地址不合法,应类似 http://127.0.0.1:3000"})
return
new_host = h
if "interval" in data: if "interval" in data:
if not valid_interval(data.get("interval")): if not valid_interval(data.get("interval")):
self._json({"ok": False, "msg": f"同步间隔须为 {MIN_INTERVAL} ~ {MAX_INTERVAL} 之间的整数(秒)"}) self._json({"ok": False, "msg": f"同步间隔须为 {MIN_INTERVAL} ~ {MAX_INTERVAL} 之间的整数(秒)"})
return return
# 预校验:涉及 Gitea 账号的改动,先用新凭据调一次 /user 验证 # 预校验:新地址先探连通性;涉及账号的改动再用新凭据调一次 /user 验证
trial = dict(cfg) trial = dict(cfg)
if new_user is not None: if new_user is not None:
trial["gitea_user"] = new_user trial["gitea_user"] = new_user
if new_pass is not None: if new_pass is not None:
trial["gitea_pass"] = new_pass trial["gitea_pass"] = new_pass
if new_host is not None:
trial["gitea_host"] = new_host
ok, msg = gitea_probe(new_host)
if not ok:
self._json({"ok": False, "msg": msg})
return
if (new_user is not None or new_pass is not None) and trial["gitea_pass"]: if (new_user is not None or new_pass is not None) and trial["gitea_pass"]:
try: try:
r = subprocess.run( r = subprocess.run(
["curl", "-sS", "-m", "10", "-w", "\n%{http_code}", ["curl", "-sS", "-m", "10", "-w", "\n%{http_code}",
"-u", f"{trial['gitea_user']}:{trial['gitea_pass']}", "-u", f"{trial['gitea_user']}:{trial['gitea_pass']}",
f"{GITEA_API}/user"], capture_output=True, text=True, timeout=15) f"{trial['gitea_host']}/api/v1/user"],
capture_output=True, text=True, timeout=15)
out, _, code = r.stdout.rpartition("\n") out, _, code = r.stdout.rpartition("\n")
st = int(code) if code.strip().isdigit() else 0 st = int(code) if code.strip().isdigit() else 0
if not (200 <= st < 300): if not (200 <= st < 300):
@@ -1228,11 +1328,14 @@ class Handler(BaseHTTPRequestHandler):
except Exception as e: except Exception as e:
self._json({"ok": False, "msg": f"Gitea 账号验证出错: {e}"}) self._json({"ok": False, "msg": f"Gitea 账号验证出错: {e}"})
return return
host_changed = new_host is not None and new_host != gitea_host()
if new_user is not None: if new_user is not None:
cfg["gitea_user"] = new_user cfg["gitea_user"] = new_user
if new_pass is not None: if new_pass is not None:
cfg["gitea_pass"] = new_pass cfg["gitea_pass"] = new_pass
log("Gitea 密码已" + ("更新" if new_pass else "清除")) log("Gitea 密码已" + ("更新" if new_pass else "清除"))
if new_host is not None:
cfg["gitea_host"] = new_host
if "auto_create" in data: if "auto_create" in data:
cfg["auto_create"] = bool(data.get("auto_create")) cfg["auto_create"] = bool(data.get("auto_create"))
if "interval" in data: if "interval" in data:
@@ -1241,15 +1344,23 @@ class Handler(BaseHTTPRequestHandler):
if cfg["interval"] != old_iv: if cfg["interval"] != old_iv:
log(f"同步间隔已调整: {old_iv}s -> {cfg['interval']}s") log(f"同步间隔已调整: {old_iv}s -> {cfg['interval']}s")
save_settings() save_settings()
notes = []
if new_host is not None:
ok, msg = gitea_probe()
notes.append(f"Gitea 地址已设为 {new_host}({msg})")
if host_changed:
n = repoint_mirror_remotes()
log(f"Gitea 地址变更: {n} 个镜像仓库的 origin 已更新")
notes.append(f"{n} 个已有镜像的拉取地址已更新")
if cfg.get("gitea_pass"): if cfg.get("gitea_pass"):
msg = "已保存(Gitea 密码验证通过 ✓,可同步私有仓库)" notes.append("Gitea 密码验证通过 ✓,可同步私有仓库")
else: else:
ok, d = gitea_api(f"/users/{cfg['gitea_user']}") ok, d = gitea_api(f"/users/{cfg['gitea_user']}")
if ok and isinstance(d, dict) and d.get("login"): if ok and isinstance(d, dict) and d.get("login"):
msg = "已保存(未配置 Gitea 密码,只能同步公开仓库;仓库为私有时请填写密码)" notes.append("未配置 Gitea 密码,只能同步公开仓库;仓库为私有时请填写密码")
else: else:
msg = f"已保存(Gitea 上找不到用户 {cfg['gitea_user']},请检查)" notes.append(f"注意:在新地址上找不到用户 {cfg['gitea_user']},请检查")
self._json({"ok": True, "msg": msg}) self._json({"ok": True, "msg": "已保存(" + ";".join(notes) + ")"})
elif path == "/api/accounts/add": elif path == "/api/accounts/add":
token = (data.get("token") or "").strip() token = (data.get("token") or "").strip()
label = (data.get("label") or "").strip() label = (data.get("label") or "").strip()
@@ -1440,7 +1551,7 @@ def main():
log(f"GiteaHubSync 守护进程启动 (pid {os.getpid()}, 自动同步={'开' if state['enabled'] else '关'}, " log(f"GiteaHubSync 守护进程启动 (pid {os.getpid()}, 自动同步={'开' if state['enabled'] else '关'}, "
f"项目 {len(load_repos())} 个, GitHub 账号={acc['label'] if acc else '无'}" f"项目 {len(load_repos())} 个, GitHub 账号={acc['label'] if acc else '无'}"
f"({acc['gh_user'] if acc else '-'}/{'Token' if acc and acc.get('token') else 'SSH'}), " f"({acc['gh_user'] if acc else '-'}/{'Token' if acc and acc.get('token') else 'SSH'}), "
f"Gitea 用户={cfg['gitea_user']}, 自动创建={'开' if cfg.get('auto_create') else '关'})") f"Gitea 用户={cfg['gitea_user']}, 地址={gitea_host()}, 自动创建={'开' if cfg.get('auto_create') else '关'})")
threading.Thread(target=loop, daemon=True).start() threading.Thread(target=loop, daemon=True).start()
server = ThreadingHTTPServer(("0.0.0.0", PANEL_PORT), Handler) server = ThreadingHTTPServer(("0.0.0.0", PANEL_PORT), Handler)
log(f"控制面板监听 :{PANEL_PORT}") log(f"控制面板监听 :{PANEL_PORT}")
+1 -1
View File
@@ -1,5 +1,5 @@
appname = GiteaHubSync appname = GiteaHubSync
version = 1.1.2 version = 1.2.0
display_name = Gitea→GitHub 同步助手 display_name = Gitea→GitHub 同步助手
desc = 每次推送到飞牛 Gitea 的代码,10 秒内自动镜像到 GitHub 的 7 个私有仓库。内置控制面板:自动同步开关、立即同步、仓库状态、同步日志,一键打开 Gitea。数据全部保存在 NAS 本地。 desc = 每次推送到飞牛 Gitea 的代码,10 秒内自动镜像到 GitHub 的 7 个私有仓库。内置控制面板:自动同步开关、立即同步、仓库状态、同步日志,一键打开 Gitea。数据全部保存在 NAS 本地。
platform = all platform = all
+7 -2
View File
@@ -14,6 +14,8 @@ NAS 重启自动恢复。控制面板带自动同步开关、立即同步、仓
- **多 GitHub 账号**:设置页可添加多个 GitHub 账号(用户名 + Personal Access Token), - **多 GitHub 账号**:设置页可添加多个 GitHub 账号(用户名 + Personal Access Token),
一键切换推送目标;每个账号独立记录同步进度,切回原账号不会重复推送 一键切换推送目标;每个账号独立记录同步进度,切回原账号不会重复推送
- **同步间隔可调**:设置页可改检查间隔(5 秒 ~ 1 天,默认 10 秒),改完立即生效 - **同步间隔可调**:设置页可改检查间隔(5 秒 ~ 1 天,默认 10 秒),改完立即生效
- **Gitea 地址可改**:设置页可改同步引擎访问的 Gitea 地址(默认 `http://127.0.0.1:3000`),
保存前自动探测连通性;改地址后已有镜像的拉取地址自动更新,「打开 Gitea」按钮随访问地址自适应
- **自动创建仓库**:Gitea 上的新仓库加入同步时,若 GitHub 上不存在同名仓库, - **自动创建仓库**:Gitea 上的新仓库加入同步时,若 GitHub 上不存在同名仓库,
自动按账号设置为私有/公开创建(可开关) 自动按账号设置为私有/公开创建(可开关)
- **两种推送方式**:配置了 Token 的账号走 HTTPS 推送(通用,任何人都能用自己的 - **两种推送方式**:配置了 Token 的账号走 HTTPS 推送(通用,任何人都能用自己的
@@ -63,6 +65,9 @@ FNOS_HOST=192.168.31.10 FNOS_USER=<管理员账号> FNOS_PASS=<密码> \
> Token 只保存在 NAS 本地 `accounts.json`(权限 600),不上传任何地方; > Token 只保存在 NAS 本地 `accounts.json`(权限 600),不上传任何地方;
> 日志中的 Token 会自动打码。卸载应用不会动 GitHub 上的任何仓库。 > 日志中的 Token 会自动打码。卸载应用不会动 GitHub 上的任何仓库。
如果你的 Gitea 不在默认的 `127.0.0.1:3000`(比如换了端口),到设置页「Gitea 地址」一栏改掉即可,
保存时会自动探测连通性,并同步更新所有已有镜像的拉取地址。
## 重新构建 ## 重新构建
```bash ```bash
@@ -76,5 +81,5 @@ cd /vol1/@appshare/fn-deepseek-harness/fnos-gitea-sync
端口直连,私钥随应用打包安装在 NAS 本地 端口直连,私钥随应用打包安装在 NAS 本地
- 从 v1.0.0 升级会自动迁移:旧的同步进度归入「默认(SSH 私钥)」账号,行为不变 - 从 v1.0.0 升级会自动迁移:旧的同步进度归入「默认(SSH 私钥)」账号,行为不变
- 数据文件:`accounts.json`(GitHub 账号,含 Token)、`settings.json` - 数据文件:`accounts.json`(GitHub 账号,含 Token)、`settings.json`
(Gitea 用户名 / 自动创建开关)、`repos.json`(同步列表)、`.last-pushed/<账号>/<仓库>` (Gitea 地址/用户名/密码、自动创建开关、同步间隔)、`repos.json`(同步列表)、
(每个账号的同步进度) `.last-pushed/<账号>/<仓库>`(每个账号的同步进度)