后端 GET /im/messages 增加 start/end(最长12个月),前端格式化 ts 并提供日/周/月/年/总/自定义筛选。 Co-authored-by: Cursor <cursoragent@cursor.com>
202 lines
6.9 KiB
Python
202 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Deploy popadmin IM messages UI (timestamp + date range) to static-override.
|
|
|
|
Also optionally rebuilds spark-server for start/end query support.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import paramiko
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
HOST = "47.236.66.9"
|
|
USER = "root"
|
|
PEM = Path(os.path.expandvars(r"%USERPROFILE%\.ssh\wyrs_shanghai_deploy.pem"))
|
|
LOCAL_JS = ROOT / "deploy" / "popadmin" / "popadmin.js"
|
|
REMOTE_STATIC = "/opt/wyrs-admin/static-override"
|
|
CACHE = time.strftime("20261102-imts-%H%M%S")
|
|
REMOTE_BUILD = "/tmp/spark-im-messages-build"
|
|
INCLUDE = ["cmd", "internal", "migrations", "api", "go.mod", "go.sum"]
|
|
EXTRA_FILES = [
|
|
(ROOT / "deploy" / "docker" / "Dockerfile.remote", "Dockerfile.remote"),
|
|
(ROOT / "deploy" / "docker" / "deploy_remote.sh", "deploy_remote.sh"),
|
|
]
|
|
|
|
|
|
def load_key(path: Path):
|
|
for loader in (
|
|
paramiko.Ed25519Key.from_private_key_file,
|
|
paramiko.RSAKey.from_private_key_file,
|
|
paramiko.ECDSAKey.from_private_key_file,
|
|
):
|
|
try:
|
|
return loader(str(path))
|
|
except Exception:
|
|
continue
|
|
raise SystemExit(f"cannot load private key {path}")
|
|
|
|
|
|
def run(client: paramiko.SSHClient, cmd: str, timeout: int = 180) -> tuple[int, 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()
|
|
text = (out + (("\n" + err) if err.strip() else "")).strip()
|
|
if text:
|
|
print(text[-12000:])
|
|
return code, text
|
|
|
|
|
|
def deploy_static(client: paramiko.SSHClient, sftp: paramiko.SFTPClient) -> None:
|
|
ts = time.strftime("%Y%m%d%H%M%S")
|
|
print("=== backup + upload static ===")
|
|
run(
|
|
client,
|
|
f"cp -a {REMOTE_STATIC}/popadmin.js {REMOTE_STATIC}/popadmin.js.bak.imts.{ts} && "
|
|
f"cp -a {REMOTE_STATIC}/popadmin.index.html {REMOTE_STATIC}/popadmin.index.html.bak.imts.{ts}",
|
|
)
|
|
sftp.put(str(LOCAL_JS), f"{REMOTE_STATIC}/popadmin.js")
|
|
idx_cmd = f"""python3 - <<'PY'
|
|
from pathlib import Path
|
|
import re
|
|
p = Path('{REMOTE_STATIC}/popadmin.index.html')
|
|
s = p.read_text(encoding='utf-8')
|
|
s2 = re.sub(r'popadmin\\.js\\?v=[^\"\\']+', 'popadmin.js?v={CACHE}', s)
|
|
p.write_text(s2, encoding='utf-8')
|
|
print('cache', '{CACHE}')
|
|
print('js_ok', 'popadmin.js?v={CACHE}' in s2)
|
|
print('has_fmtMsgTs', 'fmtMsgTs' in Path('{REMOTE_STATIC}/popadmin.js').read_text(encoding='utf-8', errors='replace'))
|
|
print('has_imPeriod', 'imPeriod' in Path('{REMOTE_STATIC}/popadmin.js').read_text(encoding='utf-8', errors='replace'))
|
|
PY"""
|
|
code, _ = run(client, idx_cmd)
|
|
if code != 0:
|
|
raise SystemExit(f"cache bump failed: {code}")
|
|
|
|
|
|
def make_tarball(path: Path) -> None:
|
|
with tarfile.open(path, "w:gz") as tar:
|
|
for name in INCLUDE:
|
|
p = ROOT / name
|
|
if not p.exists():
|
|
raise SystemExit(f"missing {p}")
|
|
tar.add(p, arcname=name)
|
|
for src, arc in EXTRA_FILES:
|
|
if not src.is_file():
|
|
raise SystemExit(f"missing {src}")
|
|
tar.add(src, arcname=arc)
|
|
print("tarball", path, path.stat().st_size, "bytes")
|
|
|
|
|
|
def deploy_backend(client: paramiko.SSHClient, sftp: paramiko.SFTPClient) -> None:
|
|
release = "im-range-" + time.strftime("%Y%m%d%H%M%S")
|
|
print("=== conflict probe ===")
|
|
code, out = run(
|
|
client,
|
|
"for f in /tmp/spark-image-limit-build.lock /tmp/spark-compose-mutate.lock "
|
|
"/tmp/sign-energy-deploy.lock /tmp/spark-im-messages-build.lock; do "
|
|
" if [ -f \"$f\" ]; then echo LOCKED:$f; cat \"$f\"; exit 99; fi; "
|
|
"done; echo locks_clear",
|
|
timeout=60,
|
|
)
|
|
if code != 0:
|
|
raise SystemExit(f"deploy locked: {out}")
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
tar_path = Path(td) / "spark-src.tgz"
|
|
make_tarball(tar_path)
|
|
run(client, f"rm -rf {REMOTE_BUILD}; mkdir -p {REMOTE_BUILD}", timeout=60)
|
|
print("upload tarball...")
|
|
sftp.put(str(tar_path), f"{REMOTE_BUILD}/spark-src.tgz")
|
|
for src, arc in EXTRA_FILES:
|
|
data = src.read_bytes().replace(b"\r\n", b"\n")
|
|
with sftp.file(f"{REMOTE_BUILD}/{arc}", "wb") as f:
|
|
f.write(data)
|
|
try:
|
|
f.chmod(0o755 if arc.endswith(".sh") else 0o644)
|
|
except Exception:
|
|
pass
|
|
|
|
print("=== remote docker build ===")
|
|
code, out = run(
|
|
client,
|
|
f"cd {REMOTE_BUILD} && chmod +x deploy_remote.sh && "
|
|
f"SPARK_RELEASE={release} SPARK_SRC_DIR={REMOTE_BUILD} "
|
|
f"SPARK_DEPLOY_LOCK=/tmp/spark-im-messages-build.lock "
|
|
f"./deploy_remote.sh",
|
|
timeout=1800,
|
|
)
|
|
if code != 0:
|
|
raise SystemExit(f"backend deploy failed: {code}")
|
|
# probe new string in binary
|
|
run(
|
|
client,
|
|
"docker exec spark-server sh -c "
|
|
"\"strings /usr/local/bin/spark-server | grep -F '查询跨度最长' | head -3\" "
|
|
"|| true",
|
|
timeout=60,
|
|
)
|
|
|
|
|
|
def verify(client: paramiko.SSHClient) -> None:
|
|
print("=== verify ===")
|
|
run(
|
|
client,
|
|
"python3 - <<'PY'\n"
|
|
"from pathlib import Path\n"
|
|
f"js=Path('{REMOTE_STATIC}/popadmin.js').read_text(encoding='utf-8',errors='replace')\n"
|
|
f"idx=Path('{REMOTE_STATIC}/popadmin.index.html').read_text(encoding='utf-8',errors='replace')\n"
|
|
"print('fmtMsgTs', 'fmtMsgTs' in js)\n"
|
|
"print('imPeriod', 'imPeriod' in js)\n"
|
|
"print('exceedsMonths', 'exceedsMonths' in js)\n"
|
|
"print('start_end_q', 'start=' in js and 'imPeriod' in js)\n"
|
|
f"print('cache_line', [ln for ln in idx.splitlines() if 'popadmin.js' in ln][:2])\n"
|
|
"PY",
|
|
timeout=60,
|
|
)
|
|
# API auth gate still 401; bad range after auth would 400 — check route alive
|
|
run(
|
|
client,
|
|
"curl -sS -o /tmp/im_msg.out -w '%{http_code}' "
|
|
"'http://127.0.0.1:18080/api/admin/spark/im/messages?limit=1&start=2020-01-01&end=2021-02-01'; "
|
|
"echo; head -c 200 /tmp/im_msg.out; echo",
|
|
timeout=30,
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
if not PEM.is_file():
|
|
print("missing pem", PEM)
|
|
return 1
|
|
if not LOCAL_JS.is_file():
|
|
print("missing", LOCAL_JS)
|
|
return 1
|
|
do_backend = "--skip-backend" not in sys.argv
|
|
|
|
client = paramiko.SSHClient()
|
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
client.connect(HOST, username=USER, pkey=load_key(PEM), timeout=30)
|
|
sftp = client.open_sftp()
|
|
try:
|
|
deploy_static(client, sftp)
|
|
if do_backend:
|
|
deploy_backend(client, sftp)
|
|
else:
|
|
print("skip backend")
|
|
verify(client)
|
|
print("DEPLOY DONE cache=", CACHE)
|
|
finally:
|
|
sftp.close()
|
|
client.close()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|