新增
This commit is contained in:
+153
-32
@@ -14,11 +14,16 @@ import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app_version import APP_VERSION, release_status
|
||||
from runtime_paths import application_data_dir
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
|
||||
SCRIPT_DIR = application_data_dir()
|
||||
CONNECTION_FILE = SCRIPT_DIR / "backend_connection.json"
|
||||
RUNTIME_FILE = SCRIPT_DIR / "backend_runtime.json"
|
||||
DEFAULT_SERVER_URL = "http://127.0.0.1:8765"
|
||||
DEFAULT_SERVER_URL = "http://xchat.zhenyangtang.com.cn"
|
||||
DESKTOP_SYNC_KEY = "wcrpa-v1-H3q9mT7xK2pN8cR5vL4sF6dB1yG0uJ"
|
||||
DESKTOP_CONFIG_PATH = "/api/v1/desktop/config"
|
||||
_LOCK = threading.RLock()
|
||||
|
||||
|
||||
@@ -30,19 +35,75 @@ class AuthenticationError(BackendError):
|
||||
"""登录状态无效。"""
|
||||
|
||||
|
||||
def _process_is_running(pid: int) -> bool:
|
||||
"""检查运行状态文件中的进程是否仍存在,不向进程发送终止信号。"""
|
||||
if pid <= 0:
|
||||
return False
|
||||
if pid == os.getpid():
|
||||
return True
|
||||
if os.name == "nt":
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
process_query_limited_information = 0x1000
|
||||
still_active = 259
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.OpenProcess.argtypes = [
|
||||
ctypes.c_ulong,
|
||||
ctypes.c_int,
|
||||
ctypes.c_ulong,
|
||||
]
|
||||
kernel32.OpenProcess.restype = ctypes.c_void_p
|
||||
kernel32.GetExitCodeProcess.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.POINTER(ctypes.c_ulong),
|
||||
]
|
||||
kernel32.GetExitCodeProcess.restype = ctypes.c_int
|
||||
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
kernel32.CloseHandle.restype = ctypes.c_int
|
||||
handle = kernel32.OpenProcess(
|
||||
process_query_limited_information, False, pid
|
||||
)
|
||||
if not handle:
|
||||
return False
|
||||
try:
|
||||
exit_code = ctypes.c_ulong()
|
||||
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
|
||||
return False
|
||||
return exit_code.value == still_active
|
||||
finally:
|
||||
kernel32.CloseHandle(handle)
|
||||
except (AttributeError, OSError, ValueError):
|
||||
return False
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def discover_local_runtime() -> dict[str, Any]:
|
||||
"""读取并校验同项目后台发布的运行信息。"""
|
||||
try:
|
||||
info = json.loads(RUNTIME_FILE.read_text(encoding="utf-8"))
|
||||
url = normalize_server_url(info.get("server_url"))
|
||||
port = int(info.get("port", 0))
|
||||
pid = int(info.get("pid", 0))
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
if not 1 <= port <= 65535 or parsed.port != port:
|
||||
if (
|
||||
not 1 <= port <= 65535
|
||||
or parsed.port != port
|
||||
or not _process_is_running(pid)
|
||||
):
|
||||
raise ValueError
|
||||
return {
|
||||
"server_url": url,
|
||||
"port": port,
|
||||
"pid": int(info.get("pid", 0)),
|
||||
"pid": pid,
|
||||
"local_sync_token": str(info.get("local_sync_token") or ""),
|
||||
"started_at": str(info.get("started_at") or ""),
|
||||
}
|
||||
@@ -77,6 +138,13 @@ def default_settings() -> dict[str, Any]:
|
||||
"last_version": 0,
|
||||
"last_sync_at": "",
|
||||
"last_error": "",
|
||||
"last_release": {
|
||||
"latest_version": APP_VERSION,
|
||||
"download_url": "",
|
||||
"release_notes": "",
|
||||
"force_upgrade": False,
|
||||
"updated_at": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +162,8 @@ def load_settings() -> dict[str, Any]:
|
||||
if discovered_url != DEFAULT_SERVER_URL and _is_local_server_url(settings["server_url"]):
|
||||
settings["server_url"] = discovered_url
|
||||
settings["auto_sync"] = bool(settings.get("auto_sync", True))
|
||||
if not isinstance(settings.get("last_release"), dict):
|
||||
settings["last_release"] = default_settings()["last_release"]
|
||||
try:
|
||||
settings["sync_interval_seconds"] = max(
|
||||
60, int(settings.get("sync_interval_seconds", 300))
|
||||
@@ -159,6 +229,7 @@ def _request(
|
||||
token: str = "",
|
||||
payload: dict[str, Any] | None = None,
|
||||
local_sync_token: str = "",
|
||||
desktop_sync_key: str = "",
|
||||
timeout: float = 10.0,
|
||||
) -> tuple[int, dict[str, Any]]:
|
||||
url = normalize_server_url(server_url) + path
|
||||
@@ -171,6 +242,8 @@ def _request(
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
if local_sync_token:
|
||||
headers["X-Desktop-Sync-Token"] = local_sync_token
|
||||
if desktop_sync_key:
|
||||
headers["X-Desktop-Sync-Key"] = desktop_sync_key
|
||||
request = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
@@ -257,7 +330,56 @@ def logout(*, revoke_remote: bool = True) -> None:
|
||||
save_settings(settings)
|
||||
|
||||
|
||||
def _apply_config_response(
|
||||
response: dict[str, Any], settings: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
config = response.get("config")
|
||||
if not isinstance(config, dict):
|
||||
raise BackendError("后台没有返回有效的模型配置")
|
||||
version = int(response.get("version", 0))
|
||||
import ai_config
|
||||
|
||||
applied = ai_config.apply_settings(config, persist=True)
|
||||
release = release_status(response.get("release"))
|
||||
cached_release = {
|
||||
key: release[key]
|
||||
for key in (
|
||||
"latest_version",
|
||||
"download_url",
|
||||
"release_notes",
|
||||
"force_upgrade",
|
||||
"updated_at",
|
||||
)
|
||||
}
|
||||
settings.update(
|
||||
{
|
||||
"last_version": version,
|
||||
"last_sync_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"last_error": "",
|
||||
"last_release": cached_release,
|
||||
}
|
||||
)
|
||||
save_settings(settings)
|
||||
return {
|
||||
"synced": True,
|
||||
"version": version,
|
||||
"applied_count": len(applied),
|
||||
"updated_at": response.get("updated_at", ""),
|
||||
"message": f"已同步云端配置 v{version}",
|
||||
"release": release,
|
||||
"local_app_version": APP_VERSION,
|
||||
"update_available": release["update_available"],
|
||||
"force_upgrade": release["force_upgrade"],
|
||||
}
|
||||
|
||||
|
||||
def cached_release_status() -> dict[str, Any]:
|
||||
"""网络不可用时读取上次成功同步的升级策略。"""
|
||||
return release_status(load_settings().get("last_release"))
|
||||
|
||||
|
||||
def sync_config(*, force: bool = False, timeout: float = 10.0) -> dict[str, Any]:
|
||||
"""保留账号登录和同机后台的兼容同步能力。"""
|
||||
settings = load_settings()
|
||||
if not is_configured(settings):
|
||||
return {"synced": False, "reason": "not_configured", "message": "尚未登录后台"}
|
||||
@@ -274,28 +396,33 @@ def sync_config(*, force: bool = False, timeout: float = 10.0) -> dict[str, Any]
|
||||
),
|
||||
timeout=timeout,
|
||||
)
|
||||
config = response.get("config")
|
||||
if not isinstance(config, dict):
|
||||
raise BackendError("后台没有返回有效的模型配置")
|
||||
version = int(response.get("version", 0))
|
||||
import ai_config
|
||||
|
||||
applied = ai_config.apply_settings(config, persist=True)
|
||||
settings.update(
|
||||
{
|
||||
"last_version": version,
|
||||
"last_sync_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"last_error": "",
|
||||
}
|
||||
)
|
||||
return _apply_config_response(response, settings)
|
||||
except Exception as exc:
|
||||
settings["last_error"] = str(exc)
|
||||
save_settings(settings)
|
||||
return {
|
||||
"synced": True,
|
||||
"version": version,
|
||||
"applied_count": len(applied),
|
||||
"updated_at": response.get("updated_at", ""),
|
||||
"message": f"已同步后台配置 v{version}",
|
||||
raise
|
||||
|
||||
|
||||
def sync_cloud_config(*, timeout: float = 10.0) -> dict[str, Any]:
|
||||
"""无需用户登录,从固定云端读取桌面运行配置。"""
|
||||
settings = load_settings()
|
||||
settings.update(
|
||||
{
|
||||
"server_url": DEFAULT_SERVER_URL,
|
||||
"username": "",
|
||||
"access_token": "",
|
||||
"auto_sync": True,
|
||||
}
|
||||
)
|
||||
try:
|
||||
_, response = _request(
|
||||
"GET",
|
||||
DEFAULT_SERVER_URL,
|
||||
DESKTOP_CONFIG_PATH,
|
||||
desktop_sync_key=DESKTOP_SYNC_KEY,
|
||||
timeout=timeout,
|
||||
)
|
||||
return _apply_config_response(response, settings)
|
||||
except Exception as exc:
|
||||
settings["last_error"] = str(exc)
|
||||
save_settings(settings)
|
||||
@@ -303,11 +430,5 @@ def sync_config(*, force: bool = False, timeout: float = 10.0) -> dict[str, Any]
|
||||
|
||||
|
||||
def startup_sync_config(*, timeout: float = 3.0) -> dict[str, Any]:
|
||||
"""软件启动前快速检测后台,使首屏直接使用服务器配置。"""
|
||||
if not is_configured():
|
||||
return {
|
||||
"synced": False,
|
||||
"reason": "not_configured",
|
||||
"message": "未发现可用的后台配置服务",
|
||||
}
|
||||
return sync_config(timeout=timeout)
|
||||
"""软件启动前先请求固定云端,使首屏直接使用服务器配置。"""
|
||||
return sync_cloud_config(timeout=timeout)
|
||||
|
||||
Reference in New Issue
Block a user