From 46afae4b9847967f347a358fc8b44b8a74309c90 Mon Sep 17 00:00:00 2001 From: chengma Date: Wed, 8 Jul 2026 11:22:47 +0800 Subject: [PATCH] feat: tune cmhub image timeouts --- app/ai.py | 2 +- app/appconfig.py | 29 +++++++++++++++++++++++++---- app/gui/tabs/settings.py | 13 +++++++++++-- docs/tasks/T-553.md | 11 ++++++++++- tests/test_ai.py | 23 +++++++++++++---------- tests/test_appconfig.py | 18 ++++++++++++++++++ tests/test_gui.py | 2 +- 7 files changed, 79 insertions(+), 19 deletions(-) diff --git a/app/ai.py b/app/ai.py index a26808a..e4c747e 100644 --- a/app/ai.py +++ b/app/ai.py @@ -42,7 +42,7 @@ class CMHubError(AIError): CMHUB_IMAGE_MAX_BYTES = 20 * 1024 * 1024 CMHUB_TITLE_READ_TIMEOUT_SECONDS = 600 -CMHUB_IMAGE_READ_TIMEOUT_SECONDS = 650 +CMHUB_IMAGE_READ_TIMEOUT_SECONDS = 900 CMHUB_IMAGE_CONCURRENCY_LIMIT = 5 CMHUB_IMAGE_DOWNLOAD_ATTEMPTS = 3 CMHUB_IMAGE_SLOW_DOWNLOAD_SECONDS = 20.0 diff --git a/app/appconfig.py b/app/appconfig.py index 13f5aec..3d56e39 100644 --- a/app/appconfig.py +++ b/app/appconfig.py @@ -44,6 +44,8 @@ AI_CONCURRENCY_MIN = 1 AI_CONCURRENCY_MAX = 5 AI_RETRY_MIN = 0 AI_RETRY_MAX = 10 +CMHUB_CONNECT_TIMEOUT_DEFAULT = 66 +CMHUB_CONNECT_TIMEOUT_OLD_DEFAULT = 10 RUNTIME_CONFIG_KEYS = { "config_path", "ai_models_path", @@ -81,7 +83,7 @@ DEFAULT_CONFIG = { "base_url": "", "title_alias": "", "image_alias": "", - "connect_timeout": 10, + "connect_timeout": CMHUB_CONNECT_TIMEOUT_DEFAULT, "use_system_proxy": False, "download_with_curl": "auto", "check_balance_before_batch": False, @@ -350,7 +352,7 @@ def _deep_merge(defaults, loaded): return merged -def _normalize_config_values(config): +def _normalize_config_values(config, migrate_old_cmhub_connect_timeout=False): if not isinstance(config, dict): return config ai = config.get("ai") @@ -376,6 +378,10 @@ def _normalize_config_values(config): cmhub = ai.get("cmhub") if isinstance(cmhub, dict): cmhub["base_url"] = normalize_cmhub_base_url(cmhub.get("base_url", "")) + cmhub["connect_timeout"] = _normalize_cmhub_connect_timeout( + cmhub.get("connect_timeout"), + migrate_old_default=migrate_old_cmhub_connect_timeout, + ) cmhub["download_with_curl"] = _normalize_cmhub_download_with_curl( cmhub.get("download_with_curl", "auto") ) @@ -390,6 +396,18 @@ def _clamp_int(value, minimum, maximum, default): return min(int(maximum), max(int(minimum), number)) +def _normalize_cmhub_connect_timeout(value, migrate_old_default=False): + try: + number = int(value) + except (TypeError, ValueError): + number = CMHUB_CONNECT_TIMEOUT_DEFAULT + if number <= 0: + number = CMHUB_CONNECT_TIMEOUT_DEFAULT + if migrate_old_default and number == CMHUB_CONNECT_TIMEOUT_OLD_DEFAULT: + return CMHUB_CONNECT_TIMEOUT_DEFAULT + return number + + def _normalize_cmhub_download_with_curl(value): if isinstance(value, bool): return "true" if value else "false" @@ -560,7 +578,7 @@ def load_config(path=CONFIG_PATH) -> dict: except json.JSONDecodeError as exc: raise ConfigError(f"配置文件不是有效 JSON: {path}") from exc normalized = _deep_merge(DEFAULT_CONFIG, loaded) - _normalize_config_values(normalized) + _normalize_config_values(normalized, migrate_old_cmhub_connect_timeout=True) _assert_no_secrets(normalized) return _with_runtime_paths(normalized, path) @@ -633,7 +651,10 @@ def cmhub_config(config=None) -> dict: merged["base_url"] = normalize_cmhub_base_url(merged.get("base_url", "")) merged["title_alias"] = str(merged.get("title_alias", "") or "").strip() merged["image_alias"] = str(merged.get("image_alias", "") or "").strip() - merged["connect_timeout"] = int(merged.get("connect_timeout", 10) or 10) + merged["connect_timeout"] = _normalize_cmhub_connect_timeout( + merged.get("connect_timeout"), + migrate_old_default=False, + ) merged["download_with_curl"] = _normalize_cmhub_download_with_curl( merged.get("download_with_curl", "auto") ) diff --git a/app/gui/tabs/settings.py b/app/gui/tabs/settings.py index 92e0d9b..82096b4 100644 --- a/app/gui/tabs/settings.py +++ b/app/gui/tabs/settings.py @@ -88,7 +88,7 @@ class SettingsTab(QWidget): self.cmhub_connect_timeout_spin = QSpinBox() self.cmhub_connect_timeout_spin.setObjectName("cmhubConnectTimeoutSpin") self.cmhub_connect_timeout_spin.setRange(1, 3600) - self.cmhub_connect_timeout_spin.setValue(10) + self.cmhub_connect_timeout_spin.setValue(appconfig.CMHUB_CONNECT_TIMEOUT_DEFAULT) self.cmhub_check_balance_checkbox = QCheckBox("批量生成前检查余额") self.cmhub_check_balance_checkbox.setObjectName("cmhubCheckBalanceCheckbox") self.cmhub_refresh_button = QPushButton("刷新别名") @@ -825,7 +825,16 @@ class SettingsTab(QWidget): self._loaded_cmhub_api_key = appconfig.get_cmhub_api_key(path=self.cmhub_config_path) self.cmhub_api_key_edit.setText(self._loaded_cmhub_api_key) self.cmhub_connect_timeout_spin.setValue( - max(1, int(cmhub_cfg.get("connect_timeout", 10) or 10)) + max( + 1, + int( + cmhub_cfg.get( + "connect_timeout", + appconfig.CMHUB_CONNECT_TIMEOUT_DEFAULT, + ) + or appconfig.CMHUB_CONNECT_TIMEOUT_DEFAULT + ), + ) ) self.cmhub_check_balance_checkbox.setChecked( bool(cmhub_cfg.get("check_balance_before_batch", False)) diff --git a/docs/tasks/T-553.md b/docs/tasks/T-553.md index 9da7fca..b288f65 100644 --- a/docs/tasks/T-553.md +++ b/docs/tasks/T-553.md @@ -3,7 +3,7 @@ id: T-553 title: cmhub 生图超时口径调优(连接 66 秒,读取 900 秒) phase: 7 deps: [T-535, T-545, T-547, T-548] -status: TODO +status: DONE created: 2026-07-08 --- @@ -50,3 +50,12 @@ cmhub 项目侧建议:桌面端连接超时提高到 60/120 秒量级;生图 ## 边界(不改什么) 只改 cmhub 连接超时默认值、生图读取等待常量、⑤设置页显示文案、相关单测和文档;不改 cmhub API 协议、Base URL/Key/别名配置结构、标题读取等待 600 秒、图片并发上限 5、下载线程池语义、DB、Excel、Shopee/CDP 更新流程,也不增加生图读超时重试。 + +## 执行记录 + +- 2026-07-08:完成 T-553。 +- 代码:`app/appconfig.py` 新增 `CMHUB_CONNECT_TIMEOUT_DEFAULT=66` 和旧默认 `10` 的读取迁移逻辑;读取旧 `data/config.json` 时缺失或旧默认 `10` 会归一为 `66`,但用户后续显式保存 `10` 仍会保留,避免堵死高级调试。 +- 代码:`app/ai.py` 将 `CMHUB_IMAGE_READ_TIMEOUT_SECONDS` 从 `650` 改为 `900`,生图请求、requests 下载与 curl `--max-time` 继续共用该常量;生图读超时仍不自动重发。 +- 代码:`app/gui/tabs/settings.py` 的 cmhub 连接超时默认值改为 `66`,⑤设置页「返回超时」通过常量展示为「标题 600 秒 / 图片 900 秒」。 +- 测试:`tests/test_appconfig.py` 覆盖新默认 66、旧默认 10 读取迁移、显式保存 10 保留;`tests/test_ai.py` 覆盖生图请求/下载/curl/read timeout 的 900 秒口径;`tests/test_gui.py` 更新设置页展示断言。 +- 验证:`python -m ruff check app tests main.py` 通过;`py -3.10 -m compileall app main.py` 通过;`git diff --check` 通过;`py -3.10 -m unittest tests.test_appconfig tests.test_ai tests.test_gui` 通过(160 tests);`py -3.10 -m unittest discover -s tests` 通过(257 tests)。 diff --git a/tests/test_ai.py b/tests/test_ai.py index 0eb60f4..d00dc8c 100644 --- a/tests/test_ai.py +++ b/tests/test_ai.py @@ -490,9 +490,9 @@ class AITests(TempDirMixin, unittest.TestCase): self.assertEqual("1K", payload["resolution"]) self.assertEqual("1:1", payload["aspect_ratio"]) self.assertTrue(payload["image_base64"].startswith("data:image/jpeg;base64,")) - self.assertEqual((3, 650), calls[0][2]["timeout"]) + self.assertEqual((3, ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS), calls[0][2]["timeout"]) self.assertEqual("https://cdn.example.com/generated.png", downloads[0][0]) - self.assertEqual((3, 650), downloads[0][1]["timeout"]) + self.assertEqual((3, ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS), downloads[0][1]["timeout"]) self.assertEqual(91, events[0]["metadata"]["points_balance"]) timed_steps = [ event for event in steps @@ -668,7 +668,7 @@ class AITests(TempDirMixin, unittest.TestCase): image_bytes, elapsed = ai._download_cmhub_image_with_retry( "https://cdn.example.com/generated.png", connect_timeout=3, - read_timeout=650, + read_timeout=ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS, on_step=steps.append, slow_threshold=0, ) @@ -716,7 +716,10 @@ class AITests(TempDirMixin, unittest.TestCase): self.assertIn("--connect-timeout", args) self.assertEqual("3", args[args.index("--connect-timeout") + 1]) self.assertIn("--max-time", args) - self.assertEqual("650", args[args.index("--max-time") + 1]) + self.assertEqual( + str(ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS), + args[args.index("--max-time") + 1], + ) self.assertIn("--max-filesize", args) self.assertEqual(str(ai.CMHUB_IMAGE_MAX_BYTES), args[args.index("--max-filesize") + 1]) self.assertIn("--noproxy", args) @@ -733,7 +736,7 @@ class AITests(TempDirMixin, unittest.TestCase): image_bytes = ai._download_cmhub_image( url, connect_timeout=3, - read_timeout=650, + read_timeout=ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS, use_system_proxy=False, download_with_curl="true", ) @@ -748,7 +751,7 @@ class AITests(TempDirMixin, unittest.TestCase): ai._download_cmhub_image( "http://127.0.0.1/a.png", connect_timeout=3, - read_timeout=650, + read_timeout=ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS, download_with_curl="true", ) run.assert_not_called() @@ -772,7 +775,7 @@ class AITests(TempDirMixin, unittest.TestCase): image_bytes = ai._download_cmhub_image( "https://cdn.example.com/generated.png", connect_timeout=3, - read_timeout=650, + read_timeout=ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS, download_with_curl="true", ) @@ -797,7 +800,7 @@ class AITests(TempDirMixin, unittest.TestCase): image_bytes = ai._download_cmhub_image( "https://cdn.example.com/generated.png", connect_timeout=3, - read_timeout=650, + read_timeout=ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS, download_with_curl="auto", ) @@ -821,7 +824,7 @@ class AITests(TempDirMixin, unittest.TestCase): image_bytes = ai._download_cmhub_image( "https://cdn.example.com/generated.png", connect_timeout=3, - read_timeout=650, + read_timeout=ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS, download_with_curl="auto", ) @@ -886,7 +889,7 @@ class AITests(TempDirMixin, unittest.TestCase): self.assertEqual("read_timeout", raised.exception.code) self.assertEqual(1, len(calls)) - self.assertEqual((3, 650), calls[0][2]["timeout"]) + self.assertEqual((3, ai.CMHUB_IMAGE_READ_TIMEOUT_SECONDS), calls[0][2]["timeout"]) self.assert_removed(temp_dir) diff --git a/tests/test_appconfig.py b/tests/test_appconfig.py index 5cc1a85..0233200 100644 --- a/tests/test_appconfig.py +++ b/tests/test_appconfig.py @@ -227,6 +227,10 @@ class AppConfigTests(TempDirMixin, unittest.TestCase): self.assertEqual("cmhub", ai["backend"]) self.assertEqual("cmhub", appconfig.ai_backend(config)) self.assertEqual("", appconfig.cmhub_config(config)["base_url"]) + self.assertEqual( + appconfig.CMHUB_CONNECT_TIMEOUT_DEFAULT, + appconfig.cmhub_config(config)["connect_timeout"], + ) self.assertFalse(os.path.exists(cmhub_path)) self.assertEqual({"api_key": ""}, appconfig.load_cmhub_config(cmhub_path)) @@ -244,6 +248,20 @@ class AppConfigTests(TempDirMixin, unittest.TestCase): self.assertEqual("cmhub", appconfig.ai_config(migrated)["backend"]) self.assertEqual(180, appconfig.response_timeout(migrated)) + with open(config_path, "w", encoding="utf-8") as fh: + json.dump({"ai": {"cmhub": {"connect_timeout": 10}}}, fh) + migrated = appconfig.load_config(config_path) + self.assertEqual( + appconfig.CMHUB_CONNECT_TIMEOUT_DEFAULT, + appconfig.cmhub_config(migrated)["connect_timeout"], + ) + + saved = appconfig.save_config( + {"ai": {"cmhub": {"connect_timeout": 10}}}, + path=config_path, + ) + self.assertEqual(10, appconfig.cmhub_config(saved)["connect_timeout"]) + direct_cfg = appconfig.default_config() direct_cfg["ai"]["backend"] = "direct" self.assertEqual("direct", appconfig.ai_backend(direct_cfg)) diff --git a/tests/test_gui.py b/tests/test_gui.py index 901bf7d..b9218cb 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -930,7 +930,7 @@ class GuiTests(TempDirMixin, unittest.TestCase): tab.image_concurrency_spin.setValue(2) tab.retry_spin.setValue(1) tab.resolution_combo.setCurrentIndex(tab.resolution_combo.findData("2k")) - self.assertEqual("标题 600 秒 / 图片 650 秒", tab.response_timeout_label.text()) + self.assertEqual("标题 600 秒 / 图片 900 秒", tab.response_timeout_label.text()) tab.jpg_quality_spin.setValue(86) tab.chrome_path_edit.setText("D:\\Chrome\\chrome.exe") tab.default_debug_port_spin.setValue(9300)