feat: 完成T-503敏感信息提示与脱敏

- 保存或变更账号密码、AI API Key 前弹出本地明文保存提示

- 新增敏感值打码、结构化日志脱敏和自由文本替换工具

- 补充 GUI/appconfig 单测,覆盖明文提示和脱敏边界

- 同步任务看板、当前状态、架构、API、路由和编码规则文档
This commit is contained in:
chengma
2026-06-29 10:05:16 +08:00
parent 82d390c25a
commit 01e319cad8
13 changed files with 250 additions and 40 deletions
+24
View File
@@ -113,6 +113,30 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_sanitize_for_log_masks_secret_fields(self):
payload = {
"name": "demo",
"api_key": "sk-1234567890",
"nested": {
"password": "account-secret",
"items": [
{"provider_token": "token-secret"},
{"value": "safe"},
{"api_key": {"value": "nested-secret"}},
],
},
}
sanitized = appconfig.sanitize_for_log(payload)
self.assertEqual("demo", sanitized["name"])
self.assertEqual("sk-1***7890", sanitized["api_key"])
self.assertEqual("acco***cret", sanitized["nested"]["password"])
self.assertEqual("toke***cret", sanitized["nested"]["items"][0]["provider_token"])
self.assertEqual("safe", sanitized["nested"]["items"][1]["value"])
self.assertEqual("***", sanitized["nested"]["items"][2]["api_key"])
self.assertEqual("sk-1234567890", payload["api_key"])
if __name__ == "__main__":
unittest.main()
+75 -1
View File
@@ -179,7 +179,12 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab.api_key_edit.setText("sk-custom-secret")
tab.api_type_combo.setCurrentIndex(tab.api_type_combo.findData("chat"))
tab.connect_timeout_spin.setValue(12)
tab.save_model()
with mock.patch("app.gui.QMessageBox.warning") as warning:
tab.save_model()
warning.assert_called_once()
self.assertIn("本地明文保存", warning.call_args[0][1])
self.assertIn("config/ai_models.json", warning.call_args[0][2])
saved = appconfig.get_model("Text Custom", path=models_path)
self.assertEqual("text", saved["category"])
@@ -187,6 +192,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("sk-custom-secret", saved["api_key"])
self.assertEqual(12, saved["connect_timeout_seconds"])
self.assertIn("AI 模型已保存:Text Custom", statuses[-1])
self.assertNotIn("sk-custom-secret", statuses[-1])
self.assertTrue(tab.delete_model_button.isEnabled())
with mock.patch("app.gui.QMessageBox.question", return_value=gui.QMessageBox.Yes):
@@ -444,6 +450,29 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_ai_model_test_worker_sanitizes_secret_payload_fields(self):
with self.make_temp_dir() as temp_dir:
models_path = os.path.join(temp_dir, "ai_models.json")
worker = AIModelTestWorker("Text A", ai_models_path=models_path)
with mock.patch(
"app.gui.appconfig.test_ai_model",
return_value={
"ok": False,
"api_key": "sk-worker-secret",
"password": "worker-password",
"error": "连接失败",
},
):
result = worker.execute()
self.assertEqual("Text A", result["name"])
self.assertEqual("sk-w***cret", result["api_key"])
self.assertEqual("work***word", result["password"])
self.assertEqual("连接失败", result["error"])
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")
@@ -1343,6 +1372,51 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_accounts_tab_warns_before_saving_plaintext_password(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
statuses = []
tab = AccountsTab(config=cfg, status_callback=statuses.append)
self.addCleanup(tab.close)
class FakeDialog:
def __init__(self, *args, **kwargs):
pass
def exec(self):
return gui.QDialog.Accepted
def values(self):
return {
"account_name": "主店",
"alias": "alias",
"region_host": "seller.shopee.tw",
"debug_port": 9222,
"password": "plain-password",
"note": "",
}
with mock.patch("app.gui.AccountDialog", FakeDialog), mock.patch(
"app.gui.QMessageBox.warning"
) as warning:
tab.add_account()
warning.assert_called_once()
self.assertIn("本地明文保存", warning.call_args[0][1])
self.assertIn("SQLite", warning.call_args[0][2])
stored = accounts.get_account("alias", config=cfg)
self.assertEqual("plain-password", stored.password)
visible_values = [
tab.table.item(0, column).text()
for column in range(tab.table.columnCount())
if tab.table.item(0, column) is not None
]
self.assertNotIn("plain-password", visible_values)
self.assertIn("账号已新增", statuses[-1])
self.assertNotIn("plain-password", statuses[-1])
self.assert_removed(temp_dir)
def test_accounts_tab_updates_login_status_cell(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)