Skip to content

Commit 3920b3a

Browse files
committed
fix: preserve API settings and report env write failures
1 parent b09bfdd commit 3920b3a

12 files changed

Lines changed: 195 additions & 4 deletions

File tree

desktop_qt_ui/locales/en_US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,7 @@
12981298
"Skip only when OCR text exactly matches one of these rules.": "Skip only when OCR text exactly matches one of these rules.",
12991299
"Warning": "Warning",
13001300
"Error": "Error",
1301+
"Configuration save failed. Changes were not saved. Check file permissions and antivirus or security software blocking access.": "Configuration save failed. Changes were not saved. Check file permissions and antivirus or security software blocking access.",
13011302
"Filter:": "Filter:",
13021303
"Type to filter by pattern / replace / comment...": "Type to filter by pattern / replace / comment...",
13031304
"Selection": "Selection",

desktop_qt_ui/locales/es_ES.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,7 @@
12891289
"Skip only when OCR text exactly matches one of these rules.": "Omitir solo cuando el texto OCR coincida exactamente con una de estas reglas.",
12901290
"Warning": "Advertencia",
12911291
"Error": "Error",
1292+
"Configuration save failed. Changes were not saved. Check file permissions and antivirus or security software blocking access.": "No se pudo guardar la configuración. Los cambios no se guardaron. Compruebe los permisos del archivo y si el antivirus o el software de seguridad está bloqueando el acceso.",
12921293
"Filter:": "Filtrar:",
12931294
"Type to filter by pattern / replace / comment...": "Escriba para filtrar por patrón / reemplazo / comentario...",
12941295
"Selection": "Selección",

desktop_qt_ui/locales/ja_JP.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,7 @@
12891289
"Skip only when OCR text exactly matches one of these rules.": "OCR テキストが以下のいずれかと完全一致する場合のみ、そのテキスト領域をスキップします。",
12901290
"Warning": "警告",
12911291
"Error": "エラー",
1292+
"Configuration save failed. Changes were not saved. Check file permissions and antivirus or security software blocking access.": "設定の保存に失敗し、変更は保存されませんでした。ファイルのアクセス権限、およびウイルス対策・セキュリティソフトによるアクセス遮断を確認してください。",
12921293
"Filter:": "フィルター:",
12931294
"Type to filter by pattern / replace / comment...": "パターン / 置換 / コメントで絞り込み...",
12941295
"Selection": "選択範囲",

desktop_qt_ui/locales/ko_KR.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,7 @@
12891289
"Skip only when OCR text exactly matches one of these rules.": "OCR 텍스트가 아래 규칙 중 하나와 완전히 일치할 때만 해당 텍스트 영역을 건너뜁니다.",
12901290
"Warning": "경고",
12911291
"Error": "오류",
1292+
"Configuration save failed. Changes were not saved. Check file permissions and antivirus or security software blocking access.": "구성 저장에 실패하여 변경 사항이 저장되지 않았습니다. 파일 권한 및 백신 또는 보안 소프트웨어의 접근 차단 여부를 확인하세요.",
12921293
"Filter:": "필터:",
12931294
"Type to filter by pattern / replace / comment...": "패턴 / 치환 / 주석으로 필터링하려면 입력...",
12941295
"Selection": "선택 영역",

desktop_qt_ui/locales/zh_CN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,7 @@
12981298
"Skip only when OCR text exactly matches one of these rules.": "仅当 OCR 文本与以下某条规则完全一致时跳过该文本区域。",
12991299
"Warning": "警告",
13001300
"Error": "错误",
1301+
"Configuration save failed. Changes were not saved. Check file permissions and antivirus or security software blocking access.": "配置保存失败,修改未保存。请检查文件权限,以及杀毒软件或安全软件是否拦截了文件访问。",
13011302
"Filter:": "过滤:",
13021303
"Type to filter by pattern / replace / comment...": "输入匹配 / 替换 / 备注以过滤...",
13031304
"Selection": "选区",

