diff --git a/GiteaHubSync/app/backend/panel.py b/GiteaHubSync/app/backend/panel.py
index b4502ea..5d50410 100644
--- a/GiteaHubSync/app/backend/panel.py
+++ b/GiteaHubSync/app/backend/panel.py
@@ -71,7 +71,8 @@ ENV["GIT_TERMINAL_PROMPT"] = "0"
# ---------------- 应用设置(settings.json) ----------------
cfg = {"gitea_user": LEGACY_GITEA_USER, "gitea_pass": "", "auto_create": True,
"interval": DEFAULT_INTERVAL, "gitea_host": DEFAULT_GITEA_HOST,
- "max_workers": DEFAULT_WORKERS, "max_retries": DEFAULT_MAX_RETRIES}
+ "max_workers": DEFAULT_WORKERS, "max_retries": DEFAULT_MAX_RETRIES,
+ "open_url": ""} # 「打开 Gitea」按钮专用链接(留空则用 gitea_host + 自动推断)
def norm_gitea_host(h):
@@ -98,11 +99,15 @@ def gitea_host():
def gitea_ui(host_header="", referer="", xfwd_host=""):
- """「打开 Gitea」按钮地址:把 127.0.0.1/localhost 替换成真实远程主机名。
- 推断顺序:X-Forwarded-Host(CGI 转发) > Host > Referer(浏览器自带)。
- 注意:仅当推断出的真实主机名不是 loopback 时才替换(本地直连保持原样)。"""
- if GITEA_URL_ENV:
- return GITEA_URL_ENV
+ """「打开 Gitea」按钮地址。
+ 优先级:环境变量 GITEA_URL > 配置 open_url > gitea_host(自动推断替换)。
+ 自动推断顺序:X-Forwarded-Host > Host > Referer,仅当真实主机非 loopback 时替换 gitea_host。"""
+ env_url = GITEA_URL_ENV
+ if env_url:
+ return env_url
+ ou = (cfg.get("open_url") or "").strip()
+ if ou:
+ return ou
base = gitea_host()
real = (xfwd_host or host_header or "").split(":")[0].strip()
if not real and referer:
@@ -110,7 +115,7 @@ def gitea_ui(host_header="", referer="", xfwd_host=""):
if m:
real = m.group(1)
if not real or real in ("127.0.0.1", "localhost", "0.0.0.0", "::1"):
- return base # 本地/无法推断,保留 gitea_host 原值
+ return base
try:
p = urlsplit(base)
if p.hostname in ("127.0.0.1", "localhost", "0.0.0.0", "::1", ""):
@@ -184,6 +189,12 @@ def load_settings():
h = norm_gitea_host(d.get("gitea_host"))
if h:
cfg["gitea_host"] = h
+ # open_url 校验:必须是 http(s) URL 且无路径
+ ou = d.get("open_url")
+ if isinstance(ou, str):
+ cfg["open_url"] = norm_gitea_host(ou.strip()) or ""
+ else:
+ cfg["open_url"] = str(ou or "").strip() or cfg.get("open_url", "")
except Exception:
pass
write_gitcreds()
@@ -1309,6 +1320,11 @@ PAGE_SETTINGS = """
+
+
+
+
+
@@ -1386,6 +1402,7 @@ async function refresh(){
document.getElementById('giteaPass').placeholder=s.gitea_pass_set
?'Gitea 密码(已设置;留空保存可清除)':'Gitea 密码(私有仓库必填)';
document.getElementById('giteaUrlTip').textContent='「打开 Gitea」按钮地址:'+(s.gitea_url||'—')+'(随访问地址自动适配);密码只保存在 NAS 本地,仅用于读取私有仓库。';
+ document.getElementById('openUrl').value=s.open_url||'';
document.getElementById('swAuto').checked=!!s.auto_create;
const iv=document.getElementById('syncInterval');
iv.value=s.interval||10;iv.min=s.min_interval||5;iv.max=s.max_interval||86400;
@@ -1526,6 +1543,12 @@ async function saveGitea(withPass){
if(withPass)document.getElementById('giteaPass').value='';
refresh();
}
+async function saveOpenUrl(){
+ const v=document.getElementById('openUrl').value.trim();
+ const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({open_url:v})})).json();
+ showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok);
+ refresh();
+}
async function saveAuto(){
const auto_create=document.getElementById('swAuto').checked;
const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({auto_create})})).json();
@@ -1713,6 +1736,7 @@ class Handler(BaseHTTPRequestHandler):
"max_retries": max_retries(),
"retry_interval": RETRY_INTERVAL,
"gitea_host": gitea_host(),
+ "open_url": cfg.get("open_url", ""),
"gitea_url": gitea_ui(
self.headers.get("Host"),
self.headers.get("Referer"),
@@ -1751,6 +1775,15 @@ class Handler(BaseHTTPRequestHandler):
self._json({"ok": False, "msg": "Gitea 地址不合法,应类似 http://127.0.0.1:3000"})
return
new_host = h
+ if "open_url" in data:
+ v = (data.get("open_url") or "").strip()
+ if v:
+ if "://" not in v:
+ self._json({"ok": False, "msg": "「打开 Gitea 链接」必须带协议头,如 http://nas.local:3000 或 https://..."})
+ return
+ if not norm_gitea_host(v):
+ self._json({"ok": False, "msg": "「打开 Gitea 链接」格式不合法,应类似 http://nas.local:3000(留空则用默认推断)"})
+ return
if "interval" in data:
if not valid_interval(data.get("interval")):
self._json({"ok": False, "msg": f"同步间隔须为 {MIN_INTERVAL} ~ {MAX_INTERVAL} 之间的整数(秒)"})
@@ -1802,6 +1835,8 @@ class Handler(BaseHTTPRequestHandler):
log("Gitea 密码已" + ("更新" if new_pass else "清除"))
if new_host is not None:
cfg["gitea_host"] = new_host
+ if "open_url" in data:
+ cfg["open_url"] = (data.get("open_url") or "").strip()
if "auto_create" in data:
cfg["auto_create"] = bool(data.get("auto_create"))
if "interval" in data:
diff --git a/GiteaHubSync/manifest b/GiteaHubSync/manifest
index 6d5d2f9..2a2775b 100644
--- a/GiteaHubSync/manifest
+++ b/GiteaHubSync/manifest
@@ -1,5 +1,5 @@
appname = GiteaHubSync
-version = 1.5.5
+version = 1.5.6
display_name = Gitea→GitHub 同步助手
desc = 每次推送到飞牛 Gitea 的代码,10 秒内自动镜像到 GitHub 的 7 个私有仓库。内置控制面板:自动同步开关、立即同步、仓库状态、同步日志,一键打开 Gitea。数据全部保存在 NAS 本地。
platform = all