站点配置(百世密码/韵达账密/安能路径)从 config.yaml 移至 state.db

- 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 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-07-18 08:37:15 +08:00
parent 96e7018146
commit 6d45a9f5e5
5 changed files with 100 additions and 35 deletions

View File

@@ -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=""): def launch_and_prepare(debug_mode=False, debug_target=""):
"""启动 Playwright + 各站 page + 就绪轮询 + 弹窗清理 + 心跳初值,返回 RuntimeContext。 """启动 Playwright + 各站 page + 就绪轮询 + 弹窗清理 + 心跳初值,返回 RuntimeContext。
必须在"持有 Playwright 的线程"调用(交互模式主线程 / 服务模式 worker 线程)。 必须在"持有 Playwright 的线程"调用(交互模式主线程 / 服务模式 worker 线程)。
阻塞至所有站点登录就绪才返回。 阻塞至所有站点登录就绪才返回。
""" """
# 0. 状态库建表/迁移(须在 reset_login_states 等读写新字段的调用之前) # 0. 状态库建表/迁移 + 从 config.yaml 灌入站点配置(须在 reset_login_states 等之前)
state_store.init_db() state_store.init_db()
seed_legacy_config()
# 1. 读 configanneng.app_path # 1. 安能 Electron 应用路径state.db前端站点配置seed 已灌入
anneng_app_path = "" anneng_app_path = state_store.get_setting("安能", "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}")
# 2. 确定要挂载的网页站点 + 安能标记 # 2. 确定要挂载的网页站点 + 安能标记
anneng_active = False anneng_active = False

View File

@@ -17,7 +17,7 @@ import queue
import threading import threading
import time import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Optional from typing import Dict, Optional
import uvicorn import uvicorn
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
@@ -190,6 +190,7 @@ class ConfigRequest(BaseModel):
actual_offset: Optional[int] = None actual_offset: Optional[int] = None
schedule_enabled: Optional[bool] = None schedule_enabled: Optional[bool] = None
schedule_time: Optional[str] = None schedule_time: Optional[str] = None
settings: Optional[Dict[str, str]] = None
def _default_cfg(): def _default_cfg():
@@ -236,7 +237,21 @@ def set_config(site: str, req: ConfigRequest):
) )
state_store.set_schedule(site, enabled, time_str) state_store.set_schedule(site, enabled, time_str)
_reschedule_site(site) _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" REPORT_FILE = "应到未到数据.xlsx"

View File

@@ -5,6 +5,7 @@ import yaml
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
from paths import DOWNLOAD_DIR, CONFIG_PATH from paths import DOWNLOAD_DIR, CONFIG_PATH
import state_store
def with_retry(site_name, label, flow, reset, max_attempts=3): 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") page.locator(".ant-modal-title", has_text="导出设置").wait_for(state="visible")
# 4. 加载 YAML 配置并输入密码 # 4. 读取百世导出密码(存 state.db由前端站点配置
print(">> 正在读取配置并填写密码...") print(">> 正在读取配置并填写密码...")
password = "" password = state_store.get_setting("百世", "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: if not password:
print( print(" ⚠️ 警告:未设置百世导出密码(前端站点配置),可能导致导出失败。")
" ⚠️ 警告:在 config.yaml 中未找到百世的登录密码,可能导致导出失败。"
)
except Exception as e:
print(f" ⚠️ 读取 config.yaml 失败,请确保文件存在且格式正确: {e}")
return False
page.get_by_placeholder("请输入登录密码").fill(password) page.get_by_placeholder("请输入登录密码").fill(password)

View File

@@ -73,15 +73,9 @@ def yunda_login(page):
switch_btn.click() switch_btn.click()
page.wait_for_timeout(500) page.wait_for_timeout(500)
# 从配置读取凭证(默认空串;真实凭据仅存于被忽略的 config.yaml # 凭证存 state.db由前端站点配置
username = "" username = state_store.get_setting("韵达", "username")
password = "" password = state_store.get_setting("韵达", "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", ""))
print(f" -> 正在填充登录表单 (账号: {username})...") print(f" -> 正在填充登录表单 (账号: {username})...")
page.locator("#username").fill(username) page.locator("#username").fill(username)

View File

@@ -96,6 +96,14 @@ def init_db():
) )
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
conn.execute("""
CREATE TABLE IF NOT EXISTS site_settings (
site TEXT,
key TEXT,
value TEXT,
PRIMARY KEY (site, key)
)
""")
conn.commit() 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}
# ============================ 任务历史 ============================ # ============================ 任务历史 ============================