spark-server/deploy/popadmin/_deploy_pull_pages.py
DESKTOP-L8OUL9F\gfl0034 40ce6c7dfa feat: 发布 PopAdmin 通讯录/相册/媒体拉取完整页与门禁
补齐三页查询/开关/授权/数量布局,并附上线上 static-override 与 jar 热补部署脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 14:34:45 +08:00

196 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""Deploy popadmin pull pages static + media-pull hotpatched jar to SG."""
from __future__ import annotations
import os
import time
from pathlib import Path
import paramiko
HOST = "47.236.66.9"
USER = "root"
PEM = Path(os.path.expandvars(r"%USERPROFILE%\.ssh\wyrs_shanghai_deploy.pem"))
LOCAL_JS = Path(r"F:\0000wyrs\Aliyun\src\main\resources\static\popadmin\js\popadmin.js")
LOCAL_CSS = Path(r"F:\0000wyrs\Aliyun\src\main\resources\static\popadmin\css\popadmin.css")
LOCAL_JAR = Path(r"F:\0000wyrs\Aliyun\target\remote-control-1.0.0.media-pull.jar")
REMOTE_STATIC = "/opt/wyrs-admin/static-override"
REMOTE_JAR = "/opt/wyrs-admin/remote-control-1.0.0.jar"
CACHE = "20261102-pullpages"
def run(client: paramiko.SSHClient, cmd: str, timeout: int = 180) -> tuple[int, str, str]:
print("$", cmd[:240])
_, stdout, stderr = client.exec_command(cmd, timeout=timeout)
out = stdout.read().decode("utf-8", "replace")
err = stderr.read().decode("utf-8", "replace")
code = stdout.channel.recv_exit_status()
if out.strip():
print(out[:2000])
if err.strip():
print("[stderr]", err[:1000])
return code, out, err
def main() -> None:
if not PEM.is_file():
raise SystemExit(f"missing pem {PEM}")
for p in (LOCAL_JS, LOCAL_CSS, LOCAL_JAR):
if not p.is_file():
raise SystemExit(f"missing {p}")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Auto-detect key type (RSA / Ed25519 / ECDSA)
key = None
for loader in (
paramiko.Ed25519Key.from_private_key_file,
paramiko.RSAKey.from_private_key_file,
paramiko.ECDSAKey.from_private_key_file,
):
try:
key = loader(str(PEM))
break
except Exception:
continue
if key is None:
raise SystemExit(f"cannot load private key {PEM}")
client.connect(HOST, username=USER, pkey=key, timeout=30)
sftp = client.open_sftp()
ts = time.strftime("%Y%m%d%H%M%S")
print("=== backup static ===")
run(
client,
f"cp -a {REMOTE_STATIC}/popadmin.js {REMOTE_STATIC}/popadmin.js.bak.pullpages.{ts} && "
f"cp -a {REMOTE_STATIC}/popadmin.css {REMOTE_STATIC}/popadmin.css.bak.pullpages.{ts} && "
f"cp -a {REMOTE_STATIC}/popadmin.index.html {REMOTE_STATIC}/popadmin.index.html.bak.pullpages.{ts}",
)
print("=== upload static ===")
sftp.put(str(LOCAL_JS), f"{REMOTE_STATIC}/popadmin.js")
# Prefer remote CSS if it has nav-fix; merge by keeping remote if larger/newer features.
# Still upload local CSS then restore nav scroll rules via sed if needed.
sftp.put(str(LOCAL_CSS), f"{REMOTE_STATIC}/popadmin.css")
# bump cache on index
idx_cmd = f"""python3 - <<'PY'
from pathlib import Path
p = Path('{REMOTE_STATIC}/popadmin.index.html')
s = p.read_text(encoding='utf-8')
import re
s2 = re.sub(r'popadmin\\.js\\?v=[^\"\\']+', 'popadmin.js?v={CACHE}', s)
s2 = re.sub(r'popadmin\\.css\\?v=[^\"\\']+', 'popadmin.css?v={CACHE}', s2)
p.write_text(s2, encoding='utf-8')
print('cache', '{CACHE}')
print('js', 'popadmin.js?v={CACHE}' in s2)
print('css', 'popadmin.css?v={CACHE}' in s2)
PY"""
run(client, idx_cmd)
# Ensure sidebar scroll rule exists (from earlier navfix)
run(
client,
f"""python3 - <<'PY'
from pathlib import Path
p = Path('{REMOTE_STATIC}/popadmin.css')
s = p.read_text(encoding='utf-8')
needle = '.sidebar nav'
rule = '''
/* pullpages: keep long nav scrollable */
.sidebar {{ display:flex; flex-direction:column; min-height:0; }}
.sidebar nav {{ flex:1; overflow-y:auto; overscroll-behavior:contain; }}
'''
if 'overscroll-behavior:contain' not in s:
p.write_text(s + '\\n' + rule, encoding='utf-8')
print('css nav scroll appended')
else:
print('css nav scroll already present')
PY""",
)
print("=== markers in deployed js ===")
run(
client,
f"python3 - <<'PY'\n"
f"s=open('{REMOTE_STATIC}/popadmin.js',encoding='utf-8',errors='replace').read()\n"
f"for k in ['async contacts(','contactsConsented','albumConsented','documentsConsented','用户未授权','mediaPullQId']:\n"
f" print(k, s.count(k))\n"
f"PY",
)
print("=== deploy jar ===")
run(client, f"cp -a {REMOTE_JAR} {REMOTE_JAR}.bak.pullpages.{ts}")
remote_tmp = f"/tmp/remote-control-1.0.0.media-pull.{ts}.jar"
print("uploading jar...", LOCAL_JAR.stat().st_size)
sftp.put(str(LOCAL_JAR), remote_tmp)
run(client, f"mv {remote_tmp} {REMOTE_JAR} && chmod 644 {REMOTE_JAR} && ls -lh {REMOTE_JAR}")
print("=== restart wyrs-admin ===")
run(client, "systemctl restart wyrs-admin", timeout=60)
# wait health
for i in range(30):
time.sleep(2)
code, out, _ = run(
client,
"curl -sS -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8090/api/admin/auth/login || true",
timeout=20,
)
# login endpoint should be 405/400/200 not 000
if out.strip() and out.strip() != "000":
print("health http", out.strip(), "attempt", i)
break
else:
print("WARN: health check timed out")
print("=== verify APIs ===")
verify = r'''
python3 - <<'PY'
import json, os, urllib.request
env={}
for line in open('/etc/wyrs-admin/app.env',encoding='utf-8',errors='replace'):
line=line.strip()
if not line or line.startswith('#') or '=' not in line: continue
k,v=line.split('=',1); env[k.strip()]=v.strip().strip('"').strip("'")
password=env.get('WYRS_ADMIN_PASSWORD') or ''
body=json.dumps({'username':'admin','password':password}).encode()
req=urllib.request.Request('http://127.0.0.1:8090/api/admin/auth/login',data=body,headers={'Content-Type':'application/json'},method='POST')
with urllib.request.urlopen(req,timeout=15) as resp:
token=json.loads(resp.read().decode())['data']['token']
for url in [
'http://127.0.0.1:8090/api/admin/spark/media-pull?userId=1017',
'http://127.0.0.1:8090/api/admin/spark/media-pull?email=spark.test.01@wyrs.test.local',
'http://127.0.0.1:8090/api/admin/spark/contacts/summary',
]:
req=urllib.request.Request(url,headers={'Authorization':'Bearer '+token})
try:
with urllib.request.urlopen(req,timeout=15) as resp:
raw=resp.read().decode()
print('OK', url, raw[:400])
except Exception as e:
body=''
if hasattr(e,'read'):
try: body=e.read().decode()
except Exception: pass
print('ERR', url, e, body[:300])
# PUT pullContacts false should work
req=urllib.request.Request(
'http://127.0.0.1:8090/api/admin/spark/media-pull?email=spark.test.01@wyrs.test.local',
data=json.dumps({'pullContacts':False,'count':25}).encode(),
headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'},
method='PUT')
with urllib.request.urlopen(req,timeout=15) as resp:
print('PUT', resp.read().decode()[:500])
PY'''
run(client, verify, timeout=60)
print("=== nginx reload (noop if unchanged) ===")
run(client, "nginx -t && nginx -s reload")
sftp.close()
client.close()
print("DEPLOY DONE cache=", CACHE)
if __name__ == "__main__":
main()