diff --git a/GiteaHubSync/app/backend/panel.py b/GiteaHubSync/app/backend/panel.py
index 64527fc..3097665 100644
--- a/GiteaHubSync/app/backend/panel.py
+++ b/GiteaHubSync/app/backend/panel.py
@@ -956,6 +956,29 @@ h2 .h2sub{font-size:12px;font-weight:400;color:#8a919e;margin-left:10px}
.saved-tip{font-size:12px;font-weight:600;white-space:nowrap}
.saved-tip.ok{color:#0a7d34}
.saved-tip.err{color:#b91c1c}
+input[readonly]{background:#f6f8fa;color:#646a73;cursor:not-allowed}
+.b-icon{padding:5px 9px;min-width:auto;font-size:13px}
+/* ---------- 模态弹窗 ---------- */
+.modal{display:none;position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000;align-items:center;justify-content:center;padding:20px}
+.modal.show{display:flex}
+.modal-box{background:#fff;border-radius:14px;width:100%;max-width:480px;box-shadow:0 20px 60px rgba(0,0,0,.25);max-height:90vh;overflow:auto;animation:modalIn .15s ease-out}
+@keyframes modalIn{from{transform:translateY(-8px);opacity:.6}to{transform:translateY(0);opacity:1}}
+.modal-head{display:flex;align-items:center;justify-content:space-between;padding:18px 22px;border-bottom:1px solid #eef0f3}
+.modal-head h3{font-size:17px;margin:0}
+.modal-close{background:none;border:none;font-size:24px;color:#646a73;cursor:pointer;padding:2px 10px;border-radius:6px;line-height:1}
+.modal-close:hover{background:#f0f2f5;color:#1f2329}
+.modal-body{padding:22px;display:flex;flex-direction:column;gap:8px}
+.modal-body input[type=text],.modal-body input[type=password]{
+ border:1px solid #d0d5dd;border-radius:10px;padding:11px 14px;font-size:14px;outline:none;width:100%;box-sizing:border-box;font-family:inherit}
+.modal-body input:focus{border-color:#7c3aed}
+.modal-foot{padding:14px 22px;border-top:1px solid #eef0f3;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
+.modal-msg{font-size:13px;flex:1;min-width:0;word-break:break-word}
+.modal-msg.ok{color:#0a7d34}
+.modal-msg.err{color:#b91c1c}
+.modal-msg:empty{display:none}
+@media (max-width:760px){
+ .modal-box{max-width:none}
+}
table.hover tbody tr:hover{background:#f8fafc}
td.ops{white-space:nowrap}
td.ops button,td.ops .pill{margin:2px 4px 2px 0}
@@ -1219,21 +1242,9 @@ PAGE_SETTINGS = """
添加 / 更新 GitHub 账号Token 验证通过后自动识别用户名
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
添加多个账号后可随时切换推送目标,每个账号独立记录同步进度。已存在的账号可点表格行右边的 ✏️ 编辑。
+
+
Token 获取:GitHub → Settings → Developer settings → Personal access tokens →
Tokens (classic) → Generate new token,勾选
repo 权限(即可自动创建仓库并推送)。
链接:
github.com/settings/tokens ·
@@ -1352,15 +1363,16 @@ async function refresh(){
?`
⚠️当前账号「${esc(cur.label)}」通过 SSH 私钥推送,暂时无法自动创建仓库。
GitHub 的创建仓库接口只认 Token 认证,SSH 私钥只能推送已存在的仓库——这是 GitHub 的限制,不是软件没做。
给这个账号配上 Token 即可解锁(推送也会改走 HTTPS,更稳更快):
-
`
+
`
:'';
+ allAccounts=s.accounts||[];
document.getElementById('tbAcc').innerHTML=(s.accounts||[]).map(a=>{
const isCur=a.id===s.active;
const ops=isCur
? '
当前使用'
: `
`;
const verify=a.has_token?`
`:'';
- const upgrade=a.has_token?'':`
`;
+ const edit=`
`;
const del=(a.has_token||(s.accounts||[]).length>1)
? `
`:'';
const method=a.has_token
@@ -1373,33 +1385,78 @@ async function refresh(){
+`
${esc(a.token||'(未配置)')} | `
+`
${method} | `
+`
${create} | `
- +`
${ops} ${verify} ${upgrade} ${del} | `;
+ +`
${ops} ${edit} ${verify} ${del} | `;
}).join('')||'
| 暂无账号 |
';
}catch(e){showMsg('msgAcc','刷新失败: '+e,false)}
}
-function upgradeAcc(id,ghUser){
- // 预填「添加/更新账号」表单并滚动过去,引导给旧账号补 Token
- document.getElementById('accLabel').value='';
- document.getElementById('accUser').value=ghUser||'';
- document.getElementById('accToken').value='';
- document.getElementById('accToken').focus();
- document.getElementById('accToken').scrollIntoView({behavior:'smooth',block:'center'});
- showMsg('msgAcc','已为你填好用户名 '+ghUser+',把 Token 粘贴进去点「验证并添加」即可(会更新原账号)',true);
+// ---- GitHub 账号:弹窗式添加/编辑 ----
+let allAccounts=[];
+function showModalMsg(id,t,ok){
+ const m=document.getElementById(id);
+ m.textContent=t||''; m.className='modal-msg'+(ok===true?' ok':(ok===false?' err':''));
}
-async function addAccount(){
- const label=document.getElementById('accLabel').value.trim();
- const gh_user=document.getElementById('accUser').value.trim();
- const token=document.getElementById('accToken').value.trim();
- const priv=document.getElementById('accPrivate').checked;
- if(!token){showMsg('msgAcc','请填写 Personal Access Token',false);return;}
- const b=document.getElementById('btnAccAdd');b.disabled=true;b.textContent='⏳ 验证中…';
+function closeModal(id){ document.getElementById(id).classList.remove('show'); }
+function openAddModal(){
+ document.getElementById('addLabel').value='';
+ document.getElementById('addUser').value='';
+ document.getElementById('addToken').value='';
+ document.getElementById('addPrivate').checked=true;
+ showModalMsg('addMsg','',null);
+ document.getElementById('modalAdd').classList.add('show');
+ setTimeout(()=>document.getElementById('addLabel').focus(),50);
+}
+function openEditModal(id, focusToken){
+ const acc=allAccounts.find(a=>a.id===id);
+ if(!acc)return;
+ document.getElementById('modalEdit').dataset.id=id;
+ document.getElementById('editLabel').value=acc.label||'';
+ document.getElementById('editUser').value=acc.gh_user||'';
+ document.getElementById('editToken').value='';
+ document.getElementById('editToken').placeholder=acc.token?'当前:'+acc.token:'(未配置)';
+ document.getElementById('editPrivate').checked=!!acc.private;
+ showModalMsg('editMsg','',null);
+ document.getElementById('modalEdit').classList.add('show');
+ setTimeout(()=>{
+ const el=focusToken?document.getElementById('editToken'):document.getElementById('editLabel');
+ el.focus();
+ },50);
+}
+function upgradeAcc(id){
+ // 顶部横幅「去添加 Token」:直接打开当前账号的编辑弹窗,聚焦 Token
+ openEditModal(id, true);
+}
+async function saveAdd(){
+ const body={
+ label: document.getElementById('addLabel').value.trim(),
+ gh_user: document.getElementById('addUser').value.trim(),
+ token: document.getElementById('addToken').value.trim(),
+ private: document.getElementById('addPrivate').checked,
+ };
+ if(!body.token){ showModalMsg('addMsg','请填写 Personal Access Token',false); return; }
+ const b=document.getElementById('btnAddSave'); b.disabled=true; b.textContent='⏳ 验证中…';
try{
- const r=await (await fetch('api/accounts/add',{method:'POST',headers:{'Content-Type':'application/json'},
- body:JSON.stringify({label,gh_user,token,private:priv})})).json();
- showMsg('msgAcc',r.msg||JSON.stringify(r),!!r.ok);
- if(r.ok){document.getElementById('accLabel').value='';document.getElementById('accUser').value='';document.getElementById('accToken').value='';}
- }catch(e){showMsg('msgAcc','请求失败: '+e,false)}
- finally{b.disabled=false;b.textContent='✓ 验证并添加';refresh();}
+ const r=await (await fetch('api/accounts/add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
+ showModalMsg('addMsg', r.msg||'', !!r.ok);
+ if(r.ok){ setTimeout(()=>{ closeModal('modalAdd'); refresh(); }, 700); }
+ }catch(e){ showModalMsg('addMsg','请求失败: '+e, false); }
+ finally{ b.disabled=false; b.textContent='✓ 验证并添加'; }
+}
+async function saveEdit(){
+ const id=document.getElementById('modalEdit').dataset.id;
+ if(!id){ showModalMsg('editMsg','内部错误:未选中账号',false); return; }
+ const body={
+ id,
+ label: document.getElementById('editLabel').value.trim(),
+ token: document.getElementById('editToken').value.trim(), // 空 = 不改
+ private: document.getElementById('editPrivate').checked,
+ };
+ const b=document.getElementById('btnEditSave'); b.disabled=true; b.textContent='⏳ 保存中…';
+ try{
+ const r=await (await fetch('api/accounts/edit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
+ showModalMsg('editMsg', r.msg||'', !!r.ok);
+ if(r.ok){ setTimeout(()=>{ closeModal('modalEdit'); refresh(); }, 700); }
+ }catch(e){ showModalMsg('editMsg','请求失败: '+e, false); }
+ finally{ b.disabled=false; b.textContent='保存'; }
}
async function switchAcc(id){
const r=await (await fetch('api/accounts/switch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})})).json();
@@ -1437,6 +1494,64 @@ function fmtInterval(s){
}
refresh();
+
+// ---- 模态弹窗:添加 ----
+
+
+// ---- 模态弹窗:编辑 ----
+
+
+
"""
@@ -1633,6 +1748,7 @@ class Handler(BaseHTTPRequestHandler):
notes.append(f"注意:在新地址上找不到用户 {cfg['gitea_user']},请检查")
self._json({"ok": True, "msg": "已保存(" + ";".join(notes) + ")"})
elif path == "/api/accounts/add":
+ # 严格新增:Token 对应的 gh_user 若已存在则拒绝,引导用户用编辑功能
token = (data.get("token") or "").strip()
label = (data.get("label") or "").strip()
gh_user = (data.get("gh_user") or "").strip()
@@ -1647,16 +1763,9 @@ class Handler(BaseHTTPRequestHandler):
if gh_user and gh_user != login:
self._json({"ok": False, "msg": f"Token 对应的账号是 {login},与填写的 {gh_user} 不一致"})
return
- existing = next((a for a in accounts if a["gh_user"] == login), None)
- if existing:
- existing["token"] = token
- existing["private"] = private
- if label:
- existing["label"] = label
- active_id = existing["id"]
- save_accounts()
- log(f"GitHub 账号 {login} 的 Token 已更新")
- self._json({"ok": True, "msg": f"账号 {login} 已存在,Token 已更新并切换为当前账号"})
+ if next((a for a in accounts if a["gh_user"] == login), None):
+ self._json({"ok": False,
+ "msg": f"GitHub 账号 {login} 已存在,请用编辑功能(每行右边的✏️)更新 Token/备注"})
return
acc = {"id": uuid.uuid4().hex[:8], "label": label or login,
"gh_user": login, "token": token, "private": private,
@@ -1670,6 +1779,34 @@ class Handler(BaseHTTPRequestHandler):
log(f"新增 GitHub 账号 {login}({'私有' if private else '公开'}仓库)")
self._json({"ok": True, "msg": f"账号 {login} 验证通过,已添加"
+ ("并切换为当前账号" if active_id == acc["id"] else "")})
+ elif path == "/api/accounts/edit":
+ # 按 id 编辑已有账号:token 留空则保留,可改 label / private
+ aid = (data.get("id") or "").strip()
+ acc = find_account(aid)
+ if not acc:
+ self._json({"ok": False, "msg": "账号不存在"})
+ return
+ new_token = (data.get("token") or "").strip()
+ if new_token:
+ ok, login, err = gh_verify_token(new_token)
+ if not ok:
+ self._json({"ok": False, "msg": f"Token 验证失败: {err}"})
+ return
+ if login != acc["gh_user"]:
+ self._json({"ok": False,
+ "msg": f"Token 对应的账号是 {login},与当前账号 {acc['gh_user']} 不一致"})
+ return
+ acc["token"] = new_token
+ log(f"GitHub 账号 {acc['gh_user']} 的 Token 已通过编辑更新")
+ if "private" in data:
+ acc["private"] = bool(data.get("private"))
+ if "label" in data:
+ label = (data.get("label") or "").strip()
+ if label:
+ acc["label"] = label
+ save_accounts()
+ self._json({"ok": True,
+ "msg": f"账号 {acc['gh_user']} 已保存" + ("(Token 已更新)" if new_token else "")})
elif path == "/api/accounts/switch":
aid = (data.get("id") or "").strip()
if not find_account(aid):
diff --git a/GiteaHubSync/manifest b/GiteaHubSync/manifest
index b16e4e7..f0716e1 100644
--- a/GiteaHubSync/manifest
+++ b/GiteaHubSync/manifest
@@ -1,5 +1,5 @@
appname = GiteaHubSync
-version = 1.4.1
+version = 1.5.0
display_name = Gitea→GitHub 同步助手
desc = 每次推送到飞牛 Gitea 的代码,10 秒内自动镜像到 GitHub 的 7 个私有仓库。内置控制面板:自动同步开关、立即同步、仓库状态、同步日志,一键打开 Gitea。数据全部保存在 NAS 本地。
platform = all