desktop_qt_ui/locales/zh_TW.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,6 +1291,7 @@
12911291
"Skip only when OCR text exactly matches one of these rules.": "僅當 OCR 文字與以下某條規則完全一致時跳過該文字區域。",
12921292
"Warning": "警告",
12931293
"Error": "錯誤",
1294+
"Configuration save failed. Changes were not saved. Check file permissions and antivirus or security software blocking access.": "設定儲存失敗,變更未儲存。請檢查檔案權限,以及防毒軟體或安全軟體是否攔截了檔案存取。",
12941295
"Filter:": "過濾:",
12951296
"Type to filter by pattern / replace / comment...": "輸入匹配 / 替換 / 備註以過濾...",
12961297
"Selection": "選區",

desktop_qt_ui/services/config_service.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
format_env_line,
3434
load_app_dotenv,
3535
read_dotenv_file,
36+
remove_invalid_dotenv_lines,
3637
validate_env_key,
3738
)
3839
from manga_translator.utils.openai_compat import resolve_openai_compatible_api_key
@@ -159,6 +160,8 @@ def __init__(self, root_dir: str):
159160
else:
160161
self.env_path = os.path.join(self.root_dir, ".env")
161162
os.environ[APP_DOTENV_PATH_ENV] = self.env_path
163+
self._deferred_write_error: Optional[str] = None
164+
self._remove_invalid_env_lines()
162165
try:
163166
self._env_values = read_dotenv_file(self.env_path)
164167
load_app_dotenv(self.env_path, override=True)
@@ -201,6 +204,22 @@ def __init__(self, root_dir: str):
201204
self._config_cache = None
202205
self._initialize_write_pipeline()
203206

207+
def _remove_invalid_env_lines(self) -> None:
208+
try:
209+
removed = remove_invalid_dotenv_lines(self.env_path)
210+
except Exception as exc:
211+
self._deferred_write_error = f"{self.env_path}: {exc}"
212+
self.logger.error(f"清理 .env 解析错误行失败: {exc}")
213+
return
214+
if removed:
215+
self.logger.warning(f"已删除 .env 中 {removed} 行无法解析的配置")
216+
217+
def take_deferred_write_error(self) -> Optional[str]:
218+
error = self._deferred_write_error
219+
self._deferred_write_error = None
220+
return error
221+
222+
204223
def _initialize_write_pipeline(self) -> None:
205224
self._write_lock = threading.RLock()
206225
self._pending_config_writes: Dict[str, Dict[str, Any]] = {}
@@ -654,7 +673,7 @@ def on_done(done_future: Future) -> None:
654673
message,
655674
exc_info=(type(error), error, error.__traceback__),
656675
)
657-
self.write_failed.emit(message)
676+
self.write_failed.emit(str(error))
658677

