From 6d45a9f5e5eecbe3c176c9f74f016e0dab3308c6 Mon Sep 17 00:00:00 2001 From: Misaka Date: Sat, 18 Jul 2026 08:37:15 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AB=99=E7=82=B9=E9=85=8D=E7=BD=AE=EF=BC=88?= =?UTF-8?q?=E7=99=BE=E4=B8=96=E5=AF=86=E7=A0=81/=E9=9F=B5=E8=BE=BE?= =?UTF-8?q?=E8=B4=A6=E5=AF=86/=E5=AE=89=E8=83=BD=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=EF=BC=89=E4=BB=8E=20config.yaml=20=E7=A7=BB=E8=87=B3=20state.d?= =?UTF-8?q?b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - state_store:加 site_settings(site,key,value) 表 + get_setting/set_setting/get_site_settings - runtime:seed_legacy_config 启动一次性从 config.yaml 灌入(已存在不覆盖);安能 app_path 改读 state_store - site_baishi:导出密码改读 get_setting("百世","password") - site_yunda:登录账密改读 get_setting("韵达",...) - server:/config PUT 接受 settings;新增 GET /config/{site}/settings(不进 5s 轮询) debug 仍保留在 config.yaml;站点配置现由前端配置弹窗管理(state.db 为准)。 Co-Authored-By: Claude --- runtime.py | 40 ++++++++++++++++++++++++++++++---------- server.py | 19 +++++++++++++++++-- site_baishi.py | 19 +++++-------------- site_yunda.py | 12 +++--------- state_store.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 35 deletions(-) diff --git a/runtime.py b/runtime.py index a15c9d8..50b471f 100644 --- a/runtime.py +++ b/runtime.py @@ -206,24 +206,44 @@ class RuntimeContext: # ============================ 启动 + 就绪 + 弹窗 ============================ +def seed_legacy_config(): + """一次性:把 config.yaml 里的站点配置(百世密码 / 韵达账密 / 安能 exe 路径) + 灌入 state.db。已存在的值不覆盖(前端改过的不动)。""" + if not os.path.exists(CONFIG_PATH): + return + try: + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) or {} + except Exception as e: + print(f"⚠️ 读取 config.yaml 做 seed 失败: {e}") + return + items = [ + ("百世", "password", (cfg.get("baishi", {}) or {}).get("password", "")), + ("韵达", "username", (cfg.get("yunda", {}) or {}).get("username", "")), + ("韵达", "password", (cfg.get("yunda", {}) or {}).get("password", "")), + ("安能", "app_path", (cfg.get("anneng", {}) or {}).get("app_path", "")), + ] + seeded = [] + for site, key, val in items: + if val and not state_store.get_setting(site, key): + state_store.set_setting(site, key, str(val)) + seeded.append(f"{site}.{key}") + if seeded: + print(f">> [seed] 从 config.yaml 灌入站点配置: {', '.join(seeded)}") + + def launch_and_prepare(debug_mode=False, debug_target=""): """启动 Playwright + 各站 page + 就绪轮询 + 弹窗清理 + 心跳初值,返回 RuntimeContext。 必须在"持有 Playwright 的线程"调用(交互模式主线程 / 服务模式 worker 线程)。 阻塞至所有站点登录就绪才返回。 """ - # 0. 状态库建表/迁移(须在 reset_login_states 等读写新字段的调用之前) + # 0. 状态库建表/迁移 + 从 config.yaml 灌入站点配置(须在 reset_login_states 等之前) state_store.init_db() + seed_legacy_config() - # 1. 读 config(anneng.app_path) - anneng_app_path = "" - try: - if os.path.exists(CONFIG_PATH): - with open(CONFIG_PATH, "r", encoding="utf-8") as f: - config = yaml.safe_load(f) or {} - anneng_app_path = (config.get("anneng", {}) or {}).get("app_path", "") - except Exception as e: - print(f"⚠️ 读取 config.yaml 异常: {e}") + # 1. 安能 Electron 应用路径(state.db,前端站点配置;seed 已灌入) + anneng_app_path = state_store.get_setting("安能", "app_path") # 2. 确定要挂载的网页站点 + 安能标记 anneng_active = False diff --git a/server.py b/server.py index 7ff2556..8bfc5ca 100644 --- a/server.py +++ b/server.py @@ -17,7 +17,7 @@ import queue import threading import time from contextlib import asynccontextmanager -from typing import Optional +from typing import Dict, Optional import uvicorn from apscheduler.schedulers.background import BackgroundScheduler @@ -190,6 +190,7 @@ class ConfigRequest(BaseModel): actual_offset: Optional[int] = None schedule_enabled: Optional[bool] = None schedule_time: Optional[str] = None + settings: Optional[Dict[str, str]] = None def _default_cfg(): @@ -236,7 +237,21 @@ def set_config(site: str, req: ConfigRequest): ) state_store.set_schedule(site, enabled, time_str) _reschedule_site(site) - return state_store.get_all_config().get(site, _default_cfg()) + # 站点专属配置(密码/账号/路径…) + if req.settings: + for k, v in req.settings.items(): + state_store.set_setting(site, k, v) + cfg = state_store.get_all_config().get(site, _default_cfg()) + cfg["settings"] = state_store.get_site_settings(site) + return cfg + + +@app.get("/config/{site}/settings") +def get_site_settings_api(site: str): + """单站专属配置(密码/账号/路径…;不进 5s 轮询)。""" + if site not in ALL_SITES: + raise HTTPException(status_code=400, detail=f"未知站点: {site}") + return state_store.get_site_settings(site) REPORT_FILE = "应到未到数据.xlsx" diff --git a/site_baishi.py b/site_baishi.py index 969cd8b..e657284 100644 --- a/site_baishi.py +++ b/site_baishi.py @@ -5,6 +5,7 @@ import yaml from playwright.sync_api import sync_playwright from paths import DOWNLOAD_DIR, CONFIG_PATH +import state_store def with_retry(site_name, label, flow, reset, max_attempts=3): @@ -143,21 +144,11 @@ def baishi_download_undelivered_data_impl(page): # 验证模态框弹出 page.locator(".ant-modal-title", has_text="导出设置").wait_for(state="visible") - # 4. 加载 YAML 配置并输入密码 + # 4. 读取百世导出密码(存 state.db,由前端站点配置) print(">> 正在读取配置并填写密码...") - password = "" - try: - with open(CONFIG_PATH, "r", encoding="utf-8") as f: - config = yaml.safe_load(f) - password = config.get("baishi", {}).get("password", "") - - if not password: - print( - " ⚠️ 警告:在 config.yaml 中未找到百世的登录密码,可能导致导出失败。" - ) - except Exception as e: - print(f" ⚠️ 读取 config.yaml 失败,请确保文件存在且格式正确: {e}") - return False + password = state_store.get_setting("百世", "password") + if not password: + print(" ⚠️ 警告:未设置百世导出密码(前端站点配置),可能导致导出失败。") page.get_by_placeholder("请输入登录密码").fill(password) diff --git a/site_yunda.py b/site_yunda.py index f2478ab..8ba848b 100644 --- a/site_yunda.py +++ b/site_yunda.py @@ -73,15 +73,9 @@ def yunda_login(page): switch_btn.click() page.wait_for_timeout(500) - # 从配置读取凭证(默认空串;真实凭据仅存于被忽略的 config.yaml) - username = "" - password = "" - if os.path.exists(CONFIG_PATH): - with open(CONFIG_PATH, "r", encoding="utf-8") as f: - config = yaml.safe_load(f) or {} - yd_cfg = config.get("yunda", {}) - username = str(yd_cfg.get("username", "")) - password = str(yd_cfg.get("password", "")) + # 凭证存 state.db(由前端站点配置) + username = state_store.get_setting("韵达", "username") + password = state_store.get_setting("韵达", "password") print(f" -> 正在填充登录表单 (账号: {username})...") page.locator("#username").fill(username) diff --git a/state_store.py b/state_store.py index 9906144..84761d7 100644 --- a/state_store.py +++ b/state_store.py @@ -96,6 +96,14 @@ def init_db(): ) except sqlite3.OperationalError: pass + conn.execute(""" + CREATE TABLE IF NOT EXISTS site_settings ( + site TEXT, + key TEXT, + value TEXT, + PRIMARY KEY (site, key) + ) + """) conn.commit() @@ -282,6 +290,43 @@ def get_all_config(): } +# ============================ 站点键值配置(site_settings)============================ +# 各站专属配置(百世密码、韵达账密、安能 exe 路径…),由前端配置弹窗设置。 + + +def get_setting(site, key): + """读取单站某个配置值;未设置返回 ''。""" + if not os.path.exists(STATE_DB_PATH): + return "" + with sqlite3.connect(STATE_DB_PATH) as conn: + row = conn.execute( + "SELECT value FROM site_settings WHERE site=? AND key=?", (site, key) + ).fetchone() + return row[0] if row else "" + + +def set_setting(site, key, value): + """设置单站某个配置值(upsert)。""" + with sqlite3.connect(STATE_DB_PATH) as conn: + conn.execute( + "INSERT INTO site_settings (site, key, value) VALUES (?, ?, ?) " + "ON CONFLICT(site, key) DO UPDATE SET value=excluded.value", + (site, key, value if value is not None else ""), + ) + conn.commit() + + +def get_site_settings(site): + """返回单站全部配置 {key: value}。""" + if not os.path.exists(STATE_DB_PATH): + return {} + with sqlite3.connect(STATE_DB_PATH) as conn: + rows = conn.execute( + "SELECT key, value FROM site_settings WHERE site=?", (site,) + ).fetchall() + return {r[0]: r[1] for r in rows} + + # ============================ 任务历史 ============================