feat: add cmhub settings panel

This commit is contained in:
chengma
2026-07-04 16:29:15 +08:00
parent 1efb095767
commit 27f23a4738
9 changed files with 813 additions and 21 deletions
+227
View File
@@ -25,6 +25,7 @@ from app.gui import (
AccountLoginCheckWorker,
AccountsTab,
AIModelTestWorker,
CMHubSettingsWorker,
ApplyTab,
ApplyWorker,
CollectWorker,
@@ -705,6 +706,232 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_cmhub_settings_worker_fetches_models_and_balance(self):
worker = CMHubSettingsWorker(
"https://cmhub.example.com",
"sk-cmhub-secret",
connect_timeout=7,
include_balance=True,
)
models = [
{
"alias": "title-standard",
"operation_type": "title",
"pricing_status": "priced",
}
]
balance = {"user": {"id": "u1"}, "points_balance": 88}
with mock.patch("app.gui.ai.fetch_cmhub_models", return_value=models) as fetch_models, \
mock.patch("app.gui.ai.fetch_cmhub_balance", return_value=balance) as fetch_balance:
result = worker.execute()
fetch_models.assert_called_once_with(
"https://cmhub.example.com",
"sk-cmhub-secret",
connect_timeout=7,
)
fetch_balance.assert_called_once_with(
"https://cmhub.example.com",
"sk-cmhub-secret",
connect_timeout=7,
)
self.assertTrue(result["ok"])
self.assertEqual(models, result["models"])
self.assertEqual(88, result["points_balance"])
def test_cmhub_settings_worker_redacts_key_on_failure(self):
worker = CMHubSettingsWorker(
"https://cmhub.example.com",
"sk-cmhub-secret",
connect_timeout=7,
include_balance=False,
)
with mock.patch(
"app.gui.ai.fetch_cmhub_models",
side_effect=RuntimeError("bad key sk-cmhub-secret"),
):
with self.assertRaises(RuntimeError) as raised:
worker.execute()
self.assertIn("bad key", str(raised.exception))
self.assertNotIn("sk-cmhub-secret", str(raised.exception))
def test_settings_tab_cmhub_backend_panel_saves_config_and_key(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
cfg["ai"] = appconfig.default_config()["ai"]
cfg["ai"]["backend"] = "cmhub"
cfg["ai"]["cmhub"] = {
"base_url": "https://cmhub.old",
"title_alias": "title-old",
"image_alias": "image-old",
"connect_timeout": 9,
"check_balance_before_batch": False,
}
cfg["cmhub_config_path"] = os.path.join(temp_dir, "config", "cmhub.json")
appconfig.save_cmhub_config(
{"api_key": "sk-old-secret"},
path=cfg["cmhub_config_path"],
)
appconfig.save_ai_models_config(
{
"models": [
{
"name": "Text A",
"category": "text",
"enabled": True,
"url": "",
"model": "",
"api_key": "",
"api_type": "chat",
"connect_timeout_seconds": 30,
"timeout_seconds": 0,
"extra_body": {},
},
{
"name": "Image A",
"category": "image",
"enabled": True,
"url": "",
"model": "",
"api_key": "",
"api_type": "auto",
"connect_timeout_seconds": 30,
"timeout_seconds": 0,
"extra_body": {},
},
]
},
path=cfg["ai_models_path"],
)
tab = SettingsTab(
config=cfg,
config_path=cfg["config_path"],
ai_models_path=cfg["ai_models_path"],
)
self.addCleanup(tab.close)
self.assertEqual("cmhub", tab.backend_combo.currentData())
self.assertTrue(tab.model_picker_panel.isHidden())
self.assertFalse(tab.cmhub_panel.isHidden())
self.assertEqual("https://cmhub.old", tab.cmhub_base_url_edit.text())
self.assertEqual("sk-old-secret", tab.cmhub_api_key_edit.text())
self.assertEqual(QLineEdit.Password, tab.cmhub_api_key_edit.echoMode())
self.assertEqual("title-old", tab.cmhub_title_alias_combo.currentData())
self.assertEqual("image-old", tab.cmhub_image_alias_combo.currentData())
tab.cmhub_base_url_edit.setText("https://cmhub.example.com")
tab.cmhub_api_key_edit.setText("sk-new-secret")
tab.cmhub_connect_timeout_spin.setValue(12)
tab.cmhub_check_balance_checkbox.setChecked(True)
tab._populate_cmhub_alias_combos(
[
{
"alias": "title-standard",
"operation_type": "title",
"requires_image": False,
"pricing_status": "priced",
"prices": [{"resolution": "1K", "points_cost": 1}],
},
{
"alias": "image-standard",
"operation_type": "image",
"requires_image": True,
"pricing_status": "priced",
"prices": [{"resolution": "1K", "points_cost": 5}],
},
],
title_selected="title-standard",
image_selected="image-standard",
)
with mock.patch("app.gui.QMessageBox.warning") as warning, \
mock.patch("app.gui.QMessageBox.information") as info:
tab.save_app_settings()
warning.assert_called_once()
self.assertIn("config/cmhub.json", warning.call_args[0][2])
info.assert_called_once_with(tab, "保存设置", "设置已保存")
saved = appconfig.load_config(cfg["config_path"])
self.assertEqual("cmhub", saved["ai"]["backend"])
self.assertEqual("https://cmhub.example.com", saved["ai"]["cmhub"]["base_url"])
self.assertEqual("title-standard", saved["ai"]["cmhub"]["title_alias"])
self.assertEqual("image-standard", saved["ai"]["cmhub"]["image_alias"])
self.assertEqual(12, saved["ai"]["cmhub"]["connect_timeout"])
self.assertTrue(saved["ai"]["cmhub"]["check_balance_before_batch"])
self.assertEqual(
"sk-new-secret",
appconfig.get_cmhub_api_key(path=cfg["cmhub_config_path"]),
)
self.assertTrue(os.path.exists(cfg["ai_models_path"]))
self.assert_removed(temp_dir)
def test_settings_tab_cmhub_alias_refresh_filters_unpriced_and_keeps_saved(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
cfg["ai"] = appconfig.default_config()["ai"]
cfg["ai"]["backend"] = "cmhub"
cfg["ai"]["cmhub"] = {
"base_url": "https://cmhub.example.com",
"title_alias": "title-saved",
"image_alias": "image-saved",
"connect_timeout": 10,
"check_balance_before_batch": False,
}
cfg["cmhub_config_path"] = os.path.join(temp_dir, "config", "cmhub.json")
appconfig.save_cmhub_config({"api_key": "sk-cmhub-secret"}, path=cfg["cmhub_config_path"])
tab = SettingsTab(config=cfg, config_path=cfg["config_path"], ai_models_path=cfg["ai_models_path"])
self.addCleanup(tab.close)
tab._on_cmhub_finished(
{
"ok": True,
"points_balance": 55,
"models": [
{
"alias": "title-priced",
"operation_type": "title",
"requires_image": False,
"pricing_status": "priced",
"prices": [{"resolution": "512", "points_cost": 1}],
},
{
"alias": "title-free",
"operation_type": "title",
"pricing_status": "unpriced",
"prices": [],
},
{
"alias": "image-priced",
"operation_type": "image",
"requires_image": True,
"pricing_status": "priced",
"prices": [{"resolution": "1K", "points_cost": 5}],
},
],
}
)
title_aliases = [
tab.cmhub_title_alias_combo.itemData(index)
for index in range(tab.cmhub_title_alias_combo.count())
]
image_labels = [
tab.cmhub_image_alias_combo.itemText(index)
for index in range(tab.cmhub_image_alias_combo.count())
]
self.assertIn("title-priced", title_aliases)
self.assertIn("title-saved", title_aliases)
self.assertNotIn("title-free", title_aliases)
self.assertIn("512:1点", tab.cmhub_title_alias_combo.itemText(0))
self.assertTrue(any("需参考图" in label for label in image_labels))
self.assertIn("余额 55", tab.cmhub_result_label.text())
self.assert_removed(temp_dir)
def test_generate_tab_has_prompt_editors_and_task_table(self):
with self.make_temp_dir() as temp_dir:
title_prompt_path = os.path.join(temp_dir, "title_prompt.txt")