659678
future.add_done_callback(on_done)
660679
return future
@@ -704,6 +723,7 @@ def reload_config(self):
704723
"""
705724
self.logger.info("正在强制重新加载配置...")
706725
self.flush_pending_writes()
726+
self._remove_invalid_env_lines()
707727

708728
# 1. 重新加载 .env 文件到 os.environ。翻译引擎会自动从此读取。
709729
load_app_dotenv(self.env_path, override=True)

desktop_qt_ui/ui/main_page/env_management.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -843,9 +843,17 @@ def flush_env_var_immediately(self, key: str):
843843

844844

845845
def flush_all_pending_env_vars(self, wait: bool = True):
846-
"""显式 API/预设操作可等待 ConfigService 原子落盘。"""
847-
config_service = getattr(self.controller, 'config_service', None)
848-
flush = getattr(config_service, 'flush_pending_writes', None)
846+
"""先提交当前控件值,再按需等待 ConfigService 原子落盘。"""
847+
config_service = getattr(self.controller, "config_service", None)
848+
save_many = getattr(config_service, "save_env_vars", None)
849+
if callable(save_many):
850+
visible_values = {
851+
key: _get_env_widget_value(widget)
852+
for key, (_label, widget) in getattr(self, "env_widgets", {}).items()
853+
}
854+
if visible_values:
855+
save_many(visible_values)
856+
flush = getattr(config_service, "flush_pending_writes", None)
849857
if wait and callable(flush):
850858
flush()
851859

desktop_qt_ui/ui/main_window.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,16 @@ def _connect_signals(self):
452452
self.app_logic.warning_dialog_requested.connect(
453453
self._show_warning_dialog, type=Qt.ConnectionType.QueuedConnection
454454
)
455+
self.config_service.write_failed.connect(
456+
self._show_config_write_failed,
457+
type=Qt.ConnectionType.QueuedConnection,
458+
)
459+
deferred_write_error = self.config_service.take_deferred_write_error()
460+
if deferred_write_error:
461+
QTimer.singleShot(
462+
0,
463+
lambda error=deferred_write_error: self._show_config_write_failed(error),
464+
)
455465
self.file_list_data_service.loading.connect(
456466
self._on_main_catalog_loading,
457467
type=Qt.ConnectionType.QueuedConnection,
@@ -467,6 +477,7 @@ def _connect_signals(self):
467477

468478
# --- View to Logic Connections ---
469479
self.main_view.setting_changed.connect(self.app_logic.update_single_config)
480+
self.main_view.env_var_changed.connect(self.app_logic.save_env_var)
470481
self.main_view.editor_view_requested.connect(self.switch_to_editor_view)
471482
self.main_view.theme_change_requested.connect(self._change_theme)
472483
self.main_view.language_change_requested.connect(
@@ -812,6 +823,22 @@ def _show_warning_dialog(self, message: str):
812823
except Exception as e:
813824
self.logger.error(f"_show_warning_dialog error: {e}", exc_info=True)
814825

826+
@pyqtSlot(str)
827+
def _show_config_write_failed(self, error: str):
828+
"""明确提示配置写入失败,避免用户误以为 API Key 已保存。"""
829+
try:
830+
guidance = self._t(
831+
"Configuration save failed. Changes were not saved. Check file permissions and antivirus or security software blocking access."
832+
)
833+
detail = str(error or "").strip()
834+
message = f"{guidance}\n\n{detail}" if detail else guidance
835+
show_error_dialog(self, self._t("Error"), "", message)
836+
except Exception as exc:
837+
self.logger.error(
838+
f"_show_config_write_failed error: {exc}", exc_info=True
839+
)
840+
841+
815842
def switch_to_editor_view(self):
816843
"""
817844
Simply switches to the editor view without reloading file lists.

manga_translator/utils/dotenv_utils.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import os
88
import re
9+
import tempfile
910
from pathlib import Path
1011
from typing import Dict, Mapping, Optional, Union
1112

@@ -61,6 +62,49 @@ def read_dotenv_file(path: PathLike) -> Dict[str, str]:
6162
}
6263

6364

65+
def remove_invalid_dotenv_lines(path: PathLike) -> int:
66+
"""Delete statements that python-dotenv cannot parse, preserving valid content."""
67+
env_path = Path(path)
68+
if not env_path.exists():
69+
return 0
70+
71+
lines: list[str] = []
72+
removed = 0
73+
with env_path.open("r", encoding="utf-8") as source:
74+
for mapping in parse_stream(source):
75+
if mapping.error:
76+
removed += 1
77+
continue
78+
lines.append(mapping.original.string)
79+
80+
if removed:
81+
temp_path: str | None = None
82+
try:
83+
with tempfile.NamedTemporaryFile(
84+
mode="w",
85+
encoding="utf-8",
86+
newline="",
87+
dir=env_path.parent,
88+
prefix=f".{env_path.name}.",
89+
suffix=".tmp",
90+
delete=False,
91+
) as temp_file:
92+
temp_path = temp_file.name
93+
temp_file.write("".join(lines))
94+
temp_file.flush()
95+
os.fsync(temp_file.fileno())
96+
os.replace(temp_path, env_path)
97+
temp_path = None
98+
finally:
99+
if temp_path:
100+
try:
101+
os.remove(temp_path)
102+
except OSError:
103+
pass
104+
return removed
105+
106+
107+
64108
def load_app_dotenv(
65109
dotenv_path: Optional[PathLike] = None,
66110
*,

0 commit comments

Comments
 (0)