fix: stabilize Shopee cover replacement
This commit is contained in:
@@ -12,9 +12,9 @@ import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
from . import appconfig, db, diagnostics
|
||||
from . import appconfig, db, diagnostics, image_paths
|
||||
from . import prompts as prompt_module
|
||||
from .config import make_slug
|
||||
|
||||
|
||||
|
||||
class AIError(RuntimeError):
|
||||
@@ -390,16 +390,7 @@ def _account_name(task, account_by_alias):
|
||||
def _new_cover_path(task, account_by_alias, image_root):
|
||||
alias = str(getattr(task, "alias", "") or "").strip()
|
||||
account = account_by_alias.get(alias)
|
||||
slug = getattr(account, "slug", None) if account is not None else None
|
||||
if not slug:
|
||||
slug = make_slug(alias or getattr(task, "account_name", "") or "unknown")
|
||||
return os.path.abspath(
|
||||
os.path.join(
|
||||
image_root,
|
||||
slug,
|
||||
"%s_new.jpg" % getattr(task, "item_id", ""),
|
||||
)
|
||||
)
|
||||
return image_paths.task_image_path(image_root, task, account, "new")
|
||||
|
||||
|
||||
def _mark_generate_failed(task, exc, db_path, on_task_update):
|
||||
|
||||
+107
-69
@@ -5,6 +5,7 @@ import os
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from . import image_paths
|
||||
from .cdp import CDP, close_tab, create_tab, create_tab_info, find_product_tab, http_get
|
||||
|
||||
|
||||
@@ -55,8 +56,8 @@ JS_UPLOAD_STATE = (
|
||||
"var upload=manager?manager.querySelector('.shopee-image-manager__upload input[type=file]'):null;"
|
||||
"var uploadBox=manager?manager.querySelector('.shopee-image-manager__upload,[class*=image-manager__upload]'):null;"
|
||||
"var busy=manager?[].slice.call(manager.querySelectorAll('[class*=loading],[class*=Loading],[class*=spinner],[class*=Spinner],[class*=progress],[class*=Progress],[class*=uploading],[class*=Uploading]')).filter(visible):[];"
|
||||
"var errRe=/(失敗|失败|錯誤|错误|不支援|不支持|格式|大小|尺寸|超過|超过|error|fail|invalid|unsupported)/i;"
|
||||
"var uploadRe=/(圖片|图片|封面|照片|相片|圖像|图像|image|photo|cover|upload|上傳|上传|檔案|文件|file|格式|大小|尺寸|像素|解析度|分辨率|超過|超过)/i;"
|
||||
"var errRe=/(失敗|失败|錯誤|错误|不支援|不支持|格式|大小|尺寸|超過|超过|重複|重复|duplicate|error|fail|invalid|unsupported)/i;"
|
||||
"var uploadRe=/(圖片|图片|封面|照片|相片|圖像|图像|image|photo|cover|upload|上傳|上传|檔案|文件|file|格式|大小|尺寸|像素|解析度|分辨率|超過|超过|重複|重复|duplicate)/i;"
|
||||
"var texts=manager?[].slice.call(manager.querySelectorAll('*')).filter(visible).map(text).filter(Boolean):[];"
|
||||
"var errors=texts.filter(function(t){return errRe.test(t);}).slice(0,8);"
|
||||
"var roots=[].slice.call(document.querySelectorAll('.eds-modal__content,.eds-modal__box,[role=dialog]')).filter(visible);"
|
||||
@@ -84,6 +85,19 @@ JS_UPLOAD_STATE = (
|
||||
"});})()"
|
||||
)
|
||||
|
||||
JS_CLICK_UPLOAD_TILE = (
|
||||
"(function(){"
|
||||
"function visible(e){if(!e)return false;var r=e.getBoundingClientRect();var s=getComputedStyle(e);"
|
||||
"return r.width>0&&r.height>0&&s.visibility!=='hidden'&&s.display!=='none';}"
|
||||
"var manager=document.querySelector('.shopee-image-manager');"
|
||||
"var box=manager?manager.querySelector('.shopee-image-manager__upload,[class*=image-manager__upload]'):null;"
|
||||
"if(!box)return JSON.stringify({clicked:false,reason:'NO_UPLOAD_TILE'});"
|
||||
"if(!visible(box))return JSON.stringify({clicked:false,reason:'UPLOAD_TILE_HIDDEN'});"
|
||||
"box.scrollIntoView({block:'center',inline:'center'});"
|
||||
"box.click();"
|
||||
"return JSON.stringify({clicked:true,reason:null});"
|
||||
"})()"
|
||||
)
|
||||
JS_TITLE_STATE = (
|
||||
"(function(){"
|
||||
f"var r=document.evaluate({json.dumps(TITLE_XPATH)},document,null,"
|
||||
@@ -317,6 +331,14 @@ def _upload_state(cdp):
|
||||
return _json_value(cdp, JS_UPLOAD_STATE, default={}) or {}
|
||||
|
||||
|
||||
def _click_upload_tile(cdp):
|
||||
return _json_value(
|
||||
cdp,
|
||||
JS_CLICK_UPLOAD_TILE,
|
||||
default={"clicked": False, "reason": "NO_UPLOAD_TILE"},
|
||||
) or {"clicked": False, "reason": "NO_UPLOAD_TILE"}
|
||||
|
||||
|
||||
def _post_update_state(cdp):
|
||||
return _json_value(cdp, JS_POST_UPDATE_STATE, default=None) or {}
|
||||
|
||||
@@ -431,9 +453,24 @@ def _wait_image_manager_stable(
|
||||
}
|
||||
|
||||
|
||||
def _has_duplicate_upload_error(state):
|
||||
texts = []
|
||||
for key in ("errors", "error_toasts", "toasts", "page_error_toasts"):
|
||||
values = (state or {}).get(key) or []
|
||||
texts.extend(str(value) for value in values)
|
||||
return any(
|
||||
token in text.lower()
|
||||
for text in texts
|
||||
for token in ("重複", "重复", "duplicate")
|
||||
)
|
||||
|
||||
|
||||
def _cover_upload_error_message(result):
|
||||
reason = result.get("reason") or "COVER_UPDATE_FAILED"
|
||||
state = result.get("upload_state") or {}
|
||||
if reason == "UPLOAD_DUPLICATE_IMAGE":
|
||||
details = state.get("errors") or state.get("error_toasts") or state.get("toasts") or state.get("page_error_toasts") or []
|
||||
return "新封面与现有商品图片重复:" + ";".join(map(str, details[:3])) if details else "新封面与现有商品图片重复"
|
||||
if reason == "UPLOAD_PAGE_ERROR":
|
||||
details = state.get("errors") or state.get("error_toasts") or state.get("toasts") or []
|
||||
return "新封面上传失败:" + ";".join(map(str, details[:3])) if details else "新封面上传失败"
|
||||
@@ -443,6 +480,8 @@ def _cover_upload_error_message(result):
|
||||
return "新封面上传仍在处理中,未取得 Shopee CDN 地址"
|
||||
if reason == "UPLOAD_TIMEOUT":
|
||||
return "新封面上传超时,未取得 Shopee CDN 地址"
|
||||
if reason == "UPLOAD_TILE_NOT_READY":
|
||||
return "新封面上传入口未可点击,未开始上传新封面"
|
||||
if reason == "IMAGE_MANAGER_BUSY":
|
||||
return "商品图片区域仍在加载,未开始上传新封面"
|
||||
if reason == "IMAGE_COUNT_NOT_READY":
|
||||
@@ -634,7 +673,7 @@ def collect(account, task, on_step=None) -> dict:
|
||||
old_cover_src = read_cover_src(cdp)
|
||||
out_path = _get(task, "old_cover_path")
|
||||
if not out_path:
|
||||
out_path = os.path.join(_image_root(account), f"{item_id}_old.jpg")
|
||||
out_path = image_paths.task_image_path("images", task, account, "old")
|
||||
_notify_collect_step(on_step, "download_cover")
|
||||
old_cover_path = download_cover(old_cover_src, out_path)
|
||||
return {
|
||||
@@ -694,8 +733,8 @@ def change_title(cdp, new_title) -> dict:
|
||||
def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict:
|
||||
"""Upload an image and drag it to the first position.
|
||||
|
||||
When the image manager is full, only delete the current first image if the
|
||||
old cover backup from the collect stage exists locally.
|
||||
Cover replacement always deletes the current first Shopee image first, and
|
||||
only proceeds when the old cover backup from the collect stage exists.
|
||||
"""
|
||||
|
||||
image_win_path = os.path.abspath(str(image_win_path))
|
||||
@@ -726,61 +765,52 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
|
||||
before = stable.get("rects") or _image_rects(cdp)
|
||||
count_before = len(before)
|
||||
delete_result = None
|
||||
if len(before) >= 9:
|
||||
backup_path = _validated_old_cover_backup(old_cover_path)
|
||||
if not backup_path:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "OLD_COVER_BACKUP_MISSING",
|
||||
"count_before": count_before,
|
||||
"old_cover_path": old_cover_path,
|
||||
}
|
||||
delete_result = _delete_first_cover(cdp, before)
|
||||
if not delete_result.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": delete_result.get("reason"),
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
}
|
||||
stable = _wait_image_manager_stable(
|
||||
cdp,
|
||||
expected_count=delete_result.get("count_after"),
|
||||
timeout=30,
|
||||
settle_seconds=2.0,
|
||||
require_upload_input=True,
|
||||
)
|
||||
if not stable.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": stable.get("reason"),
|
||||
"count_before": count_before,
|
||||
"count_after": stable.get("count_after"),
|
||||
"delete": delete_result,
|
||||
"upload_state": stable.get("upload_state"),
|
||||
"stable": stable,
|
||||
}
|
||||
before = stable.get("rects") or _image_rects(cdp)
|
||||
else:
|
||||
stable = _wait_image_manager_stable(
|
||||
cdp,
|
||||
expected_count=len(before),
|
||||
timeout=20,
|
||||
settle_seconds=1.0,
|
||||
require_upload_input=True,
|
||||
)
|
||||
if not stable.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": stable.get("reason"),
|
||||
"count_before": count_before,
|
||||
"count_after": stable.get("count_after"),
|
||||
"upload_state": stable.get("upload_state"),
|
||||
"stable": stable,
|
||||
}
|
||||
before = stable.get("rects") or _image_rects(cdp)
|
||||
backup_path = _validated_old_cover_backup(old_cover_path)
|
||||
if not backup_path:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "OLD_COVER_BACKUP_MISSING",
|
||||
"count_before": count_before,
|
||||
"old_cover_path": old_cover_path,
|
||||
}
|
||||
delete_result = _delete_first_cover(cdp, before)
|
||||
if not delete_result.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": delete_result.get("reason"),
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
}
|
||||
stable = _wait_image_manager_stable(
|
||||
cdp,
|
||||
expected_count=delete_result.get("count_after"),
|
||||
timeout=30,
|
||||
settle_seconds=2.0,
|
||||
require_upload_input=True,
|
||||
)
|
||||
if not stable.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": stable.get("reason"),
|
||||
"count_before": count_before,
|
||||
"count_after": stable.get("count_after"),
|
||||
"delete": delete_result,
|
||||
"upload_state": stable.get("upload_state"),
|
||||
"stable": stable,
|
||||
}
|
||||
before = stable.get("rects") or _image_rects(cdp)
|
||||
before_srcs = {r.get("src") for r in before}
|
||||
upload_click = _click_upload_tile(cdp)
|
||||
if not upload_click.get("clicked"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "UPLOAD_TILE_NOT_READY",
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": _upload_state(cdp),
|
||||
}
|
||||
time.sleep(1)
|
||||
oid = cdp.object_id("document.querySelector('.shopee-image-manager__upload input[type=file]')")
|
||||
if not oid:
|
||||
return {
|
||||
@@ -788,6 +818,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"reason": "NO_UPLOAD_INPUT",
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": _upload_state(cdp),
|
||||
}
|
||||
|
||||
@@ -807,6 +838,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"reason": "UPLOAD_INPUT_NOT_READY",
|
||||
"count_before": count_before,
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": last_state,
|
||||
}
|
||||
blob_seen = bool(last_state.get("blob_count"))
|
||||
@@ -816,6 +848,18 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
cur = _image_rects(cdp)
|
||||
last_state = _upload_state(cdp)
|
||||
blob_seen = blob_seen or any(str(r.get("src") or "").startswith("blob:") for r in cur)
|
||||
if (last_state.get("errors") or last_state.get("error_toasts")) and not last_state.get("busy_count"):
|
||||
reason = "UPLOAD_DUPLICATE_IMAGE" if _has_duplicate_upload_error(last_state) else "UPLOAD_PAGE_ERROR"
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": reason,
|
||||
"count_before": count_before,
|
||||
"count_after": len(cur),
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": last_state,
|
||||
"file_size": file_size,
|
||||
}
|
||||
ready = [
|
||||
r for r in cur
|
||||
if r.get("src") not in before_srcs
|
||||
@@ -826,16 +870,6 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
if ready and (len(cur) > len(before) or len(ready) == 1):
|
||||
new_src = ready[-1]["src"]
|
||||
break
|
||||
if (last_state.get("errors") or last_state.get("error_toasts")) and not last_state.get("busy_count"):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "UPLOAD_PAGE_ERROR",
|
||||
"count_before": count_before,
|
||||
"count_after": len(cur),
|
||||
"delete": delete_result,
|
||||
"upload_state": last_state,
|
||||
"file_size": file_size,
|
||||
}
|
||||
if last_state.get("crop_modal"):
|
||||
return {
|
||||
"ok": False,
|
||||
@@ -843,6 +877,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"count_before": count_before,
|
||||
"count_after": len(cur),
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": last_state,
|
||||
"file_size": file_size,
|
||||
}
|
||||
@@ -856,6 +891,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"count_before": count_before,
|
||||
"count_after": len(cur),
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
"upload_state": last_state,
|
||||
"blob_seen": blob_seen,
|
||||
"file_size": file_size,
|
||||
@@ -881,6 +917,7 @@ def replace_cover(cdp, image_win_path, old_cover_path=None, timeout=180) -> dict
|
||||
"count_before": count_before,
|
||||
"count_after": len(after),
|
||||
"delete": delete_result,
|
||||
"upload_click": upload_click,
|
||||
}
|
||||
|
||||
|
||||
@@ -1069,6 +1106,7 @@ def _close_applied_product(cdp, close_success_tab=False):
|
||||
finally:
|
||||
if close_success_tab and created_by_app and target_id:
|
||||
try:
|
||||
time.sleep(2)
|
||||
close_tab(target_id, host=host)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
+167
-114
@@ -85,7 +85,7 @@ QTabBar::tab:hover:!selected {
|
||||
|
||||
|
||||
if QT_IMPORT_ERROR is None:
|
||||
from . import accounts, ai, appconfig, chrome, db, diagnostics, editor, excel, prompts
|
||||
from . import accounts, ai, appconfig, chrome, db, diagnostics, editor, excel, image_paths, prompts
|
||||
from . import config as account_config
|
||||
|
||||
|
||||
@@ -1157,7 +1157,7 @@ if QT_IMPORT_ERROR is None:
|
||||
filter_layout.addWidget(self.refresh_button)
|
||||
|
||||
self.summary_label = QLabel("任务 0 条")
|
||||
self.risk_label = QLabel("点击「开始更新」后会先确认当前筛选范围;确认后才允许后续任务提交线上。")
|
||||
self.risk_label = QLabel("可先点击「预览本轮更新」检查当前筛选范围;点击「开始更新」后会再次确认并按批提交线上。")
|
||||
self.task_table = QTableView()
|
||||
self.model = ApplyTaskTableModel(self.task_table)
|
||||
self.task_table.setModel(self.model)
|
||||
@@ -1172,6 +1172,8 @@ if QT_IMPORT_ERROR is None:
|
||||
self.run_log_view.setMaximumHeight(128)
|
||||
self.run_log_view.setPlaceholderText("运行日志")
|
||||
|
||||
self.preview_update_button = QPushButton("预览本轮更新")
|
||||
self.preview_update_button.setObjectName("previewUpdateButton")
|
||||
self.start_update_button = QPushButton("开始更新")
|
||||
self.stop_update_button = QPushButton("停止")
|
||||
self.reset_update_button = QPushButton("重置更新状态")
|
||||
@@ -1181,6 +1183,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self.write_back_button.setEnabled(False)
|
||||
|
||||
action_layout = QHBoxLayout()
|
||||
action_layout.addWidget(self.preview_update_button)
|
||||
action_layout.addWidget(self.start_update_button)
|
||||
action_layout.addWidget(self.stop_update_button)
|
||||
action_layout.addWidget(self.reset_update_button)
|
||||
@@ -1202,6 +1205,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self.item_filter.textChanged.connect(self.refresh_tasks)
|
||||
self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||
self.refresh_button.clicked.connect(self.refresh_tasks)
|
||||
self.preview_update_button.clicked.connect(self.preview_update)
|
||||
self.start_update_button.clicked.connect(self.start_update)
|
||||
self.stop_update_button.clicked.connect(self.stop_update)
|
||||
self.reset_update_button.clicked.connect(self.reset_apply_status)
|
||||
@@ -1249,6 +1253,12 @@ if QT_IMPORT_ERROR is None:
|
||||
self._update_write_back_button()
|
||||
|
||||
def start_update(self, checked=False):
|
||||
self._start_update(dry_run=False)
|
||||
|
||||
def preview_update(self, checked=False):
|
||||
self._start_update(dry_run=True)
|
||||
|
||||
def _start_update(self, dry_run=False):
|
||||
if self.apply_thread is not None:
|
||||
self._set_status("更新正在进行...")
|
||||
return
|
||||
@@ -1260,7 +1270,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self._set_status("当前筛选结果没有可更新任务")
|
||||
return
|
||||
update_cfg = self._shopee_update_config()
|
||||
dry_run = bool(update_cfg.get("dry_run", False))
|
||||
dry_run = bool(dry_run)
|
||||
safety_error = self._update_safety_error(tasks, dry_run=dry_run)
|
||||
if safety_error:
|
||||
QMessageBox.warning(self, "更新安全开关", safety_error)
|
||||
@@ -1268,14 +1278,15 @@ if QT_IMPORT_ERROR is None:
|
||||
return
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"确认开始更新",
|
||||
self._confirmation_message(tasks),
|
||||
"确认预览本轮更新" if dry_run else "确认开始更新",
|
||||
self._confirmation_message(tasks, dry_run=dry_run),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if answer != QMessageBox.Yes:
|
||||
self._set_status("已取消开始更新")
|
||||
self._set_status("已取消预览本轮更新" if dry_run else "已取消开始更新")
|
||||
return
|
||||
batch_size = max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
|
||||
worker = ApplyWorker(
|
||||
tasks,
|
||||
db_path=self.db_path,
|
||||
@@ -1287,6 +1298,7 @@ if QT_IMPORT_ERROR is None:
|
||||
1,
|
||||
int(update_cfg.get("max_parallel_accounts", 1) or 1),
|
||||
),
|
||||
batch_size=batch_size,
|
||||
)
|
||||
worker.progress.connect(self._on_apply_progress)
|
||||
worker.row_updated.connect(self._on_apply_row_updated)
|
||||
@@ -1301,9 +1313,9 @@ if QT_IMPORT_ERROR is None:
|
||||
self._set_apply_running(True)
|
||||
self.run_log_view.clear()
|
||||
if dry_run:
|
||||
self._set_status(f"开始 dry-run 预览:{len(tasks)} 条")
|
||||
self._set_status(f"开始预览本轮更新:{len(tasks)} 条")
|
||||
else:
|
||||
self._set_status(f"开始更新:{len(tasks)} 条")
|
||||
self._set_status(f"开始更新:{len(tasks)} 条,按每批最多 {batch_size} 条执行")
|
||||
thread.start()
|
||||
|
||||
def stop_update(self, checked=False):
|
||||
@@ -1466,34 +1478,39 @@ if QT_IMPORT_ERROR is None:
|
||||
return task.status == "skipped"
|
||||
return True
|
||||
|
||||
def _confirmation_message(self, tasks):
|
||||
def _confirmation_message(self, tasks, dry_run=False):
|
||||
update_cfg = self._shopee_update_config()
|
||||
cover_text = "允许" if update_cfg.get("allow_cover_update") else "不允许"
|
||||
close_text = "是" if update_cfg.get("close_success_tab") else "否"
|
||||
dry_run_text = "开启" if update_cfg.get("dry_run") else "关闭"
|
||||
batch_size = max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
|
||||
batch_count = (len(tasks) + batch_size - 1) // batch_size if tasks else 0
|
||||
parallel_text = (
|
||||
f"开启,最多 {update_cfg.get('max_parallel_accounts', 1)} 个账号"
|
||||
if update_cfg.get("parallel_accounts")
|
||||
else "关闭"
|
||||
)
|
||||
intro = (
|
||||
"即将预览当前筛选结果。\n\n"
|
||||
if dry_run
|
||||
else "即将按当前筛选结果分批更新 Shopee 线上商品。\n\n"
|
||||
)
|
||||
return (
|
||||
"即将按当前筛选结果开始更新 Shopee 线上商品。\n\n"
|
||||
f"批次:{self._batch_filter_label()}\n"
|
||||
f"店铺:{self._shop_filter_label()}\n"
|
||||
f"商品ID:{self._item_filter_label()}\n"
|
||||
f"状态:{self._status_label()}\n"
|
||||
f"任务数:{len(tasks)}\n\n"
|
||||
"安全设置:"
|
||||
f"测试商品ID={update_cfg.get('test_item_id') or '未配置'},"
|
||||
f"封面更新={cover_text},"
|
||||
f"最大条数={update_cfg.get('max_items_per_run', 1)},"
|
||||
f"成功后关闭新页={close_text},"
|
||||
f"dry-run={dry_run_text},"
|
||||
f"多账号并行={parallel_text}\n\n"
|
||||
intro
|
||||
+ f"批次:{self._batch_filter_label()}\n"
|
||||
+ f"店铺:{self._shop_filter_label()}\n"
|
||||
+ f"商品ID:{self._item_filter_label()}\n"
|
||||
+ f"状态:{self._status_label()}\n"
|
||||
+ f"任务数:{len(tasks)}\n"
|
||||
+ f"预计批次:{batch_count}\n\n"
|
||||
+ "安全设置:"
|
||||
+ f"封面更新={cover_text},"
|
||||
+ f"每批最大更新条数={batch_size},"
|
||||
+ f"成功后关闭新页={close_text},"
|
||||
+ f"多账号并行={parallel_text}\n\n"
|
||||
+ (
|
||||
"dry-run 开启时只写运行日志和预览,不打开 Shopee、不点击「更新」、不改任务状态。"
|
||||
if update_cfg.get("dry_run")
|
||||
else "确认后后续执行会打开商品编辑页、替换标题/允许时替换封面,并点击「更新」提交线上。"
|
||||
"预览只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态。"
|
||||
if dry_run
|
||||
else f"确认后会打开商品编辑页、替换标题/允许时替换封面,并按每批最多 {batch_size} 条点击「更新」提交线上;点击停止后不再开始下一条或下一批。"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1503,20 +1520,6 @@ if QT_IMPORT_ERROR is None:
|
||||
return None
|
||||
if not update_cfg.get("allow_real_submit", False):
|
||||
return "设置未开启「允许真实提交线上商品」,已阻止本次更新。"
|
||||
max_items = max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
|
||||
if len(tasks) > max_items:
|
||||
return f"当前筛选结果有 {len(tasks)} 条,超过单次最大更新条数 {max_items}。"
|
||||
test_item_id = str(update_cfg.get("test_item_id", "")).strip()
|
||||
if not test_item_id:
|
||||
return "未配置测试商品ID,已阻止真实提交。"
|
||||
mismatched = [
|
||||
str(getattr(task, "item_id", ""))
|
||||
for task in tasks
|
||||
if str(getattr(task, "item_id", "")) != test_item_id
|
||||
]
|
||||
if mismatched:
|
||||
shown = "、".join(mismatched[:5])
|
||||
return f"当前任务包含非测试商品ID:{shown}。只允许更新测试商品 {test_item_id}。"
|
||||
if not update_cfg.get("allow_cover_update", False):
|
||||
cover_tasks = [
|
||||
str(getattr(task, "item_id", ""))
|
||||
@@ -1537,6 +1540,7 @@ if QT_IMPORT_ERROR is None:
|
||||
return merged
|
||||
|
||||
def _set_apply_running(self, running):
|
||||
self.preview_update_button.setEnabled(not running)
|
||||
self.start_update_button.setEnabled(not running)
|
||||
self.stop_update_button.setEnabled(running)
|
||||
self.reset_update_button.setEnabled(not running)
|
||||
@@ -1548,6 +1552,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self._update_write_back_button()
|
||||
|
||||
def _set_result_write_back_running(self, running):
|
||||
self.preview_update_button.setEnabled(not running)
|
||||
self.start_update_button.setEnabled(not running)
|
||||
self.reset_update_button.setEnabled(not running)
|
||||
self.refresh_button.setEnabled(not running)
|
||||
@@ -1588,7 +1593,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self._show_apply_blocked(payload)
|
||||
return
|
||||
self.last_apply_summary = dict(payload)
|
||||
prefix = "dry-run 预览完成:" if payload.get("dry_run") else "更新完成:"
|
||||
prefix = "预览本轮更新完成:" if payload.get("dry_run") else "更新完成:"
|
||||
message = prefix + self._apply_progress_text(payload)
|
||||
batch_ids = payload.get("batch_ids") or self._active_batch_ids()
|
||||
if (not payload.get("dry_run")) and payload.get("done", 0) > 0 and batch_ids:
|
||||
@@ -1665,6 +1670,7 @@ if QT_IMPORT_ERROR is None:
|
||||
def _show_account_guide(self, message):
|
||||
full_message = (
|
||||
f"{message}\n\n"
|
||||
"本轮更新已中止,不会自动打开账号 Chrome,也不会提交任何商品。\n"
|
||||
"请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录 Shopee。"
|
||||
)
|
||||
QMessageBox.warning(self, "账号未就绪", full_message)
|
||||
@@ -1775,14 +1781,14 @@ if QT_IMPORT_ERROR is None:
|
||||
def _show_apply_summary(self, apply_summary, write_back_payload=None):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"dry-run 预览完成" if apply_summary.get("dry_run") else "更新完成",
|
||||
"预览本轮更新完成" if apply_summary.get("dry_run") else "更新完成",
|
||||
self._apply_summary_message(apply_summary, write_back_payload),
|
||||
)
|
||||
|
||||
def _apply_summary_message(self, apply_summary, write_back_payload=None, error=None):
|
||||
dry_run = bool(apply_summary.get("dry_run"))
|
||||
lines = [
|
||||
"dry-run 预览完成,未打开 Shopee、未提交线上、未改任务状态。"
|
||||
"预览本轮更新完成,未打开 Shopee、未提交线上、未改任务状态。"
|
||||
if dry_run
|
||||
else "更新完成。",
|
||||
"{success_label}:{applied},失败:{failed},略过:{skipped}".format(
|
||||
@@ -1933,6 +1939,7 @@ if QT_IMPORT_ERROR is None:
|
||||
def _show_account_guide(self, message):
|
||||
full_message = (
|
||||
f"{message}\n\n"
|
||||
"本轮采集已中止,不会自动打开账号 Chrome。\n"
|
||||
"请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录 Shopee。"
|
||||
)
|
||||
QMessageBox.warning(self, "账号未就绪", full_message)
|
||||
@@ -2577,6 +2584,7 @@ if QT_IMPORT_ERROR is None:
|
||||
dry_run=False,
|
||||
parallel_accounts=False,
|
||||
max_parallel_accounts=1,
|
||||
batch_size=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.tasks = list(tasks)
|
||||
@@ -2587,6 +2595,9 @@ if QT_IMPORT_ERROR is None:
|
||||
self.dry_run = bool(dry_run)
|
||||
self.parallel_accounts = bool(parallel_accounts)
|
||||
self.max_parallel_accounts = max(1, int(max_parallel_accounts or 1))
|
||||
self.batch_size = None if batch_size is None else max(1, int(batch_size or 1))
|
||||
self._current_batch_size = None
|
||||
self._batch_count = 0
|
||||
self._progress_lock = threading.Lock()
|
||||
self._run_id = None
|
||||
|
||||
@@ -2600,21 +2611,23 @@ if QT_IMPORT_ERROR is None:
|
||||
eligible = [task for task in self.tasks if self._is_actionable_task(task)]
|
||||
batch_ids = self._batch_ids(eligible)
|
||||
total = len(eligible)
|
||||
applied = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
done = 0
|
||||
batch_size = self._effective_batch_size(total)
|
||||
batches = self._task_batches(eligible, batch_size)
|
||||
self._current_batch_size = batch_size
|
||||
self._batch_count = len(batches)
|
||||
counters = {
|
||||
"done": done,
|
||||
"applied": applied,
|
||||
"skipped": skipped,
|
||||
"failed": failed,
|
||||
"done": 0,
|
||||
"applied": 0,
|
||||
"skipped": 0,
|
||||
"failed": 0,
|
||||
}
|
||||
self._run_id = self._create_run_log(eligible, batch_ids)
|
||||
self._log_run_event(
|
||||
"运行开始:{mode},任务{total},{parallel}".format(
|
||||
mode="dry-run 预览" if self.dry_run else "真实更新",
|
||||
"运行开始:{mode},任务{total},每批最多{batch_size},批次{batch_count},{parallel}".format(
|
||||
mode="预览本轮更新" if self.dry_run else "真实更新",
|
||||
total=total,
|
||||
batch_size=batch_size,
|
||||
batch_count=len(batches),
|
||||
parallel=(
|
||||
f"多账号并行最多{self.max_parallel_accounts}"
|
||||
if self.parallel_accounts
|
||||
@@ -2637,20 +2650,24 @@ if QT_IMPORT_ERROR is None:
|
||||
self._finish_run_log("blocked", summary)
|
||||
return summary
|
||||
|
||||
if self.dry_run:
|
||||
for task in eligible:
|
||||
if self.should_cancel():
|
||||
break
|
||||
outcome = self._preview_task(task, account_by_alias)
|
||||
self._record_outcome(counters, total, outcome)
|
||||
elif self.parallel_accounts and self.max_parallel_accounts > 1:
|
||||
self._run_parallel_by_account(eligible, account_by_alias, counters, total)
|
||||
else:
|
||||
for task in eligible:
|
||||
if self.should_cancel():
|
||||
break
|
||||
outcome = self._apply_one_task(task, account_by_alias)
|
||||
self._record_outcome(counters, total, outcome)
|
||||
for batch_index, batch_tasks in enumerate(batches, start=1):
|
||||
if self.should_cancel():
|
||||
break
|
||||
self._log_batch_start(batch_index, len(batches), batch_tasks, counters, total)
|
||||
if self.dry_run:
|
||||
for task in batch_tasks:
|
||||
if self.should_cancel():
|
||||
break
|
||||
outcome = self._preview_task(task, account_by_alias)
|
||||
self._record_outcome(counters, total, outcome)
|
||||
elif self.parallel_accounts and self.max_parallel_accounts > 1:
|
||||
self._run_parallel_by_account(batch_tasks, account_by_alias, counters, total)
|
||||
else:
|
||||
for task in batch_tasks:
|
||||
if self.should_cancel():
|
||||
break
|
||||
outcome = self._apply_one_task(task, account_by_alias)
|
||||
self._record_outcome(counters, total, outcome)
|
||||
|
||||
summary = self._summary(
|
||||
ok=counters["failed"] == 0,
|
||||
@@ -2729,6 +2746,27 @@ if QT_IMPORT_ERROR is None:
|
||||
)
|
||||
return duplicates
|
||||
|
||||
def _effective_batch_size(self, total):
|
||||
if self.batch_size is None:
|
||||
return max(1, int(total or 1))
|
||||
return self.batch_size
|
||||
|
||||
def _task_batches(self, tasks, batch_size):
|
||||
if not tasks:
|
||||
return []
|
||||
return [
|
||||
tasks[index:index + batch_size]
|
||||
for index in range(0, len(tasks), batch_size)
|
||||
]
|
||||
|
||||
def _log_batch_start(self, batch_index, batch_count, batch_tasks, counters, total):
|
||||
first = counters["done"] + 1
|
||||
last = min(first + len(batch_tasks) - 1, total)
|
||||
label = "预览批次" if self.dry_run else "更新批次"
|
||||
self._log_run_event(
|
||||
f"{label} {batch_index}/{batch_count} 开始:任务 {first}-{last}/{total}"
|
||||
)
|
||||
|
||||
def _run_parallel_by_account(self, eligible, account_by_alias, counters, total):
|
||||
groups = self._group_tasks_by_alias(eligible)
|
||||
max_workers = min(self.max_parallel_accounts, len(groups))
|
||||
@@ -2773,7 +2811,7 @@ if QT_IMPORT_ERROR is None:
|
||||
if account is None:
|
||||
reason = "别名未匹配账号"
|
||||
self._log_run_event(
|
||||
f"dry-run:任务 {task.id} 商品 {task.item_id} 将略过:{reason}",
|
||||
f"预览:任务 {task.id} 商品 {task.item_id} 将略过:{reason}",
|
||||
task=task,
|
||||
level="warning",
|
||||
)
|
||||
@@ -2785,7 +2823,7 @@ if QT_IMPORT_ERROR is None:
|
||||
action_parts.append("封面")
|
||||
action_text = "+".join(action_parts) or "无变更"
|
||||
self._log_run_event(
|
||||
"dry-run:任务 {task_id} 商品 {item_id} 账号 {alias} 将更新 {action}".format(
|
||||
"预览:任务 {task_id} 商品 {item_id} 账号 {alias} 将更新 {action}".format(
|
||||
task_id=task.id,
|
||||
item_id=task.item_id,
|
||||
alias=account.alias,
|
||||
@@ -2903,6 +2941,8 @@ if QT_IMPORT_ERROR is None:
|
||||
"skipped": skipped,
|
||||
"failed": failed,
|
||||
"dry_run": self.dry_run,
|
||||
"batch_size": self._current_batch_size,
|
||||
"batch_count": self._batch_count,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2938,6 +2978,8 @@ if QT_IMPORT_ERROR is None:
|
||||
"batch_ids": batch_ids,
|
||||
"dry_run": self.dry_run,
|
||||
"parallel_accounts": self.parallel_accounts,
|
||||
"batch_size": self._current_batch_size,
|
||||
"batch_count": self._batch_count,
|
||||
"run_id": self._run_id,
|
||||
}
|
||||
if blocked:
|
||||
@@ -2958,6 +3000,8 @@ if QT_IMPORT_ERROR is None:
|
||||
"dry_run": self.dry_run,
|
||||
"parallel_accounts": self.parallel_accounts,
|
||||
"max_parallel_accounts": self.max_parallel_accounts,
|
||||
"batch_size": self._current_batch_size,
|
||||
"batch_count": self._batch_count,
|
||||
},
|
||||
path=self.db_path,
|
||||
)
|
||||
@@ -3284,13 +3328,7 @@ if QT_IMPORT_ERROR is None:
|
||||
|
||||
def _old_cover_path(self, account, task):
|
||||
image_root = appconfig.image_dir(self.config)
|
||||
return os.path.abspath(
|
||||
os.path.join(
|
||||
image_root,
|
||||
account.slug,
|
||||
f"{task.item_id}_old.jpg",
|
||||
)
|
||||
)
|
||||
return image_paths.task_image_path(image_root, task, account, "old")
|
||||
|
||||
def _batch_ids(self, tasks):
|
||||
batch_ids = []
|
||||
@@ -3630,6 +3668,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self.save_config_button = QPushButton("保存设置")
|
||||
self.test_item_id_edit = QLineEdit()
|
||||
self.test_item_id_edit.setObjectName("testItemIdEdit")
|
||||
self.test_item_id_edit.setVisible(False)
|
||||
self.allow_real_submit_checkbox = QCheckBox("允许真实提交线上商品")
|
||||
self.allow_real_submit_checkbox.setObjectName("allowRealSubmitCheckbox")
|
||||
self.allow_cover_update_checkbox = QCheckBox("允许更新封面")
|
||||
@@ -3637,15 +3676,27 @@ if QT_IMPORT_ERROR is None:
|
||||
self.max_items_per_run_spin = QSpinBox()
|
||||
self.max_items_per_run_spin.setObjectName("maxItemsPerRunSpin")
|
||||
self.max_items_per_run_spin.setRange(1, 9999)
|
||||
self.max_items_per_run_spin.setToolTip("作为每批最大更新条数;正式更新会分批处理当前筛选全部可更新记录。")
|
||||
self.close_success_tab_checkbox = QCheckBox("成功后关闭本次新开编辑页")
|
||||
self.close_success_tab_checkbox.setObjectName("closeSuccessTabCheckbox")
|
||||
self.dry_run_checkbox = QCheckBox("dry-run 只预览不提交")
|
||||
self.dry_run_checkbox = QCheckBox("预览本轮更新")
|
||||
self.dry_run_checkbox.setObjectName("dryRunCheckbox")
|
||||
self.dry_run_checkbox.setVisible(False)
|
||||
self.parallel_accounts_checkbox = QCheckBox("多账号并行更新")
|
||||
self.parallel_accounts_checkbox.setObjectName("parallelAccountsCheckbox")
|
||||
self.max_parallel_accounts_spin = QSpinBox()
|
||||
self.max_parallel_accounts_spin.setObjectName("maxParallelAccountsSpin")
|
||||
self.max_parallel_accounts_spin.setRange(1, 16)
|
||||
self.max_parallel_accounts_label = QLabel("最大并行账号数")
|
||||
self.parallel_accounts_group = QWidget()
|
||||
self.parallel_accounts_group.setObjectName("parallelAccountsGroup")
|
||||
parallel_accounts_layout = QHBoxLayout(self.parallel_accounts_group)
|
||||
parallel_accounts_layout.setContentsMargins(0, 0, 0, 0)
|
||||
parallel_accounts_layout.setSpacing(12)
|
||||
parallel_accounts_layout.addWidget(self.parallel_accounts_checkbox)
|
||||
parallel_accounts_layout.addWidget(self.max_parallel_accounts_label)
|
||||
parallel_accounts_layout.addWidget(self.max_parallel_accounts_spin)
|
||||
parallel_accounts_layout.addStretch(1)
|
||||
|
||||
model_picker_layout = QHBoxLayout()
|
||||
model_picker_layout.addWidget(self.model_combo, 1)
|
||||
@@ -3657,7 +3708,7 @@ if QT_IMPORT_ERROR is None:
|
||||
action_layout.addWidget(self.test_connection_button)
|
||||
action_layout.addStretch(1)
|
||||
|
||||
form = self._two_column_form(
|
||||
form = self._three_column_form(
|
||||
[
|
||||
("状态", self.enabled_checkbox),
|
||||
("服务商名", self.name_edit),
|
||||
@@ -3670,7 +3721,7 @@ if QT_IMPORT_ERROR is None:
|
||||
]
|
||||
)
|
||||
|
||||
ai_form = self._two_column_form(
|
||||
ai_form = self._three_column_form(
|
||||
[
|
||||
("标题大模型", self.default_text_model_combo),
|
||||
("图片大模型", self.default_image_model_combo),
|
||||
@@ -3691,7 +3742,7 @@ if QT_IMPORT_ERROR is None:
|
||||
port_range_widget = QWidget()
|
||||
port_range_widget.setLayout(port_range_layout)
|
||||
|
||||
path_form = self._two_column_form(
|
||||
path_form = self._three_column_form(
|
||||
[
|
||||
("Chrome路径", self.chrome_path_edit, True),
|
||||
("账号数据根目录", self.user_data_root_edit),
|
||||
@@ -3703,22 +3754,20 @@ if QT_IMPORT_ERROR is None:
|
||||
]
|
||||
)
|
||||
|
||||
update_form = self._two_column_form(
|
||||
self.shopee_update_form_layout = self._three_column_form(
|
||||
[
|
||||
("测试商品ID", self.test_item_id_edit),
|
||||
("单次最大更新条数", self.max_items_per_run_spin),
|
||||
("每批最大更新条数", self.max_items_per_run_spin),
|
||||
("", self.allow_real_submit_checkbox),
|
||||
("", self.allow_cover_update_checkbox),
|
||||
("", self.close_success_tab_checkbox),
|
||||
("", self.dry_run_checkbox),
|
||||
("", self.parallel_accounts_checkbox),
|
||||
("最大并行账号数", self.max_parallel_accounts_spin),
|
||||
("", self.allow_cover_update_checkbox),
|
||||
("", self.parallel_accounts_group, 2),
|
||||
]
|
||||
)
|
||||
|
||||
panel = QWidget()
|
||||
panel.setMaximumWidth(1800)
|
||||
panel_layout = QVBoxLayout(panel)
|
||||
panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||
panel_layout.setContentsMargins(13, 18, 13, 18)
|
||||
panel_layout.addWidget(QLabel("AI 模型"))
|
||||
panel_layout.addLayout(model_picker_layout)
|
||||
panel_layout.addSpacing(14)
|
||||
@@ -3734,13 +3783,19 @@ if QT_IMPORT_ERROR is None:
|
||||
panel_layout.addLayout(path_form)
|
||||
panel_layout.addSpacing(18)
|
||||
panel_layout.addWidget(QLabel("Shopee 更新安全"))
|
||||
panel_layout.addLayout(update_form)
|
||||
panel_layout.addLayout(self.shopee_update_form_layout)
|
||||
panel_layout.addWidget(self.save_config_button)
|
||||
panel_layout.addStretch(1)
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(panel)
|
||||
scroll_content = QWidget()
|
||||
scroll_layout = QHBoxLayout(scroll_content)
|
||||
scroll_layout.setContentsMargins(0, 0, 0, 0)
|
||||
scroll_layout.addStretch(1)
|
||||
scroll_layout.addWidget(panel)
|
||||
scroll_layout.addStretch(1)
|
||||
scroll.setWidget(scroll_content)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(18, 18, 18, 18)
|
||||
@@ -3759,46 +3814,43 @@ if QT_IMPORT_ERROR is None:
|
||||
self.refresh_models()
|
||||
self._populate_app_settings()
|
||||
|
||||
def _two_column_form(self, fields):
|
||||
def _three_column_form(self, fields):
|
||||
layout = QGridLayout()
|
||||
layout.setHorizontalSpacing(18)
|
||||
layout.setVerticalSpacing(8)
|
||||
layout.setColumnStretch(1, 1)
|
||||
layout.setColumnStretch(3, 1)
|
||||
for column in (1, 3, 5):
|
||||
layout.setColumnStretch(column, 1)
|
||||
row = 0
|
||||
column_pair = 0
|
||||
for field in fields:
|
||||
label = field[0]
|
||||
widget = field[1]
|
||||
full_width = len(field) > 2 and bool(field[2])
|
||||
if full_width and column_pair:
|
||||
span_pairs = self._form_field_span_pairs(field)
|
||||
if span_pairs > 3 - column_pair:
|
||||
row += 1
|
||||
column_pair = 0
|
||||
column = column_pair * 2
|
||||
self._add_form_field(layout, row, column, label, widget, full_width)
|
||||
if full_width:
|
||||
row += 1
|
||||
column_pair = 0
|
||||
elif column_pair == 0:
|
||||
column_pair = 1
|
||||
else:
|
||||
self._add_form_field(layout, row, column, label, widget, span_pairs)
|
||||
column_pair += span_pairs
|
||||
if column_pair >= 3:
|
||||
row += 1
|
||||
column_pair = 0
|
||||
return layout
|
||||
|
||||
def _add_form_field(self, layout, row, column, label, widget, full_width):
|
||||
if full_width:
|
||||
if label:
|
||||
layout.addWidget(QLabel(label), row, 0)
|
||||
layout.addWidget(widget, row, 1, 1, 3)
|
||||
else:
|
||||
layout.addWidget(widget, row, 0, 1, 4)
|
||||
return
|
||||
def _form_field_span_pairs(self, field):
|
||||
if len(field) <= 2:
|
||||
return 1
|
||||
span = field[2]
|
||||
if isinstance(span, bool):
|
||||
return 3 if span else 1
|
||||
return max(1, min(3, int(span or 1)))
|
||||
|
||||
def _add_form_field(self, layout, row, column, label, widget, span_pairs):
|
||||
if label:
|
||||
layout.addWidget(QLabel(label), row, column)
|
||||
layout.addWidget(widget, row, column + 1)
|
||||
layout.addWidget(widget, row, column + 1, 1, span_pairs * 2 - 1)
|
||||
else:
|
||||
layout.addWidget(widget, row, column, 1, 2)
|
||||
layout.addWidget(widget, row, column, 1, span_pairs * 2)
|
||||
|
||||
def _set_status(self, message):
|
||||
if self.status_callback is not None:
|
||||
@@ -3939,6 +3991,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self._replace_config(saved)
|
||||
self._populate_app_settings()
|
||||
self._set_status("设置已保存")
|
||||
QMessageBox.information(self, "保存设置", "设置已保存")
|
||||
|
||||
def _app_settings_values(self):
|
||||
start_port = self.debug_port_start_spin.value()
|
||||
@@ -3991,7 +4044,7 @@ if QT_IMPORT_ERROR is None:
|
||||
"allow_cover_update": self.allow_cover_update_checkbox.isChecked(),
|
||||
"max_items_per_run": self.max_items_per_run_spin.value(),
|
||||
"close_success_tab": self.close_success_tab_checkbox.isChecked(),
|
||||
"dry_run": self.dry_run_checkbox.isChecked(),
|
||||
"dry_run": False,
|
||||
"parallel_accounts": self.parallel_accounts_checkbox.isChecked(),
|
||||
"max_parallel_accounts": self.max_parallel_accounts_spin.value(),
|
||||
},
|
||||
@@ -4059,7 +4112,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self.close_success_tab_checkbox.setChecked(
|
||||
bool(update_cfg.get("close_success_tab", False))
|
||||
)
|
||||
self.dry_run_checkbox.setChecked(bool(update_cfg.get("dry_run", False)))
|
||||
self.dry_run_checkbox.setChecked(False)
|
||||
self.parallel_accounts_checkbox.setChecked(
|
||||
bool(update_cfg.get("parallel_accounts", False))
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import os
|
||||
|
||||
from .config import make_slug
|
||||
|
||||
|
||||
def task_image_path(image_root, task, account=None, suffix="old", ext=".jpg"):
|
||||
batch_id = _safe_component(_get(task, "batch_id"), "unknown_batch")
|
||||
slug = _account_slug(account, task)
|
||||
task_id = _safe_component(_get(task, "id"), "task")
|
||||
item_id = _safe_component(_get(task, "item_id"), "item")
|
||||
suffix = _safe_component(suffix, "image")
|
||||
ext = str(ext or ".jpg")
|
||||
if not ext.startswith("."):
|
||||
ext = "." + ext
|
||||
return os.path.abspath(
|
||||
os.path.join(
|
||||
str(image_root or "images"),
|
||||
batch_id,
|
||||
slug,
|
||||
f"{task_id}_{item_id}_{suffix}{ext}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _account_slug(account, task):
|
||||
slug = _get(account, "slug")
|
||||
if slug:
|
||||
return _safe_component(slug, "unknown_account")
|
||||
alias = _get(task, "alias") or _get(account, "alias") or _get(task, "account_name") or _get(account, "account_name")
|
||||
if alias:
|
||||
return _safe_component(make_slug(alias), "unknown_account")
|
||||
return "unknown_account"
|
||||
|
||||
|
||||
def _get(obj, name, default=None):
|
||||
if obj is None:
|
||||
return default
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(name, default)
|
||||
return getattr(obj, name, default)
|
||||
|
||||
|
||||
def _safe_component(value, default):
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
text = str(default)
|
||||
safe = "".join(ch if ch.isalnum() or ch in "_-" else "_" for ch in text).strip("_")
|
||||
return safe or str(default)
|
||||
Reference in New Issue
Block a user