feat: add generate progress and readable logs
This commit is contained in:
@@ -62,6 +62,14 @@ def gen_title(
|
|||||||
cfg,
|
cfg,
|
||||||
attempts,
|
attempts,
|
||||||
request_kind="json",
|
request_kind="json",
|
||||||
|
on_retry=lambda attempt, total_attempts, exc: _notify_retry(
|
||||||
|
on_step,
|
||||||
|
"title_request",
|
||||||
|
attempt,
|
||||||
|
total_attempts,
|
||||||
|
exc,
|
||||||
|
model,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
_notify_step(on_step, "title_parse_response")
|
_notify_step(on_step, "title_parse_response")
|
||||||
text = _extract_text(data).strip()
|
text = _extract_text(data).strip()
|
||||||
@@ -110,11 +118,33 @@ def gen_cover(
|
|||||||
attempts,
|
attempts,
|
||||||
request_kind="multipart",
|
request_kind="multipart",
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
|
on_retry=lambda attempt, total_attempts, exc: _notify_retry(
|
||||||
|
on_step,
|
||||||
|
"cover_request",
|
||||||
|
attempt,
|
||||||
|
total_attempts,
|
||||||
|
exc,
|
||||||
|
model,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
payload = _image_chat_payload(model, cover_prompt, old_cover_path, resolution)
|
payload = _image_chat_payload(model, cover_prompt, old_cover_path, resolution)
|
||||||
_notify_step(on_step, "cover_request")
|
_notify_step(on_step, "cover_request")
|
||||||
data = _call_with_retry(model, payload, cfg, attempts, request_kind="json")
|
data = _call_with_retry(
|
||||||
|
model,
|
||||||
|
payload,
|
||||||
|
cfg,
|
||||||
|
attempts,
|
||||||
|
request_kind="json",
|
||||||
|
on_retry=lambda attempt, total_attempts, exc: _notify_retry(
|
||||||
|
on_step,
|
||||||
|
"cover_request",
|
||||||
|
attempt,
|
||||||
|
total_attempts,
|
||||||
|
exc,
|
||||||
|
model,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
_notify_step(on_step, "cover_parse_response")
|
_notify_step(on_step, "cover_parse_response")
|
||||||
image_bytes = _extract_image_bytes(data, model, cfg)
|
image_bytes = _extract_image_bytes(data, model, cfg)
|
||||||
@@ -131,6 +161,23 @@ def _notify_step(callback, step):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_retry(callback, step, attempt, attempts, exc, model):
|
||||||
|
if callback is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
callback(
|
||||||
|
{
|
||||||
|
"step": step,
|
||||||
|
"result": "retry",
|
||||||
|
"attempt": attempt,
|
||||||
|
"attempts": attempts,
|
||||||
|
"detail": _redact(str(exc), model),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=None):
|
def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=None):
|
||||||
"""Generate titles first, then covers, and persist each successful task."""
|
"""Generate titles first, then covers, and persist each successful task."""
|
||||||
|
|
||||||
@@ -186,9 +233,25 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
|||||||
return step_by_task.get(getattr(task, "id", None), fallback)
|
return step_by_task.get(getattr(task, "id", None), fallback)
|
||||||
|
|
||||||
def step_callback(task, phase):
|
def step_callback(task, phase):
|
||||||
def callback(step):
|
def callback(event):
|
||||||
set_step(task, step)
|
if isinstance(event, dict):
|
||||||
_emit_generation_event(on_event, task, phase, step, "start")
|
step = event.get("step") or "unknown"
|
||||||
|
result = event.get("result") or "start"
|
||||||
|
set_step(task, step)
|
||||||
|
_emit_generation_event(
|
||||||
|
on_event,
|
||||||
|
task,
|
||||||
|
phase,
|
||||||
|
step,
|
||||||
|
result,
|
||||||
|
detail=event.get("detail"),
|
||||||
|
level=event.get("level") or ("warning" if result == "retry" else "info"),
|
||||||
|
attempt=event.get("attempt"),
|
||||||
|
attempts=event.get("attempts"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
set_step(task, event)
|
||||||
|
_emit_generation_event(on_event, task, phase, event, "start")
|
||||||
return callback
|
return callback
|
||||||
|
|
||||||
with ThreadPoolExecutor(
|
with ThreadPoolExecutor(
|
||||||
@@ -300,7 +363,7 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
|||||||
"new_cover_path": new_cover_path,
|
"new_cover_path": new_cover_path,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
_emit_generation_event(on_event, task, "cover", "db_write", "success")
|
_emit_generation_event(on_event, task, "cover", "db_write", "success", detail=new_cover_path)
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
summary["cancelled"] = True
|
summary["cancelled"] = True
|
||||||
_emit_generation_event(on_event, task, "cover", get_step(task, "cover_request"), "cancelled", level="warning")
|
_emit_generation_event(on_event, task, "cover", get_step(task, "cover_request"), "cancelled", level="warning")
|
||||||
@@ -401,7 +464,17 @@ def _mark_generate_failed(task, exc, db_path, on_task_update):
|
|||||||
return error
|
return error
|
||||||
|
|
||||||
|
|
||||||
def _emit_generation_event(callback, task, phase, step, result, detail=None, level="info"):
|
def _emit_generation_event(
|
||||||
|
callback,
|
||||||
|
task,
|
||||||
|
phase,
|
||||||
|
step,
|
||||||
|
result,
|
||||||
|
detail=None,
|
||||||
|
level="info",
|
||||||
|
attempt=None,
|
||||||
|
attempts=None,
|
||||||
|
):
|
||||||
if callback is None:
|
if callback is None:
|
||||||
return
|
return
|
||||||
payload = {
|
payload = {
|
||||||
@@ -413,6 +486,10 @@ def _emit_generation_event(callback, task, phase, step, result, detail=None, lev
|
|||||||
}
|
}
|
||||||
if detail is not None:
|
if detail is not None:
|
||||||
payload["detail"] = diagnostics.redact_log_text(detail)
|
payload["detail"] = diagnostics.redact_log_text(detail)
|
||||||
|
if attempt is not None:
|
||||||
|
payload["attempt"] = attempt
|
||||||
|
if attempts is not None:
|
||||||
|
payload["attempts"] = attempts
|
||||||
try:
|
try:
|
||||||
callback(payload)
|
callback(payload)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -475,7 +552,15 @@ def _headers(model, content_type):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _call_with_retry(model, body, config, attempts, request_kind, content_type=None):
|
def _call_with_retry(
|
||||||
|
model,
|
||||||
|
body,
|
||||||
|
config,
|
||||||
|
attempts,
|
||||||
|
request_kind,
|
||||||
|
content_type=None,
|
||||||
|
on_retry=None,
|
||||||
|
):
|
||||||
last_exc = None
|
last_exc = None
|
||||||
for index in range(attempts):
|
for index in range(attempts):
|
||||||
try:
|
try:
|
||||||
@@ -484,6 +569,11 @@ def _call_with_retry(model, body, config, attempts, request_kind, content_type=N
|
|||||||
last_exc = exc
|
last_exc = exc
|
||||||
if index + 1 >= attempts:
|
if index + 1 >= attempts:
|
||||||
break
|
break
|
||||||
|
if on_retry is not None:
|
||||||
|
try:
|
||||||
|
on_retry(index + 1, attempts, exc)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
time.sleep(min(2.0, 0.4 * (index + 1)))
|
time.sleep(min(2.0, 0.4 * (index + 1)))
|
||||||
raise AIError(
|
raise AIError(
|
||||||
"AI 调用失败(已尝试 %s 次): %s"
|
"AI 调用失败(已尝试 %s 次): %s"
|
||||||
|
|||||||
+167
-15
@@ -30,6 +30,7 @@ try:
|
|||||||
QMenu,
|
QMenu,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPlainTextEdit,
|
QPlainTextEdit,
|
||||||
|
QProgressBar,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QScrollArea,
|
QScrollArea,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
@@ -829,6 +830,22 @@ if QT_IMPORT_ERROR is None:
|
|||||||
self.reset_generate_button.setObjectName("resetGenerateButton")
|
self.reset_generate_button.setObjectName("resetGenerateButton")
|
||||||
self.stop_generate_button.setEnabled(False)
|
self.stop_generate_button.setEnabled(False)
|
||||||
self.progress_label = QLabel("进度:标题0/0 · 封面0/0 · 失败0")
|
self.progress_label = QLabel("进度:标题0/0 · 封面0/0 · 失败0")
|
||||||
|
self.title_progress_label = QLabel("标题 0/0")
|
||||||
|
self.title_progress_label.setObjectName("generateTitleProgressLabel")
|
||||||
|
self.title_progress_bar = QProgressBar()
|
||||||
|
self.title_progress_bar.setObjectName("generateTitleProgressBar")
|
||||||
|
self.title_progress_bar.setTextVisible(False)
|
||||||
|
self.title_progress_bar.setRange(0, 1)
|
||||||
|
self.title_progress_bar.setValue(0)
|
||||||
|
self.cover_progress_label = QLabel("图片 0/0")
|
||||||
|
self.cover_progress_label.setObjectName("generateCoverProgressLabel")
|
||||||
|
self.cover_progress_bar = QProgressBar()
|
||||||
|
self.cover_progress_bar.setObjectName("generateCoverProgressBar")
|
||||||
|
self.cover_progress_bar.setTextVisible(False)
|
||||||
|
self.cover_progress_bar.setRange(0, 1)
|
||||||
|
self.cover_progress_bar.setValue(0)
|
||||||
|
self.failed_progress_label = QLabel("失败 0")
|
||||||
|
self.failed_progress_label.setObjectName("generateFailedProgressLabel")
|
||||||
|
|
||||||
left_panel = QWidget()
|
left_panel = QWidget()
|
||||||
left_layout = QVBoxLayout(left_panel)
|
left_layout = QVBoxLayout(left_panel)
|
||||||
@@ -916,12 +933,26 @@ if QT_IMPORT_ERROR is None:
|
|||||||
self.splitter.setStretchFactor(1, 3)
|
self.splitter.setStretchFactor(1, 3)
|
||||||
self.splitter.setSizes([280, 860])
|
self.splitter.setSizes([280, 860])
|
||||||
|
|
||||||
|
title_progress_layout = QHBoxLayout()
|
||||||
|
title_progress_layout.addWidget(self.title_progress_label)
|
||||||
|
title_progress_layout.addWidget(self.title_progress_bar, 1)
|
||||||
|
cover_progress_layout = QHBoxLayout()
|
||||||
|
cover_progress_layout.addWidget(self.cover_progress_label)
|
||||||
|
cover_progress_layout.addWidget(self.cover_progress_bar, 1)
|
||||||
|
cover_progress_layout.addWidget(self.failed_progress_label)
|
||||||
|
progress_layout = QVBoxLayout()
|
||||||
|
progress_layout.addLayout(title_progress_layout)
|
||||||
|
progress_layout.addLayout(cover_progress_layout)
|
||||||
|
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
button_layout.addStretch(1)
|
||||||
|
button_layout.addWidget(self.generate_button)
|
||||||
|
button_layout.addWidget(self.stop_generate_button)
|
||||||
|
button_layout.addWidget(self.reset_generate_button)
|
||||||
|
|
||||||
bottom_layout = QHBoxLayout()
|
bottom_layout = QHBoxLayout()
|
||||||
bottom_layout.addWidget(self.progress_label)
|
bottom_layout.addLayout(progress_layout, 1)
|
||||||
bottom_layout.addStretch(1)
|
bottom_layout.addLayout(button_layout)
|
||||||
bottom_layout.addWidget(self.generate_button)
|
|
||||||
bottom_layout.addWidget(self.stop_generate_button)
|
|
||||||
bottom_layout.addWidget(self.reset_generate_button)
|
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setContentsMargins(18, 18, 18, 18)
|
layout.setContentsMargins(18, 18, 18, 18)
|
||||||
@@ -961,6 +992,8 @@ if QT_IMPORT_ERROR is None:
|
|||||||
|
|
||||||
def _append_generate_log(self, message):
|
def _append_generate_log(self, message):
|
||||||
self.run_log_view.appendPlainText(str(message))
|
self.run_log_view.appendPlainText(str(message))
|
||||||
|
scroll_bar = self.run_log_view.verticalScrollBar()
|
||||||
|
scroll_bar.setValue(scroll_bar.maximum())
|
||||||
|
|
||||||
def _load_latest_generate_run_log(self):
|
def _load_latest_generate_run_log(self):
|
||||||
try:
|
try:
|
||||||
@@ -972,9 +1005,11 @@ if QT_IMPORT_ERROR is None:
|
|||||||
return
|
return
|
||||||
lines = [
|
lines = [
|
||||||
f"{event.created_at} [{event.level}] {event.message}"
|
f"{event.created_at} [{event.level}] {event.message}"
|
||||||
for event in events
|
for event in reversed(events)
|
||||||
]
|
]
|
||||||
self.run_log_view.setPlainText("\n".join(lines))
|
self.run_log_view.setPlainText("\n".join(lines))
|
||||||
|
scroll_bar = self.run_log_view.verticalScrollBar()
|
||||||
|
scroll_bar.setValue(scroll_bar.maximum())
|
||||||
|
|
||||||
def save_title_prompt(self, checked=False):
|
def save_title_prompt(self, checked=False):
|
||||||
try:
|
try:
|
||||||
@@ -1159,6 +1194,7 @@ if QT_IMPORT_ERROR is None:
|
|||||||
def stop_generate(self, checked=False):
|
def stop_generate(self, checked=False):
|
||||||
if self.generate_worker is not None:
|
if self.generate_worker is not None:
|
||||||
self.generate_worker.cancel()
|
self.generate_worker.cancel()
|
||||||
|
self._append_generate_log("[停止] 已收到停止请求,当前正在运行的任务结束后停止")
|
||||||
self._set_status("正在停止 AI 生成...")
|
self._set_status("正在停止 AI 生成...")
|
||||||
|
|
||||||
def reset_generated_result(self, checked=False):
|
def reset_generated_result(self, checked=False):
|
||||||
@@ -1312,7 +1348,22 @@ if QT_IMPORT_ERROR is None:
|
|||||||
self._set_status("AI 生成已停止:" + self._generate_progress_text(payload))
|
self._set_status("AI 生成已停止:" + self._generate_progress_text(payload))
|
||||||
|
|
||||||
def _update_generate_progress(self, payload):
|
def _update_generate_progress(self, payload):
|
||||||
|
total = max(0, int(payload.get("total", 0) or 0))
|
||||||
|
title_done = max(0, int(payload.get("title_done", 0) or 0))
|
||||||
|
cover_done = max(0, int(payload.get("cover_done", 0) or 0))
|
||||||
|
failed = max(0, int(payload.get("failed", 0) or 0))
|
||||||
self.progress_label.setText("进度:" + self._generate_progress_text(payload))
|
self.progress_label.setText("进度:" + self._generate_progress_text(payload))
|
||||||
|
self.title_progress_label.setText(f"标题 {title_done}/{total}")
|
||||||
|
self.cover_progress_label.setText(f"图片 {cover_done}/{total}")
|
||||||
|
self.failed_progress_label.setText(f"失败 {failed}")
|
||||||
|
self._set_progress_bar(self.title_progress_bar, title_done, total)
|
||||||
|
self._set_progress_bar(self.cover_progress_bar, cover_done, total)
|
||||||
|
|
||||||
|
def _set_progress_bar(self, bar, done, total):
|
||||||
|
maximum = max(1, int(total or 0))
|
||||||
|
value = min(max(0, int(done or 0)), maximum)
|
||||||
|
bar.setRange(0, maximum)
|
||||||
|
bar.setValue(value)
|
||||||
|
|
||||||
def _generate_progress_text(self, payload):
|
def _generate_progress_text(self, payload):
|
||||||
return "标题{title}/{total} · 封面{cover}/{total} · 失败{failed}".format(
|
return "标题{title}/{total} · 封面{cover}/{total} · 失败{failed}".format(
|
||||||
@@ -2108,9 +2159,11 @@ if QT_IMPORT_ERROR is None:
|
|||||||
return
|
return
|
||||||
lines = [
|
lines = [
|
||||||
f"{event.created_at} [{event.level}] {event.message}"
|
f"{event.created_at} [{event.level}] {event.message}"
|
||||||
for event in events
|
for event in reversed(events)
|
||||||
]
|
]
|
||||||
self.run_log_view.setPlainText("\n".join(lines))
|
self.run_log_view.setPlainText("\n".join(lines))
|
||||||
|
scroll_bar = self.run_log_view.verticalScrollBar()
|
||||||
|
scroll_bar.setValue(scroll_bar.maximum())
|
||||||
|
|
||||||
def _show_apply_blocked(self, payload):
|
def _show_apply_blocked(self, payload):
|
||||||
lines = ["更新前检查未通过。"]
|
lines = ["更新前检查未通过。"]
|
||||||
@@ -2453,9 +2506,11 @@ if QT_IMPORT_ERROR is None:
|
|||||||
return
|
return
|
||||||
lines = [
|
lines = [
|
||||||
f"{event.created_at} [{event.level}] {event.message}"
|
f"{event.created_at} [{event.level}] {event.message}"
|
||||||
for event in events
|
for event in reversed(events)
|
||||||
]
|
]
|
||||||
self.run_log_view.setPlainText("\n".join(lines))
|
self.run_log_view.setPlainText("\n".join(lines))
|
||||||
|
scroll_bar = self.run_log_view.verticalScrollBar()
|
||||||
|
scroll_bar.setValue(scroll_bar.maximum())
|
||||||
|
|
||||||
def _log_collect_run_event(self, run_id, message, level="info"):
|
def _log_collect_run_event(self, run_id, message, level="info"):
|
||||||
safe_message = diagnostics.redact_log_text(message)
|
safe_message = diagnostics.redact_log_text(message)
|
||||||
@@ -3215,6 +3270,9 @@ if QT_IMPORT_ERROR is None:
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.diagnostic_log_dir = diagnostic_log_dir
|
self.diagnostic_log_dir = diagnostic_log_dir
|
||||||
self._run_id = None
|
self._run_id = None
|
||||||
|
self._account_by_alias = {}
|
||||||
|
self._task_positions = {}
|
||||||
|
self._eligible_total = 0
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
||||||
@@ -3223,14 +3281,25 @@ if QT_IMPORT_ERROR is None:
|
|||||||
for account in account_rows
|
for account in account_rows
|
||||||
if str(account.alias).strip()
|
if str(account.alias).strip()
|
||||||
}
|
}
|
||||||
|
self._account_by_alias = account_by_alias
|
||||||
eligible = [
|
eligible = [
|
||||||
task for task in self.tasks
|
task for task in self.tasks
|
||||||
if getattr(task, "stage", None) == "collected"
|
if getattr(task, "stage", None) == "collected"
|
||||||
]
|
]
|
||||||
|
self._eligible_total = len(eligible)
|
||||||
|
self._task_positions = {
|
||||||
|
getattr(task, "id", None): index
|
||||||
|
for index, task in enumerate(eligible, start=1)
|
||||||
|
}
|
||||||
batch_ids = self._batch_ids(eligible)
|
batch_ids = self._batch_ids(eligible)
|
||||||
self._run_id = self._create_run_log(eligible, batch_ids)
|
self._run_id = self._create_run_log(eligible, batch_ids)
|
||||||
|
ai_cfg = appconfig.ai_config(self.config)
|
||||||
self._log_run_event(
|
self._log_run_event(
|
||||||
f"phase=preflight step=start result=start detail=AI生成开始 total={len(eligible)}"
|
"[开始] 本轮生成 {total} 条:标题{total},图片{total};标题并发{title_concurrency},图片并发{image_concurrency}".format(
|
||||||
|
total=len(eligible),
|
||||||
|
title_concurrency=ai_cfg.get("title_concurrency", 1),
|
||||||
|
image_concurrency=ai_cfg.get("image_concurrency", 1),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
summary = ai.generate_batch(
|
summary = ai.generate_batch(
|
||||||
@@ -3260,7 +3329,7 @@ if QT_IMPORT_ERROR is None:
|
|||||||
"cancelled": self.should_cancel(),
|
"cancelled": self.should_cancel(),
|
||||||
}
|
}
|
||||||
self._log_run_event(
|
self._log_run_event(
|
||||||
f"phase=worker step=execute result=failed detail={error}",
|
f"[失败] AI 生成运行失败:{error}",
|
||||||
level="error",
|
level="error",
|
||||||
)
|
)
|
||||||
self._write_diagnostic_log(
|
self._write_diagnostic_log(
|
||||||
@@ -3273,6 +3342,8 @@ if QT_IMPORT_ERROR is None:
|
|||||||
summary["run_id"] = self._run_id
|
summary["run_id"] = self._run_id
|
||||||
summary["batch_ids"] = batch_ids
|
summary["batch_ids"] = batch_ids
|
||||||
status = "cancelled" if summary.get("cancelled") else "done"
|
status = "cancelled" if summary.get("cancelled") else "done"
|
||||||
|
level = "warning" if summary.get("cancelled") or summary.get("error") else "info"
|
||||||
|
self._log_run_event(self._format_generate_completion(summary), level=level)
|
||||||
self._finish_run_log(status, summary)
|
self._finish_run_log(status, summary)
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
@@ -3280,15 +3351,96 @@ if QT_IMPORT_ERROR is None:
|
|||||||
self.row_updated.emit(int(task_id), dict(fields or {}))
|
self.row_updated.emit(int(task_id), dict(fields or {}))
|
||||||
|
|
||||||
def _on_generation_event(self, payload):
|
def _on_generation_event(self, payload):
|
||||||
|
task = payload.get("task")
|
||||||
|
message = self._format_generation_event(payload)
|
||||||
|
if not message:
|
||||||
|
return
|
||||||
|
self._log_run_event(message, task=task, level=payload.get("level") or "info")
|
||||||
|
|
||||||
|
def _format_generation_event(self, payload):
|
||||||
task = payload.get("task")
|
task = payload.get("task")
|
||||||
phase = payload.get("phase") or "generate"
|
phase = payload.get("phase") or "generate"
|
||||||
step = payload.get("step") or "unknown"
|
step = payload.get("step") or "unknown"
|
||||||
result = payload.get("result") or "start"
|
result = payload.get("result") or "start"
|
||||||
detail = payload.get("detail")
|
detail = self._short_detail(payload.get("detail"))
|
||||||
message = f"phase={phase} step={step} result={result}"
|
if phase == "title":
|
||||||
if detail:
|
if result == "start" and step == "title_submit":
|
||||||
message += f" detail={detail}"
|
return f"[标题] {self._task_progress_label(task)} 开始生成"
|
||||||
self._log_run_event(message, task=task, level=payload.get("level") or "info")
|
if result == "success" and step == "title_done":
|
||||||
|
return f"[标题] {self._task_progress_label(task)} 成功"
|
||||||
|
if result == "retry":
|
||||||
|
return self._retry_message("标题", task, payload, detail)
|
||||||
|
if result == "failed":
|
||||||
|
return f"[失败] {self._task_plain_label(task)} 标题生成失败:{detail or '未知错误'}"
|
||||||
|
if result == "cancelled":
|
||||||
|
return f"[停止] {self._task_plain_label(task)} 标题生成已取消"
|
||||||
|
return None
|
||||||
|
if phase == "cover":
|
||||||
|
if result == "start" and step == "cover_submit":
|
||||||
|
return f"[图片] {self._task_progress_label(task)} 开始生成"
|
||||||
|
if result == "success" and step == "db_write":
|
||||||
|
suffix = f",已保存 {detail}" if detail else ""
|
||||||
|
return f"[图片] {self._task_progress_label(task)} 成功{suffix}"
|
||||||
|
if result == "retry":
|
||||||
|
return self._retry_message("图片", task, payload, detail)
|
||||||
|
if result == "failed":
|
||||||
|
return f"[失败] {self._task_plain_label(task)} 图片生成失败:{detail or '未知错误'}"
|
||||||
|
if result == "cancelled":
|
||||||
|
return f"[停止] {self._task_plain_label(task)} 图片生成已取消"
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _retry_message(self, label, task, payload, detail):
|
||||||
|
attempt = int(payload.get("attempt", 0) or 0)
|
||||||
|
attempts = int(payload.get("attempts", 0) or 0)
|
||||||
|
max_retries = max(0, attempts - 1)
|
||||||
|
retry_text = f"准备重试 {attempt}/{max_retries}" if max_retries else "准备重试"
|
||||||
|
reason = f":{detail}" if detail else ""
|
||||||
|
return f"[{label}] {self._task_progress_label(task)} 调用失败,{retry_text}{reason}"
|
||||||
|
|
||||||
|
def _task_progress_label(self, task):
|
||||||
|
index = self._task_positions.get(getattr(task, "id", None), 0)
|
||||||
|
total = self._eligible_total or 0
|
||||||
|
item_id = getattr(task, "item_id", "") or "未知商品"
|
||||||
|
shop = self._task_shop_label(task)
|
||||||
|
shop_text = f"({shop})" if shop else ""
|
||||||
|
return f"{index}/{total} 商品 {item_id}{shop_text}"
|
||||||
|
|
||||||
|
def _task_plain_label(self, task):
|
||||||
|
item_id = getattr(task, "item_id", "") or "未知商品"
|
||||||
|
shop = self._task_shop_label(task)
|
||||||
|
return f"商品 {item_id}({shop})" if shop else f"商品 {item_id}"
|
||||||
|
|
||||||
|
def _task_shop_label(self, task):
|
||||||
|
alias = str(getattr(task, "alias", "") or "").strip()
|
||||||
|
account = self._account_by_alias.get(alias)
|
||||||
|
if account is not None:
|
||||||
|
return getattr(account, "account_name", None) or getattr(account, "alias", None) or alias
|
||||||
|
return getattr(task, "account_name", None) or alias
|
||||||
|
|
||||||
|
def _short_detail(self, detail):
|
||||||
|
if detail is None:
|
||||||
|
return ""
|
||||||
|
text = diagnostics.redact_log_text(str(detail)).replace("\r", " ").replace("\n", " ").strip()
|
||||||
|
if len(text) > 180:
|
||||||
|
return text[:177] + "..."
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _format_generate_completion(self, summary):
|
||||||
|
progress = self._summary_text(summary)
|
||||||
|
if summary.get("cancelled"):
|
||||||
|
return f"[停止] AI 生成已停止:{progress}"
|
||||||
|
if summary.get("error"):
|
||||||
|
return f"[失败] AI 生成失败:{summary.get('error')},{progress}"
|
||||||
|
return f"[完成] AI 生成完成:{progress}"
|
||||||
|
|
||||||
|
def _summary_text(self, summary):
|
||||||
|
return "标题{title}/{total},图片{cover}/{total},失败{failed}".format(
|
||||||
|
title=summary.get("title_done", 0),
|
||||||
|
cover=summary.get("cover_done", 0),
|
||||||
|
total=summary.get("total", 0),
|
||||||
|
failed=summary.get("failed", 0),
|
||||||
|
)
|
||||||
|
|
||||||
def _on_generation_error(self, payload):
|
def _on_generation_error(self, payload):
|
||||||
task = payload.get("task")
|
task = payload.get("task")
|
||||||
|
|||||||
@@ -102,6 +102,7 @@
|
|||||||
| T-516 | ①筛选对齐②③ | T-202, T-303b | 依据 `docs/ux-review.md` P2。① 导入采集补齐与②③一致的店铺、商品ID、状态筛选,保持三个列表页筛选心智一致。筛选栏顺序为批次/店铺/商品ID/状态;当前筛选结果用于①表格显示与「采集旧标题/旧封面」作用范围;导入汇总栏、批次进度总览、未匹配数量仍按当前批次全量任务统计,未匹配按钮继续筛出当前筛选结果中的未匹配任务;不改导入、采集、回写、CDP 或 Shopee 更新逻辑 | DONE |
|
| T-516 | ①筛选对齐②③ | T-202, T-303b | 依据 `docs/ux-review.md` P2。① 导入采集补齐与②③一致的店铺、商品ID、状态筛选,保持三个列表页筛选心智一致。筛选栏顺序为批次/店铺/商品ID/状态;当前筛选结果用于①表格显示与「采集旧标题/旧封面」作用范围;导入汇总栏、批次进度总览、未匹配数量仍按当前批次全量任务统计,未匹配按钮继续筛出当前筛选结果中的未匹配任务;不改导入、采集、回写、CDP 或 Shopee 更新逻辑 | DONE |
|
||||||
| T-517 | ⑤设置分区 + 清理兼容字段 | T-506, T-507 | 依据 `docs/ux-review.md` P2。⑤ 视觉分区:将高频「Shopee 更新安全 / 执行模式」前置成独立分区,低频「基础设施(Chrome路径/端口/DB路径)」独立后置;彻底隐藏 `test_item_id`、`dry_run` 用户入口,不再占用可见表单位置,但保留 `config.json` 内部兼容字段与③「检查本轮更新」语义。只改⑤设置页 UI 分区、保存/加载展示和 GUI 测试,不改配置 schema,不改 Shopee/CDP 更新流程 | DONE |
|
| T-517 | ⑤设置分区 + 清理兼容字段 | T-506, T-507 | 依据 `docs/ux-review.md` P2。⑤ 视觉分区:将高频「Shopee 更新安全 / 执行模式」前置成独立分区,低频「基础设施(Chrome路径/端口/DB路径)」独立后置;彻底隐藏 `test_item_id`、`dry_run` 用户入口,不再占用可见表单位置,但保留 `config.json` 内部兼容字段与③「检查本轮更新」语义。只改⑤设置页 UI 分区、保存/加载展示和 GUI 测试,不改配置 schema,不改 Shopee/CDP 更新流程 | DONE |
|
||||||
| T-518 | ②左栏提示词区组件密度优化 | T-302p, T-303, T-517 | 依据 `docs/ux-review.md` P2。② AI生成左栏保留高频「模板选择、编辑内容、保存当前模板/开始生成」可见入口;将封面模板低频操作「另存为、重命名、删除」收敛进一个「模板操作」菜单/小按钮,避免左栏按钮堆叠和误点。第一版不迁移到⑤设置,不改 prompts 数据结构,不改 AI 生成 HTTP、DB schema、Excel、Shopee/CDP 流程;删除仍保留现有二次确认,保存/重命名/删除行为和错误处理不变。同步 GUI 测试覆盖菜单入口、动作触发和旧低频按钮不再平铺 | DONE |
|
| T-518 | ②左栏提示词区组件密度优化 | T-302p, T-303, T-517 | 依据 `docs/ux-review.md` P2。② AI生成左栏保留高频「模板选择、编辑内容、保存当前模板/开始生成」可见入口;将封面模板低频操作「另存为、重命名、删除」收敛进一个「模板操作」菜单/小按钮,避免左栏按钮堆叠和误点。第一版不迁移到⑤设置,不改 prompts 数据结构,不改 AI 生成 HTTP、DB schema、Excel、Shopee/CDP 流程;删除仍保留现有二次确认,保存/重命名/删除行为和错误处理不变。同步 GUI 测试覆盖菜单入口、动作触发和旧低频按钮不再平铺 | DONE |
|
||||||
|
| T-519 | ②AI生成长任务进度条 + 用户可读滚动日志 | T-303, T-505, T-518 | 问题:批量生成几十/上百个标题或图片超时重试时,仅底部文字 `进度:标题x/y · 封面x/y · 失败z` 和技术型结构化日志不足以让用户判断剩余量、是否卡住、慢在哪一步。方案:②底部操作区增加标题/图片两条独立 `QProgressBar`,分两行显示 `标题 x/y`、`图片 x/y`,标题阶段结束后保持 100%,图片进度按本轮总任务口径显示并在旁边保留失败数;现有「AI生成运行日志」改为用户可读自动滚动日志,显示本轮开始、标题/图片开始与成功、商品ID/店铺、图片超时/重试、失败原因、停止请求和完成汇总。日志不得显示 API Key、密码、Cookie、token、完整请求体、base64 图片或超长 prompt;保留 DB `run_logs/run_log_events` 脱敏记录。只改②生成 UI、`GenerateWorker`/AI 生成事件文案和 GUI/AI 单测,不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE |
|
||||||
|
|
||||||
## 里程碑
|
## 里程碑
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+14
@@ -992,3 +992,17 @@
|
|||||||
- 范围控制:不迁移到⑤设置,不改 prompts 数据结构,不改 AI 生成 HTTP、DB schema、Excel 或 Shopee/CDP 流程。
|
- 范围控制:不迁移到⑤设置,不改 prompts 数据结构,不改 AI 生成 HTTP、DB schema、Excel 或 Shopee/CDP 流程。
|
||||||
- 测试:`tests/test_gui.py` 覆盖「模板操作」菜单入口、菜单 action 触发原有行为,以及旧低频按钮不再平铺。
|
- 测试:`tests/test_gui.py` 覆盖「模板操作」菜单入口、菜单 action 触发原有行为,以及旧低频按钮不再平铺。
|
||||||
- 验证:`python -m py_compile app\gui.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(68 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(156 tests)。Qt 仍有本机字体目录提示,不影响测试结果。
|
- 验证:`python -m py_compile app\gui.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(68 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(156 tests)。Qt 仍有本机字体目录提示,不影响测试结果。
|
||||||
|
## 【2026-07-02】T-519 文档启动 · ②AI生成长任务进度条与用户日志
|
||||||
|
|
||||||
|
- 背景:用户反馈批量生成几十/上百个标题或图片超时重试时耗时较长,需要在②AI生成里更清楚地看到剩余量、是否卡住、慢在哪一步。
|
||||||
|
- 文档:`docs/06-tasks.md` 新增 T-519 并标记 DOING,明确②底部增加标题/图片两条独立进度条,现有「AI生成运行日志」增强为用户可读自动滚动日志;日志展示本轮开始、标题/图片开始与成功、商品ID/店铺、图片超时/重试、失败原因、停止请求和完成汇总,同时禁止显示 API Key、密码、Cookie、token、完整请求体、base64 图片或超长 prompt。
|
||||||
|
- 范围:只改②生成 UI、`GenerateWorker`/AI 生成事件文案和测试;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程。
|
||||||
|
- 验证:文档-only 更新,未运行单元测试。
|
||||||
|
- 下一步:按 T-519 文档更新 `app/gui.py`、`app/ai.py` 和相关测试。
|
||||||
|
## 【2026-07-02】T-519 完成 · ②AI生成长任务进度条与用户日志
|
||||||
|
|
||||||
|
- 代码:`app/gui.py` 在②AI生成底部增加标题/图片两条独立 `QProgressBar`,分别显示 `标题 x/y`、`图片 x/y` 和失败数;保留原进度文本用于状态汇总;现有「AI生成运行日志」追加后自动滚到底部,加载最近一次日志时按时间顺序显示。
|
||||||
|
- 日志:`GenerateWorker` 将生成事件转为用户可读短句,显示本轮开始、标题/图片开始与成功、商品ID/店铺、调用失败重试、失败、停止请求和完成汇总;日志继续通过脱敏工具处理并写入 `run_logs/run_log_events`。
|
||||||
|
- AI事件:`app/ai.py` 的 `_call_with_retry()` 支持 `on_retry` 回调,标题/图片请求失败但仍会重试时上报 `retry` 事件;不改 AI HTTP 协议、DB schema、Excel 或 Shopee/CDP 流程。
|
||||||
|
- 测试:`tests/test_gui.py` 覆盖双进度条初始状态、用户可读日志、重试日志脱敏和旧日志加载;`tests/test_ai.py` 覆盖标题生成重试事件上报。
|
||||||
|
- 验证:`python -m py_compile app\ai.py app\gui.py tests\test_ai.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_ai.py"` 通过(7 tests);`python -m unittest discover -s tests -p "test_gui.py"` 通过(68 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(156 tests)。Qt 仍有本机字体目录提示,不影响测试结果。
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class AITests(TempDirMixin, unittest.TestCase):
|
|||||||
models_path = os.path.join(temp_dir, "ai_models.json")
|
models_path = os.path.join(temp_dir, "ai_models.json")
|
||||||
self._write_models(models_path)
|
self._write_models(models_path)
|
||||||
calls = []
|
calls = []
|
||||||
|
steps = []
|
||||||
|
|
||||||
def fake_urlopen(request, timeout=None):
|
def fake_urlopen(request, timeout=None):
|
||||||
calls.append((request, timeout))
|
calls.append((request, timeout))
|
||||||
@@ -112,10 +113,18 @@ class AITests(TempDirMixin, unittest.TestCase):
|
|||||||
"旧标题",
|
"旧标题",
|
||||||
config=self._config(),
|
config=self._config(),
|
||||||
models_path=models_path,
|
models_path=models_path,
|
||||||
|
on_step=steps.append,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual("新标题", title)
|
self.assertEqual("新标题", title)
|
||||||
self.assertEqual(2, len(calls))
|
self.assertEqual(2, len(calls))
|
||||||
|
retry_events = [step for step in steps if isinstance(step, dict)]
|
||||||
|
self.assertEqual(1, len(retry_events))
|
||||||
|
self.assertEqual("title_request", retry_events[0]["step"])
|
||||||
|
self.assertEqual("retry", retry_events[0]["result"])
|
||||||
|
self.assertEqual(1, retry_events[0]["attempt"])
|
||||||
|
self.assertEqual(2, retry_events[0]["attempts"])
|
||||||
|
self.assertNotIn("sk-text-secret", retry_events[0]["detail"])
|
||||||
body = json.loads(calls[-1][0].data.decode("utf-8"))
|
body = json.loads(calls[-1][0].data.decode("utf-8"))
|
||||||
self.assertEqual("text-model", body["model"])
|
self.assertEqual("text-model", body["model"])
|
||||||
self.assertEqual(0, body["temperature"])
|
self.assertEqual(0, body["temperature"])
|
||||||
@@ -143,6 +152,7 @@ class AITests(TempDirMixin, unittest.TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
calls = []
|
calls = []
|
||||||
|
steps = []
|
||||||
|
|
||||||
def fake_urlopen(request, timeout=None):
|
def fake_urlopen(request, timeout=None):
|
||||||
calls.append(request)
|
calls.append(request)
|
||||||
@@ -154,6 +164,7 @@ class AITests(TempDirMixin, unittest.TestCase):
|
|||||||
"旧标题",
|
"旧标题",
|
||||||
config=self._config(),
|
config=self._config(),
|
||||||
models_path=models_path,
|
models_path=models_path,
|
||||||
|
on_step=steps.append,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual("新标题", title)
|
self.assertEqual("新标题", title)
|
||||||
|
|||||||
+38
-4
@@ -17,7 +17,7 @@ if gui.QT_IMPORT_ERROR is not None:
|
|||||||
raise unittest.SkipTest("PySide6 未安装")
|
raise unittest.SkipTest("PySide6 未安装")
|
||||||
|
|
||||||
from PySide6.QtGui import QTextCursor
|
from PySide6.QtGui import QTextCursor
|
||||||
from PySide6.QtWidgets import QApplication, QLineEdit, QPlainTextEdit, QTableView
|
from PySide6.QtWidgets import QApplication, QLineEdit, QPlainTextEdit, QProgressBar, QTableView
|
||||||
|
|
||||||
from app.gui import (
|
from app.gui import (
|
||||||
AccountDialog,
|
AccountDialog,
|
||||||
@@ -696,6 +696,15 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertEqual("重置生成结果", tab.reset_generate_button.text())
|
self.assertEqual("重置生成结果", tab.reset_generate_button.text())
|
||||||
self.assertFalse(tab.stop_generate_button.isEnabled())
|
self.assertFalse(tab.stop_generate_button.isEnabled())
|
||||||
self.assertEqual("进度:标题0/0 · 封面0/0 · 失败0", tab.progress_label.text())
|
self.assertEqual("进度:标题0/0 · 封面0/0 · 失败0", tab.progress_label.text())
|
||||||
|
self.assertIsInstance(tab.title_progress_bar, QProgressBar)
|
||||||
|
self.assertIsInstance(tab.cover_progress_bar, QProgressBar)
|
||||||
|
self.assertEqual("标题 0/0", tab.title_progress_label.text())
|
||||||
|
self.assertEqual("图片 0/0", tab.cover_progress_label.text())
|
||||||
|
self.assertEqual("失败 0", tab.failed_progress_label.text())
|
||||||
|
self.assertEqual(0, tab.title_progress_bar.value())
|
||||||
|
self.assertEqual(0, tab.cover_progress_bar.value())
|
||||||
|
self.assertEqual(1, tab.title_progress_bar.maximum())
|
||||||
|
self.assertEqual(1, tab.cover_progress_bar.maximum())
|
||||||
self.assertEqual("默认", tab.cover_template_combo.currentText())
|
self.assertEqual("默认", tab.cover_template_combo.currentText())
|
||||||
self.assertEqual("模板操作", tab.cover_template_actions_button.text())
|
self.assertEqual("模板操作", tab.cover_template_actions_button.text())
|
||||||
self.assertIs(tab.cover_template_actions_menu, tab.cover_template_actions_button.menu())
|
self.assertIs(tab.cover_template_actions_menu, tab.cover_template_actions_button.menu())
|
||||||
@@ -905,6 +914,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
tasks = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
|
tasks = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
|
||||||
progress = []
|
progress = []
|
||||||
rows = []
|
rows = []
|
||||||
|
logs = []
|
||||||
|
|
||||||
def fake_generate_batch(tasks_arg, prompt_values, ai_cfg=None, on_progress=None, should_stop=None):
|
def fake_generate_batch(tasks_arg, prompt_values, ai_cfg=None, on_progress=None, should_stop=None):
|
||||||
self.assertEqual(tasks, tasks_arg)
|
self.assertEqual(tasks, tasks_arg)
|
||||||
@@ -919,10 +929,22 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
{
|
{
|
||||||
"task": tasks[0],
|
"task": tasks[0],
|
||||||
"phase": "cover",
|
"phase": "cover",
|
||||||
"step": "cover_request",
|
"step": "cover_submit",
|
||||||
"result": "start",
|
"result": "start",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
ai_cfg["on_event"](
|
||||||
|
{
|
||||||
|
"task": tasks[0],
|
||||||
|
"phase": "cover",
|
||||||
|
"step": "cover_request",
|
||||||
|
"result": "retry",
|
||||||
|
"attempt": 1,
|
||||||
|
"attempts": 3,
|
||||||
|
"detail": "timeout token=SECRET-TOKEN",
|
||||||
|
"level": "warning",
|
||||||
|
}
|
||||||
|
)
|
||||||
ai_cfg["on_task_update"](tasks[0].id, {"stage": "generated"})
|
ai_cfg["on_task_update"](tasks[0].id, {"stage": "generated"})
|
||||||
return {"ok": True, "total": 1, "title_done": 1, "cover_done": 1, "failed": 0}
|
return {"ok": True, "total": 1, "title_done": 1, "cover_done": 1, "failed": 0}
|
||||||
|
|
||||||
@@ -934,6 +956,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
)
|
)
|
||||||
worker.progress.connect(progress.append)
|
worker.progress.connect(progress.append)
|
||||||
worker.row_updated.connect(lambda task_id, fields: rows.append((task_id, fields)))
|
worker.row_updated.connect(lambda task_id, fields: rows.append((task_id, fields)))
|
||||||
|
worker.log.connect(logs.append)
|
||||||
|
|
||||||
with mock.patch("app.gui.ai.generate_batch", side_effect=fake_generate_batch):
|
with mock.patch("app.gui.ai.generate_batch", side_effect=fake_generate_batch):
|
||||||
summary = worker.execute()
|
summary = worker.execute()
|
||||||
@@ -945,8 +968,17 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertEqual([(tasks[0].id, {"stage": "generated"})], rows)
|
self.assertEqual([(tasks[0].id, {"stage": "generated"})], rows)
|
||||||
run_log = db.list_run_logs(limit=1, run_type="generate", path=cfg["db_path"])[0]
|
run_log = db.list_run_logs(limit=1, run_type="generate", path=cfg["db_path"])[0]
|
||||||
self.assertEqual(summary["run_id"], run_log.id)
|
self.assertEqual(summary["run_id"], run_log.id)
|
||||||
|
joined_logs = "\n".join(logs)
|
||||||
|
self.assertIn("[开始] 本轮生成 1 条", joined_logs)
|
||||||
|
self.assertIn("[图片] 1/1 商品 51100639510", joined_logs)
|
||||||
|
self.assertIn("准备重试 1/2", joined_logs)
|
||||||
|
self.assertIn("token=***", joined_logs)
|
||||||
|
self.assertNotIn("SECRET-TOKEN", joined_logs)
|
||||||
|
self.assertIn("[完成] AI 生成完成:标题1/1,图片1/1,失败0", joined_logs)
|
||||||
events = db.list_run_log_events(summary["run_id"], path=cfg["db_path"])
|
events = db.list_run_log_events(summary["run_id"], path=cfg["db_path"])
|
||||||
self.assertTrue(any("phase=cover step=cover_request result=start" in event.message for event in events))
|
event_messages = "\n".join(event.message for event in events)
|
||||||
|
self.assertIn("[图片] 1/1 商品 51100639510", event_messages)
|
||||||
|
self.assertIn("准备重试 1/2", event_messages)
|
||||||
|
|
||||||
self.assert_removed(temp_dir)
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
@@ -1010,7 +1042,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
|
|
||||||
events = db.list_run_log_events(summary["run_id"], path=cfg["db_path"])
|
events = db.list_run_log_events(summary["run_id"], path=cfg["db_path"])
|
||||||
messages = "\n".join(event.message for event in events)
|
messages = "\n".join(event.message for event in events)
|
||||||
self.assertIn("phase=cover step=cover_request result=failed", messages)
|
self.assertIn("[失败] 商品 51100639510", messages)
|
||||||
|
self.assertIn("图片生成失败", messages)
|
||||||
self.assertIn("token=***", messages)
|
self.assertIn("token=***", messages)
|
||||||
self.assertNotIn("SECRET-TOKEN", messages)
|
self.assertNotIn("SECRET-TOKEN", messages)
|
||||||
|
|
||||||
@@ -1596,6 +1629,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
close_flags = []
|
close_flags = []
|
||||||
progress = []
|
progress = []
|
||||||
rows = []
|
rows = []
|
||||||
|
logs = []
|
||||||
|
|
||||||
def fake_apply(account, task, close_success_tab=False, on_step=None):
|
def fake_apply(account, task, close_success_tab=False, on_step=None):
|
||||||
applied_aliases.append(account.alias)
|
applied_aliases.append(account.alias)
|
||||||
|
|||||||
Reference in New Issue
Block a user