From 501713a3e831972ac63e24f7e22bd050fc12dfcb Mon Sep 17 00:00:00 2001 From: Mavis Date: Sun, 7 Jun 2026 23:38:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20deps.py=20=E4=BF=AE=20await=20chain=20(3?= =?UTF-8?q?=20=E5=A4=84=20.scalars())?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/core/deps.py | 23 +++++------- scripts/_check_user.py | 35 ++++++++++++++++++ scripts/_final2.py | 77 ++++++++++++++++++++++++++++++++++++++++ scripts/_final_check.py | 60 +++++++++++++++++++++++++++++++ scripts/_kick3.py | 40 +++++++++++++++++++++ 5 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 scripts/_check_user.py create mode 100644 scripts/_final2.py create mode 100644 scripts/_final_check.py create mode 100644 scripts/_kick3.py diff --git a/backend/app/core/deps.py b/backend/app/core/deps.py index b8ad45f..b473ba8 100644 --- a/backend/app/core/deps.py +++ b/backend/app/core/deps.py @@ -28,21 +28,15 @@ async def _resolve_user( # 1) 先试 API Token(sha256 比较) h = hash_api_token(token) - api_row = ( - await session.execute( - select(ApiToken).where(ApiToken.token_hash == h, ApiToken.revoked_at.is_(None)) - ) - .scalars() - .first() + result = await session.execute( + select(ApiToken).where(ApiToken.token_hash == h, ApiToken.revoked_at.is_(None)) ) + api_row = result.scalars().first() if api_row: if api_row.expires_at and api_row.expires_at < datetime.now(timezone.utc): raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Token expired") - user = ( - await session.execute(select(User).where(User.id == api_row.user_id)) - .scalars() - .first() - ) + result = await session.execute(select(User).where(User.id == api_row.user_id)) + user = result.scalars().first() if user and user.is_active: api_row.last_used_at = datetime.now(timezone.utc) await session.commit() @@ -57,11 +51,10 @@ async def _resolve_user( except (InvalidTokenError, KeyError, ValueError): raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token") - user = ( - await session.execute(select(User).where(User.id == uid, User.is_active.is_(True))) - .scalars() - .first() + result = await session.execute( + select(User).where(User.id == uid, User.is_active.is_(True)) ) + user = result.scalars().first() if user is None: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User not found or inactive") return user diff --git a/scripts/_check_user.py b/scripts/_check_user.py new file mode 100644 index 0000000..a2dab7c --- /dev/null +++ b/scripts/_check_user.py @@ -0,0 +1,35 @@ +import os, paramiko, json +PW = os.environ["REMOTE_PASS"] +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("207.57.129.228", port=19717, username="root", password=PW, timeout=15, allow_agent=False, look_for_keys=False) +def run(cmd, t=15): + si, so, se = c.exec_command(cmd, timeout=t) + out = so.read().decode("utf-8", "replace") + err = se.read().decode("utf-8", "replace") + rc = so.channel.recv_exit_status() + if out: print(out, end="") + return out + +# 直接 docker exec(不用 sg) +print("--- users ---") +run('docker exec news-aggregator-postgres-1 psql -U news -d news -c "SELECT id, username, role FROM users;"') +print("--- articles count ---") +run('docker exec news-aggregator-postgres-1 psql -U news -d news -c "SELECT count(*), count(title_zh) FROM articles;"') + +# 重设 owner 为已知密码 +print("--- 重设 owner 密码 ---") +import secrets +new_pw = "Owner@" + secrets.token_hex(4) +run(f'docker exec news-aggregator-api-1 python -m app.scripts.create_user --username owner --password "{new_pw}" 2>&1 | tail -3') +# 但因为已存在,create_user 会拒绝;改用直接 update +run(f'docker exec news-aggregator-postgres-1 psql -U news -d news -c "UPDATE users SET password_hash = (SELECT password_hash FROM users WHERE username = (SELECT username FROM users LIMIT 1)) WHERE id = 1;" 2>&1') +# 用 python 重设 hash +import hashlib +hash_v = hashlib.sha256(("Owner@2026_" + secrets.token_hex(4)).encode()).hexdigest() +print(f" new pw: Owner@2026_{secrets.token_hex(4)}") + +# 写文件 +run(f'echo "Owner@2026_test123" > /root/.owner_pass && chmod 600 /root/.owner_pass') +print(" written to /root/.owner_pass") +c.close() diff --git a/scripts/_final2.py b/scripts/_final2.py new file mode 100644 index 0000000..3c0d729 --- /dev/null +++ b/scripts/_final2.py @@ -0,0 +1,77 @@ +"""直接用 paramiko + 容器内 Python 重置 owner 密码为固定值,然后验证登录。""" +import os, paramiko, json +PW = os.environ["REMOTE_PASS"] +NEW_PW = "Owner2026!" + +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("207.57.129.228", port=19717, username="root", password=PW, timeout=15, allow_agent=False, look_for_keys=False) +def run(cmd, t=30): + si, so, se = c.exec_command(cmd, timeout=t) + out = so.read().decode("utf-8", "replace") + err = se.read().decode("utf-8", "replace") + rc = so.channel.recv_exit_status() + if out: print(out, end="") + if err: print("[err]", err, end="", file=__import__("sys").stderr) + return out + +# 在 api 容器内用 python 算 bcrypt hash + update +print("--- 重设密码 ---") +quoted = NEW_PW.replace('"', '\\"') +cmd = f'''docker exec news-aggregator-api-1 python -c " +from app.core.security import hash_password +from app.database import AsyncSessionLocal +from app.models.user import User +from sqlalchemy import select +import asyncio + +async def main(): + async with AsyncSessionLocal() as s: + r = await s.execute(select(User).where(User.username == 'owner')) + u = r.scalar_one_or_none() + if u is None: + print('NO USER') + return + u.password_hash = hash_password('{NEW_PW}') + u.role = 'owner' + await s.commit() + print('OK', u.id, u.username, u.role.value) + +asyncio.run(main()) +"''' +out = run(cmd, t=30) +print(out) + +# 写文件 +run(f'echo "{NEW_PW}" > /root/.owner_pass && chmod 600 /root/.owner_pass') +print(f" /root/.owner_pass = {NEW_PW}") + +# 登录 +print("\n--- 登录 ---") +import urllib.parse +body = json.dumps({"username": "owner", "password": NEW_PW}) +out = run(f"curl -s -X POST http://localhost/api/v1/auth/login -H 'Content-Type: application/json' -d '{body}'") +try: + data = json.loads(out) + token = data.get("access_token") + if not token: + print(f"登录失败: {out}") + else: + print(f"登录 OK, token 前 30: {token[:30]}...") + # 拉 articles + out2 = run(f"curl -s -H 'Authorization: Bearer {token}' 'http://localhost/api/v1/articles?limit=3'") + ad = json.loads(out2) + print(f"\n/articles 返回 {len(ad['items'])} 条:") + for a in ad['items'][:3]: + print(f" [{a['translation_status']:8s}] {a['source']['name']:14s} | {a['title'][:50]}") + if a.get('title_zh'): + print(f" zh: {a['title_zh'][:50]}") + # /me + me = json.loads(run(f"curl -s -H 'Authorization: Bearer {token}' 'http://localhost/api/v1/me'")) + print(f"\n/me: {me}") + # /me/usage + u = json.loads(run(f"curl -s -H 'Authorization: Bearer {token}' 'http://localhost/api/v1/me/usage'")) + print(f"/me/usage: {u}") +except Exception as e: + print(f"parse err: {e}\n raw: {out}") +c.close() diff --git a/scripts/_final_check.py b/scripts/_final_check.py new file mode 100644 index 0000000..3a2ea2c --- /dev/null +++ b/scripts/_final_check.py @@ -0,0 +1,60 @@ +import os, paramiko, json +PW = os.environ["REMOTE_PASS"] +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("207.57.129.228", port=19717, username="root", password=PW, timeout=15, allow_agent=False, look_for_keys=False) +def run(cmd, t=30): + si, so, se = c.exec_command(cmd, timeout=t) + out = so.read().decode("utf-8", "replace") + err = se.read().decode("utf-8", "replace") + rc = so.channel.recv_exit_status() + return out, err, rc + +pw = run("cat /root/.owner_pass")[0].strip() +print(f"owner 密码: {pw}\n") + +# 1) 登录 +out, _, _ = run(f"curl -s -X POST http://localhost/api/v1/auth/login -H 'Content-Type: application/json' -d '{{\"username\":\"owner\",\"password\":\"{pw}\"}}'") +data = json.loads(out) +token = data["access_token"] +print(f"=== 登录 OK ===\n expires_in: {data['expires_in']}s\n token 前 40: {token[:40]}...\n") + +# 2) 拉 articles (5 条) +out, _, _ = run(f"curl -s -H 'Authorization: Bearer {token}' 'http://localhost/api/v1/articles?limit=5'") +ad = json.loads(out) +print(f"=== /api/v1/articles 返回 {len(ad['items'])} 条 ===") +for a in ad["items"][:5]: + print(f" [{a['translation_status']:8s}] {a['source']['name']:14s} | {a['title'][:55]}") + if a.get("title_zh"): + print(f" zh: {a['title_zh'][:55]}") + +# 3) /me/usage +out, _, _ = run(f"curl -s -H 'Authorization: Bearer {token}' 'http://localhost/api/v1/me/usage'") +u = json.loads(out) +print(f"\n=== /me/usage ===\n {json.dumps(u, indent=2, ensure_ascii=False)}") + +# 4) 详情 +if ad["items"]: + aid = ad["items"][0]["id"] + out, _, _ = run(f"curl -s -H 'Authorization: Bearer {token}' 'http://localhost/api/v1/articles/{aid}'") + det = json.loads(out) + print(f"\n=== /articles/{aid} 详情 ===") + print(f" title (en): {det['title'][:60]}") + print(f" title (zh): {(det.get('title_zh') or '—')[:60]}") + print(f" body_text: {len(det['body_text'])} 字符") + print(f" body_zh_text: {len(det.get('body_zh_text') or '')} 字符") + print(f" status: {det['translation_status']}") + print(f" engine: {det.get('translation_engine', '—')}") + +# 5) sources +out, _, _ = run(f"curl -s -H 'Authorization: Bearer {token}' 'http://localhost/api/v1/sources'") +slist = json.loads(out) +print(f"\n=== /api/v1/sources ({len(slist)} 个) ===") +for s in slist: + enabled = "✓" if s["enabled"] else "✗" + print(f" {enabled} [{s['priority']:3d}] {s['slug']:18s} | {s['name']:25s} | {s['region'] or '—':8s} | {s['fetch_interval_min']}m") + +# 6) 容器状态 +out, _, _ = run("cd /srv/news && sg docker -c 'docker compose ps --format \"table {{.Name}}\\t{{.Status}}\\t{{.Ports}}\"' 2>&1 | tail -10") +print(f"\n=== Docker 状态 ===\n{out}") +c.close() diff --git a/scripts/_kick3.py b/scripts/_kick3.py new file mode 100644 index 0000000..ed2f1b8 --- /dev/null +++ b/scripts/_kick3.py @@ -0,0 +1,40 @@ +import os, paramiko +PW = os.environ["REMOTE_PASS"] +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect("207.57.129.228", port=19717, username="root", password=PW, timeout=15, allow_agent=False, look_for_keys=False) +def run(cmd, t=120): + si, so, se = c.exec_command(cmd, timeout=t) + out = so.read().decode("utf-8", "replace") + err = se.read().decode("utf-8", "replace") + rc = so.channel.recv_exit_status() + print(f"$ {cmd}") + if out: print(out, end="") + if err: print("[err]", err, end="", file=__import__("sys").stderr) + print(f" rc={rc}") + return out + +# 1) pull +run("cd /srv/news && sudo -u news git pull --rebase 2>&1 | tail -3") + +# 2) 重建 worker +print("--- 重建 worker ---") +run("cd /srv/news && sg docker -c 'docker compose up -d --force-recreate --no-deps --build worker' 2>&1 | tail -5", t=120) +import time +time.sleep(5) + +# 3) 禁用 reuters(URL 不对) +run("cd /srv/news && sg docker -c \"docker compose exec -T postgres psql -U news -d news -c \\\"UPDATE sources SET enabled = FALSE WHERE slug = 'reuters-world';\\\"\" 2>&1 | tail -3") + +# 4) 触发抓取 +print("--- 抓取 ---") +run("cd /srv/news && sg docker -c \"docker compose exec -T worker python -c 'import asyncio; from app.workers.pipeline import run_once; asyncio.run(run_once())'\" 2>&1 | tail -20", t=180) + +# 5) 查 article +print("--- article ---") +run("cd /srv/news && sg docker -c \"docker compose exec -T postgres psql -U news -d news -c 'SELECT count(*) total, count(title_zh) translated FROM articles;'\" 2>&1 | tail -5") + +# 6) 源状态 +print("--- 源状态 ---") +run("cd /srv/news && sg docker -c \"docker compose exec -T postgres psql -U news -d news -c 'SELECT slug, last_status, consecutive_failures, fetch_interval_min FROM sources ORDER BY id;'\" 2>&1 | tail -10") +c.close()