feat(product-suite): autosave prompt edits
This commit is contained in:
@@ -64,8 +64,72 @@ from ..workers import (
|
||||
|
||||
ORIGINAL_DOWNLOAD_CONCURRENCY = 2
|
||||
ORIGINAL_CHECK_STATE_ROLE = Qt.UserRole + 1
|
||||
PROMPT_AUTOSAVE_DELAY_MS = 500
|
||||
_PRODUCT_SUITE_THREAD_REFS = {}
|
||||
_URL_RE = re.compile(r"https?://[^\s,,;;))\]]+", re.IGNORECASE)
|
||||
|
||||
|
||||
class AutoHeightPlainTextEdit(QPlainTextEdit):
|
||||
"""A plain-text editor that delegates scrolling to its containing page."""
|
||||
|
||||
def __init__(self, parent=None, minimum_height=96):
|
||||
super().__init__(parent)
|
||||
self._minimum_content_height = max(1, int(minimum_height))
|
||||
self._height_update_pending = False
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.setFixedHeight(self._minimum_content_height)
|
||||
self.document().contentsChanged.connect(self.schedule_height_update)
|
||||
self.document().documentLayout().documentSizeChanged.connect(
|
||||
self.schedule_height_update
|
||||
)
|
||||
self.schedule_height_update()
|
||||
|
||||
def schedule_height_update(self, *args):
|
||||
if self._height_update_pending:
|
||||
return
|
||||
self._height_update_pending = True
|
||||
QTimer.singleShot(0, self.refresh_height)
|
||||
|
||||
def refresh_height(self):
|
||||
self._height_update_pending = False
|
||||
text_height = 0.0
|
||||
block = self.document().firstBlock()
|
||||
while block.isValid():
|
||||
layout = block.layout()
|
||||
line_count = max(1, layout.lineCount())
|
||||
for index in range(line_count):
|
||||
line = layout.lineAt(index)
|
||||
text_height += (
|
||||
line.height()
|
||||
if line.isValid()
|
||||
else self.fontMetrics().lineSpacing()
|
||||
)
|
||||
block = block.next()
|
||||
margins = self.contentsMargins()
|
||||
target = int(
|
||||
text_height
|
||||
+ (self.document().documentMargin() * 2)
|
||||
+ margins.top()
|
||||
+ margins.bottom()
|
||||
+ (self.frameWidth() * 2)
|
||||
+ 6
|
||||
)
|
||||
target = max(self._minimum_content_height, target)
|
||||
if self.height() != target:
|
||||
self.setFixedHeight(target)
|
||||
self.updateGeometry()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self.schedule_height_update()
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
self.schedule_height_update()
|
||||
|
||||
|
||||
def _asset_usable(asset):
|
||||
path = str(getattr(asset, "local_path", "") or "")
|
||||
return (
|
||||
@@ -618,6 +682,7 @@ class SuiteTaskState:
|
||||
project_id: int = None
|
||||
project_binding_state: str = ""
|
||||
prompt: str = ""
|
||||
last_saved_prompt: str = ""
|
||||
settings: dict = field(default_factory=product_suite.default_suite_settings)
|
||||
current_job_ids: list = field(default_factory=list)
|
||||
show_history: bool = False
|
||||
@@ -673,6 +738,7 @@ class ProductSuiteTab(QWidget):
|
||||
self._next_key = 1
|
||||
self._next_serial = 1
|
||||
self._displayed_state = None
|
||||
self._prompt_save_timers = {}
|
||||
self._original_list_context = None
|
||||
self._loading = False
|
||||
self._result_refresh_pending = False
|
||||
@@ -985,11 +1051,9 @@ class ProductSuiteTab(QWidget):
|
||||
self.prompt_settings_button.setToolTip("编辑并预览套图最终提示词")
|
||||
title_row.addWidget(self.prompt_settings_button)
|
||||
layout.addLayout(title_row)
|
||||
self.prompt_edit = QPlainTextEdit()
|
||||
self.prompt_edit = AutoHeightPlainTextEdit()
|
||||
self.prompt_edit.setObjectName("suitePromptEdit")
|
||||
self.prompt_edit.setPlaceholderText("输入产品名称、卖点、目标人群、使用场景和禁用元素")
|
||||
self.prompt_edit.setMinimumHeight(96)
|
||||
self.prompt_edit.setMaximumHeight(112)
|
||||
layout.addWidget(self.prompt_edit)
|
||||
helper = QLabel("内容越具体,生成的商品套图越稳定。AI 帮写不会阻塞其他套图任务。")
|
||||
helper.setWordWrap(True)
|
||||
@@ -1198,6 +1262,7 @@ class ProductSuiteTab(QWidget):
|
||||
project_id=int(project.id),
|
||||
project_binding_state=project.binding_state,
|
||||
prompt=str(project.draft_prompt or ""),
|
||||
last_saved_prompt=str(project.draft_prompt or ""),
|
||||
settings=product_suite.normalize_suite_settings(
|
||||
image_studio.project_suite_settings(project)
|
||||
),
|
||||
@@ -1240,6 +1305,9 @@ class ProductSuiteTab(QWidget):
|
||||
state = self._states.get(key)
|
||||
if state is None:
|
||||
return
|
||||
if state is self._displayed_state:
|
||||
self._save_controls_to_state(state)
|
||||
self._flush_prompt_save(state)
|
||||
project = self._state_project(state)
|
||||
draft_action = None
|
||||
if image_studio.is_draft_project(project) and image_studio.project_has_content(
|
||||
@@ -1278,6 +1346,7 @@ class ProductSuiteTab(QWidget):
|
||||
image_studio.discard_empty_draft_project(project.id, path=self.db_path)
|
||||
except Exception as exc:
|
||||
self._status("清理空临时草稿失败:%s" % _user_error(exc), "danger")
|
||||
self._release_prompt_save_timer(state)
|
||||
self._retired_states.append(state)
|
||||
self._states.pop(state.key, None)
|
||||
self.task_tabs.removeTab(index)
|
||||
@@ -1313,6 +1382,7 @@ class ProductSuiteTab(QWidget):
|
||||
return
|
||||
if self._displayed_state is not None:
|
||||
self._save_controls_to_state(self._displayed_state)
|
||||
self._flush_prompt_save(self._displayed_state)
|
||||
state = self._state_for_index(index)
|
||||
self._displayed_state = state
|
||||
if state is not None:
|
||||
@@ -1433,6 +1503,7 @@ class ProductSuiteTab(QWidget):
|
||||
self._loading = False
|
||||
self._update_context_actions(state)
|
||||
return
|
||||
self._flush_prompt_save(state)
|
||||
try:
|
||||
project = image_studio.bind_draft_project(
|
||||
state.project_id,
|
||||
@@ -1484,8 +1555,10 @@ class ProductSuiteTab(QWidget):
|
||||
self._update_context_actions(state)
|
||||
|
||||
def _clear_project_binding(self, state):
|
||||
self._flush_prompt_save(state)
|
||||
state.project_id = None
|
||||
state.project_binding_state = ""
|
||||
state.last_saved_prompt = ""
|
||||
state.current_job_ids = []
|
||||
state.done = state.failed = state.total = 0
|
||||
state.started_at = None
|
||||
@@ -1547,6 +1620,7 @@ class ProductSuiteTab(QWidget):
|
||||
if state is None:
|
||||
return None
|
||||
if state.project_id is not None:
|
||||
self._flush_prompt_save(state)
|
||||
return self._state_project(state)
|
||||
if state.item_id:
|
||||
return self._bind_project(state)
|
||||
@@ -1564,6 +1638,7 @@ class ProductSuiteTab(QWidget):
|
||||
project = image_studio.create_or_get_project(
|
||||
account,
|
||||
item_id=state.item_id,
|
||||
draft_prompt=state.prompt,
|
||||
path=self.db_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
@@ -1571,13 +1646,17 @@ class ProductSuiteTab(QWidget):
|
||||
return None
|
||||
state.project_id = int(project.id)
|
||||
state.project_binding_state = project.binding_state
|
||||
stored_prompt = str(project.draft_prompt or "")
|
||||
if load_existing and previous_id != state.project_id:
|
||||
state.prompt = str(project.draft_prompt or "")
|
||||
state.prompt = stored_prompt
|
||||
state.settings = product_suite.normalize_suite_settings(
|
||||
image_studio.project_suite_settings(project)
|
||||
)
|
||||
if state is self._displayed_state:
|
||||
self._load_state(state)
|
||||
state.last_saved_prompt = stored_prompt
|
||||
if state.prompt != stored_prompt:
|
||||
self._flush_prompt_save(state)
|
||||
return project
|
||||
|
||||
def _create_draft_project(self, state):
|
||||
@@ -1596,6 +1675,7 @@ class ProductSuiteTab(QWidget):
|
||||
state.project_id = int(project.id)
|
||||
state.project_binding_state = project.binding_state
|
||||
state.item_id = ""
|
||||
state.last_saved_prompt = str(project.draft_prompt or "")
|
||||
self._persist_state(state)
|
||||
self._set_task_title(state)
|
||||
if state is self._displayed_state:
|
||||
@@ -1603,15 +1683,75 @@ class ProductSuiteTab(QWidget):
|
||||
self._status("已创建临时草稿,可继续添加本地图片", "info")
|
||||
return project
|
||||
|
||||
def _persist_state(self, state):
|
||||
if state.project_id is None:
|
||||
def _prompt_save_timer(self, state):
|
||||
timer = self._prompt_save_timers.get(state.key)
|
||||
if timer is None:
|
||||
timer = QTimer(self)
|
||||
timer.setSingleShot(True)
|
||||
timer.setInterval(PROMPT_AUTOSAVE_DELAY_MS)
|
||||
timer.timeout.connect(
|
||||
lambda key=state.key: self._on_prompt_autosave_timeout(key)
|
||||
)
|
||||
self._prompt_save_timers[state.key] = timer
|
||||
return timer
|
||||
|
||||
def _schedule_prompt_save(self, state):
|
||||
if state is None or state.project_id is None:
|
||||
return
|
||||
if state.prompt == state.last_saved_prompt:
|
||||
self._cancel_prompt_save(state)
|
||||
return
|
||||
self._prompt_save_timer(state).start()
|
||||
|
||||
def _cancel_prompt_save(self, state):
|
||||
timer = self._prompt_save_timers.get(state.key) if state is not None else None
|
||||
if timer is not None:
|
||||
timer.stop()
|
||||
|
||||
def _release_prompt_save_timer(self, state):
|
||||
timer = (
|
||||
self._prompt_save_timers.pop(state.key, None)
|
||||
if state is not None
|
||||
else None
|
||||
)
|
||||
if timer is not None:
|
||||
timer.stop()
|
||||
timer.deleteLater()
|
||||
|
||||
def _on_prompt_autosave_timeout(self, state_key):
|
||||
state = self._states.get(state_key)
|
||||
if state is not None:
|
||||
self._persist_prompt(state)
|
||||
|
||||
def _persist_prompt(self, state):
|
||||
if state is None or state.project_id is None:
|
||||
return True
|
||||
prompt = str(state.prompt or "")
|
||||
if prompt == state.last_saved_prompt:
|
||||
return True
|
||||
try:
|
||||
image_studio.update_project_prompt(
|
||||
project = image_studio.update_project_prompt(
|
||||
state.project_id,
|
||||
state.prompt,
|
||||
prompt,
|
||||
path=self.db_path,
|
||||
)
|
||||
if project is None:
|
||||
raise image_studio.ImageStudioError("商品套图项目不存在")
|
||||
except Exception as exc:
|
||||
self._status("商品卖点自动保存失败:%s" % _user_error(exc), "danger")
|
||||
return False
|
||||
state.last_saved_prompt = prompt
|
||||
return True
|
||||
|
||||
def _flush_prompt_save(self, state):
|
||||
self._cancel_prompt_save(state)
|
||||
return self._persist_prompt(state)
|
||||
|
||||
def _persist_state(self, state):
|
||||
if state.project_id is None:
|
||||
return True
|
||||
prompt_saved = self._flush_prompt_save(state)
|
||||
try:
|
||||
image_studio.update_project_suite_settings(
|
||||
state.project_id,
|
||||
state.settings,
|
||||
@@ -1619,6 +1759,8 @@ class ProductSuiteTab(QWidget):
|
||||
)
|
||||
except Exception as exc:
|
||||
self._status("商品套图设置保存失败:%s" % _user_error(exc), "danger")
|
||||
return False
|
||||
return prompt_saved
|
||||
|
||||
def _on_settings_changed(self, value=None):
|
||||
if self._loading or self._displayed_state is None:
|
||||
@@ -1655,6 +1797,7 @@ class ProductSuiteTab(QWidget):
|
||||
return
|
||||
state = self._displayed_state
|
||||
state.prompt = self.prompt_edit.toPlainText()
|
||||
self._schedule_prompt_save(state)
|
||||
|
||||
def open_prompt_settings(self, checked=False):
|
||||
state = self._displayed_state
|
||||
@@ -2754,10 +2897,16 @@ class ProductSuiteTab(QWidget):
|
||||
self._refresh_results(self._displayed_state)
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self._displayed_state is not None:
|
||||
self._save_controls_to_state(self._displayed_state)
|
||||
for state in list(self._states.values()):
|
||||
self._flush_prompt_save(state)
|
||||
for state in list(self._states.values()) + list(self._retired_states):
|
||||
for worker in (state.worker, state.pull_worker, state.import_worker, state.ai_worker):
|
||||
if worker is not None and hasattr(worker, "cancel"):
|
||||
worker.cancel()
|
||||
for worker, thread in list(state.downloads.values()):
|
||||
worker.cancel()
|
||||
for state in list(self._states.values()) + list(self._retired_states):
|
||||
self._release_prompt_save_timer(state)
|
||||
super().closeEvent(event)
|
||||
|
||||
+2
-1
@@ -198,7 +198,8 @@
|
||||
|
||||
- 每个顶部任务标签持有独立账号、商品ID、设置、原图、当前 job 集合和 worker;任务可并行生成。切换任务不停止后台操作;关闭运行中任务先确认并协作式取消,线程引用保留到真正结束,避免 `QThread: Destroyed while thread is still running`。
|
||||
- 二级套图任务标签使用独立紧凑样式,不继承主模块 Tab 的大尺寸点击区。上下文栏左侧集中「历史生成 / 打开结果文件夹 / 添加图片」,右侧集中账号、商品 ID 和拉取入口;常见 11~13 位商品 ID 不得裁切,长账号可通过 tooltip 查看完整名称。
|
||||
- 项目仍以 `账号别名 + 商品ID` 唯一,复用 `image_studio_projects/assets/jobs`。`suite_settings_json` 保存平台、国家地区、语言、比例、逐图主图模式和分类数量;卖点文本继续使用 `draft_prompt`。
|
||||
- 项目仍以 `账号别名 + 商品ID` 唯一,复用 `image_studio_projects/assets/jobs`。`suite_settings_json` 保存平台、国家地区、语言、比例、逐图主图模式和分类数量;卖点文本继续使用 `draft_prompt`,用户停止输入约 500ms 后自动保存,切换任务、关闭任务或程序前同步补保存。未创建项目时只保留在当前任务内存,不因输入文字自动创建临时草稿。
|
||||
- 「商品卖点与要求」关闭内部横向和纵向滚动条,输入框按完整换行内容自然增高;文本删除后可缩回最低高度,页面过长时统一由左侧 `suiteConfigScroll` 滚动。
|
||||
- 商品原图最多16张。前6个槽位固定显示主图与参考1~5;列表关闭内部滚动条,按可用宽度换行并自然向下展开,由左侧配置区统一滚动。支持文件选择、外部拖入、剪贴板粘贴和列表内排序。历史失效远程图不占有效名额;第1张是主参考图。
|
||||
- 每张真实原图左上角提供独立勾选框,标题行显示「已选 N 张 / 全选 / 反选」;添加占位图不参与选择。勾选只在当前任务界面内临时保留,普通刷新和排序按资产 ID 保留,切换任务或删除成功后清空。右键或 Delete 可批量移除,确认框说明准确数量、主图变化及非破坏性边界;生成中或勾选项仍在下载时整批阻断。移除只删除当前项目的本地资产记录,不删除本地源文件或蝦皮线上图片。
|
||||
- 「拉取蝦皮主图」复用只读 CDP,读取 URL 后由最多2个下载 worker 后台落盘;不改标题/封面、不拖拽、不点击更新。拉取、下载期间其余界面和其他任务仍可操作。
|
||||
|
||||
+6
-1
@@ -3,7 +3,7 @@ id: T-638
|
||||
title: 商品套图卖点输入自动保存与自适应高度
|
||||
phase: 7
|
||||
deps: [T-637]
|
||||
status: TODO
|
||||
status: DONE
|
||||
created: 2026-07-16
|
||||
---
|
||||
|
||||
@@ -105,3 +105,8 @@ git diff --check
|
||||
- 不修改 CDP、蝦皮主图拉取、①导入采集、②AI生成、③更新蝦皮、④账号管理或⑤设置。
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 2026-07-16:⑥商品套图为每个 `SuiteTaskState` 增加独立的 500ms 单次保存定时器和最近成功保存快照;连续输入只写最终内容,定时回调按 `state.key` 找回原任务,不依赖届时显示中的任务。切换任务、关闭任务、关闭程序、切换项目、创建项目和开始本地工作前会停止待执行定时并同步补保存。
|
||||
- 2026-07-16:未创建项目时卖点只保存在当前任务内存,不因输入文字创建临时草稿;创建正式项目或临时草稿时把当前卖点写入 `draft_prompt`。AI 帮写结果继续通过统一项目持久化入口立即保存,失败时保留内存文本并显示中文 danger 状态。
|
||||
- 2026-07-16:新增 `AutoHeightPlainTextEdit`,关闭内部横向和纵向滚动条,按每个实际换行的文本行高度动态增高;空内容保持 96px,删除内容可缩回,宽度变化时重新计算,由现有 `suiteConfigScroll` 统一承载页面滚动。同步更新 `docs/routes.md`。
|
||||
- 2026-07-16:`tests/test_product_suite_gui.py` 新增 6 项场景测试,覆盖防抖去重、任务切换/关闭不串写、无项目不建草稿、保存失败、AI 帮写保存和自适应高度。定向 25 项通过;隔离工作树全量 524 项 unittest、Ruff、compileall、`git diff --check` 全部通过。当前主工作区全量测试仅有 3 项原有封面默认模板 `papa1` 改名导致的失败,该未提交用户改动未纳入本任务。
|
||||
|
||||
@@ -20,6 +20,7 @@ from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import QApplication, QLabel, QListWidgetItem, QPushButton
|
||||
|
||||
from app.gui.tabs.product_suite import (
|
||||
AutoHeightPlainTextEdit,
|
||||
ORIGINAL_CHECK_STATE_ROLE,
|
||||
ProductOriginalDelegate,
|
||||
ProductOriginalList,
|
||||
@@ -555,6 +556,273 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_prompt_autosaves_after_debounce_and_ignores_unchanged_text(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
config = self._config(temp_dir)
|
||||
account = accounts.create_account(
|
||||
"主店",
|
||||
"alias-a",
|
||||
debug_port=9222,
|
||||
config=config,
|
||||
)
|
||||
project = image_studio.create_or_get_project(
|
||||
account,
|
||||
item_id="51100639510",
|
||||
draft_prompt="原卖点",
|
||||
path=config["db_path"],
|
||||
)
|
||||
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
||||
self.addCleanup(tab.close)
|
||||
state = tab._displayed_state
|
||||
state.account_alias = "alias-a"
|
||||
state.item_id = project.item_id
|
||||
state.project_id = project.id
|
||||
state.project_binding_state = project.binding_state
|
||||
state.prompt = project.draft_prompt
|
||||
state.last_saved_prompt = project.draft_prompt
|
||||
tab._load_state(state)
|
||||
|
||||
original_update = image_studio.update_project_prompt
|
||||
with mock.patch(
|
||||
"app.gui.tabs.product_suite.image_studio.update_project_prompt",
|
||||
wraps=original_update,
|
||||
) as update_prompt:
|
||||
tab.prompt_edit.setPlainText("第一版卖点")
|
||||
tab.prompt_edit.setPlainText("最终卖点")
|
||||
QTest.qWait(650)
|
||||
self.app.processEvents()
|
||||
|
||||
stored = image_studio.get_project(project.id, path=config["db_path"])
|
||||
self.assertEqual("最终卖点", stored.draft_prompt)
|
||||
self.assertEqual(1, update_prompt.call_count)
|
||||
|
||||
QTest.qWait(600)
|
||||
self.app.processEvents()
|
||||
self.assertEqual(1, update_prompt.call_count)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_prompt_switch_and_close_flush_to_the_correct_projects(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
config = self._config(temp_dir)
|
||||
account = accounts.create_account(
|
||||
"主店",
|
||||
"alias-a",
|
||||
debug_port=9222,
|
||||
config=config,
|
||||
)
|
||||
first_project = image_studio.create_or_get_project(
|
||||
account,
|
||||
item_id="51100639510",
|
||||
path=config["db_path"],
|
||||
)
|
||||
second_project = image_studio.create_or_get_project(
|
||||
account,
|
||||
item_id="51100639511",
|
||||
path=config["db_path"],
|
||||
)
|
||||
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
||||
first_state = tab._displayed_state
|
||||
first_state.account_alias = "alias-a"
|
||||
first_state.item_id = first_project.item_id
|
||||
first_state.project_id = first_project.id
|
||||
first_state.project_binding_state = first_project.binding_state
|
||||
tab._load_state(first_state)
|
||||
tab.prompt_edit.setPlainText("商品一卖点")
|
||||
|
||||
second_state = tab.add_task(inherit=False)
|
||||
second_state.account_alias = "alias-a"
|
||||
second_state.item_id = second_project.item_id
|
||||
second_state.project_id = second_project.id
|
||||
second_state.project_binding_state = second_project.binding_state
|
||||
tab._load_state(second_state)
|
||||
tab.prompt_edit.setPlainText("商品二卖点")
|
||||
tab.task_tabs.setCurrentIndex(0)
|
||||
self.app.processEvents()
|
||||
|
||||
self.assertEqual(
|
||||
"商品一卖点",
|
||||
image_studio.get_project(
|
||||
first_project.id,
|
||||
path=config["db_path"],
|
||||
).draft_prompt,
|
||||
)
|
||||
self.assertEqual(
|
||||
"商品二卖点",
|
||||
image_studio.get_project(
|
||||
second_project.id,
|
||||
path=config["db_path"],
|
||||
).draft_prompt,
|
||||
)
|
||||
|
||||
tab.prompt_edit.setPlainText("商品一关闭任务前卖点")
|
||||
tab.close_task(0)
|
||||
self.app.processEvents()
|
||||
self.assertEqual(
|
||||
"商品一关闭任务前卖点",
|
||||
image_studio.get_project(
|
||||
first_project.id,
|
||||
path=config["db_path"],
|
||||
).draft_prompt,
|
||||
)
|
||||
|
||||
self.assertIs(second_state, tab._displayed_state)
|
||||
tab.prompt_edit.setPlainText("商品二关闭程序前卖点")
|
||||
tab.close()
|
||||
self.app.processEvents()
|
||||
self.assertEqual(
|
||||
"商品二关闭程序前卖点",
|
||||
image_studio.get_project(
|
||||
second_project.id,
|
||||
path=config["db_path"],
|
||||
).draft_prompt,
|
||||
)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_prompt_without_project_stays_in_memory_until_draft_creation(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
config = self._config(temp_dir)
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
|
||||
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
||||
self.addCleanup(tab.close)
|
||||
state = tab._displayed_state
|
||||
|
||||
tab.prompt_edit.setPlainText("尚未建立项目的卖点")
|
||||
QTest.qWait(650)
|
||||
self.app.processEvents()
|
||||
|
||||
self.assertEqual([], image_studio.list_projects(path=config["db_path"]))
|
||||
draft = tab._create_draft_project(state)
|
||||
self.assertIsNotNone(draft)
|
||||
stored = image_studio.get_project(draft.id, path=config["db_path"])
|
||||
self.assertEqual("尚未建立项目的卖点", stored.draft_prompt)
|
||||
self.assertEqual("尚未建立项目的卖点", state.last_saved_prompt)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_prompt_autosave_failure_keeps_memory_text_and_reports_chinese_error(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
config = self._config(temp_dir)
|
||||
account = accounts.create_account(
|
||||
"主店",
|
||||
"alias-a",
|
||||
debug_port=9222,
|
||||
config=config,
|
||||
)
|
||||
project = image_studio.create_or_get_project(
|
||||
account,
|
||||
item_id="51100639510",
|
||||
path=config["db_path"],
|
||||
)
|
||||
status = mock.Mock()
|
||||
tab = ProductSuiteTab(
|
||||
config=config,
|
||||
db_path=config["db_path"],
|
||||
status_callback=status,
|
||||
)
|
||||
self.addCleanup(tab.close)
|
||||
state = tab._displayed_state
|
||||
state.account_alias = "alias-a"
|
||||
state.item_id = project.item_id
|
||||
state.project_id = project.id
|
||||
state.project_binding_state = project.binding_state
|
||||
tab._load_state(state)
|
||||
|
||||
with mock.patch(
|
||||
"app.gui.tabs.product_suite.image_studio.update_project_prompt",
|
||||
side_effect=OSError("disk unavailable"),
|
||||
):
|
||||
tab.prompt_edit.setPlainText("保存失败仍保留")
|
||||
QTest.qWait(650)
|
||||
self.app.processEvents()
|
||||
|
||||
self.assertEqual("保存失败仍保留", state.prompt)
|
||||
self.assertEqual("", state.last_saved_prompt)
|
||||
self.assertTrue(
|
||||
any(
|
||||
"商品卖点自动保存失败" in str(call.args[0])
|
||||
for call in status.call_args_list
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
any(call.kwargs.get("level") == "danger" for call in status.call_args_list)
|
||||
)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_ai_write_result_saves_prompt_without_an_extra_user_action(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
config = self._config(temp_dir)
|
||||
account = accounts.create_account(
|
||||
"主店",
|
||||
"alias-a",
|
||||
debug_port=9222,
|
||||
config=config,
|
||||
)
|
||||
project = image_studio.create_or_get_project(
|
||||
account,
|
||||
item_id="51100639510",
|
||||
draft_prompt="原卖点",
|
||||
path=config["db_path"],
|
||||
)
|
||||
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
|
||||
self.addCleanup(tab.close)
|
||||
state = tab._displayed_state
|
||||
state.account_alias = "alias-a"
|
||||
state.item_id = project.item_id
|
||||
state.project_id = project.id
|
||||
state.project_binding_state = project.binding_state
|
||||
state.prompt = project.draft_prompt
|
||||
state.last_saved_prompt = project.draft_prompt
|
||||
state.ai_prompt_snapshot = project.draft_prompt
|
||||
tab._load_state(state)
|
||||
|
||||
tab._on_ai_write_finished(
|
||||
state,
|
||||
{"ok": True, "cancelled": False, "text": "AI生成的新卖点"},
|
||||
)
|
||||
|
||||
stored = image_studio.get_project(project.id, path=config["db_path"])
|
||||
self.assertEqual("AI生成的新卖点", stored.draft_prompt)
|
||||
self.assertEqual("AI生成的新卖点", state.last_saved_prompt)
|
||||
self.assertEqual("AI生成的新卖点", tab.prompt_edit.toPlainText())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_prompt_edit_expands_shrinks_and_reflows_without_internal_scrollbars(self):
|
||||
edit = AutoHeightPlainTextEdit()
|
||||
self.addCleanup(edit.close)
|
||||
edit.resize(420, 96)
|
||||
edit.show()
|
||||
self.app.processEvents()
|
||||
|
||||
self.assertEqual(Qt.ScrollBarAlwaysOff, edit.horizontalScrollBarPolicy())
|
||||
self.assertEqual(Qt.ScrollBarAlwaysOff, edit.verticalScrollBarPolicy())
|
||||
self.assertGreaterEqual(edit.height(), 96)
|
||||
minimum_height = edit.height()
|
||||
|
||||
edit.setPlainText("\n".join("第%d行商品卖点" % index for index in range(1, 13)))
|
||||
QTest.qWait(50)
|
||||
self.app.processEvents()
|
||||
expanded_height = edit.height()
|
||||
self.assertGreater(expanded_height, minimum_height)
|
||||
|
||||
edit.clear()
|
||||
QTest.qWait(50)
|
||||
self.app.processEvents()
|
||||
self.assertEqual(minimum_height, edit.height())
|
||||
|
||||
edit.setPlainText("这是一段用于测试窗口变窄后自动换行的商品卖点内容。" * 16)
|
||||
edit.setFixedWidth(420)
|
||||
QTest.qWait(50)
|
||||
self.app.processEvents()
|
||||
wide_height = edit.height()
|
||||
edit.setFixedWidth(180)
|
||||
QTest.qWait(50)
|
||||
self.app.processEvents()
|
||||
self.assertGreater(edit.height(), wide_height)
|
||||
|
||||
def test_project_settings_and_result_history_use_existing_backend(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
config = self._config(temp_dir)
|
||||
|
||||
Reference in New Issue
Block a user