feat: complete T-528 cmhub billing feedback

This commit is contained in:
chengma
2026-07-04 17:14:29 +08:00
parent 27f23a4738
commit 3dbcbab799
7 changed files with 377 additions and 20 deletions
+48 -2
View File
@@ -102,6 +102,8 @@ class GenerateTab(QWidget):
self.cover_progress_bar.setValue(0)
self.failed_progress_label = QLabel("失败 0")
self.failed_progress_label.setObjectName("generateFailedProgressLabel")
self.cmhub_balance_label = QLabel("cmhub余额:未获取")
self.cmhub_balance_label.setObjectName("generateCmhubBalanceLabel")
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
@@ -196,6 +198,7 @@ class GenerateTab(QWidget):
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)
cover_progress_layout.addWidget(self.cmhub_balance_label)
progress_layout = QVBoxLayout()
progress_layout.addLayout(title_progress_layout)
progress_layout.addLayout(cover_progress_layout)
@@ -239,6 +242,7 @@ class GenerateTab(QWidget):
self.refresh_cover_templates()
self.refresh_tasks()
self._show_current_run_log_empty()
self._update_cmhub_balance_label({})
def _set_status(self, message):
if self.status_callback is not None:
@@ -278,7 +282,7 @@ class GenerateTab(QWidget):
payload = {
key: value
for key, value in self.config.items()
if key not in {"config_path", "ai_models_path"}
if key not in {"config_path", "ai_models_path", "cmhub_config_path"}
}
payload["ai"] = ai_settings
try:
@@ -289,7 +293,7 @@ class GenerateTab(QWidget):
internal = {
key: value
for key, value in self.config.items()
if key in {"config_path", "ai_models_path"}
if key in {"config_path", "ai_models_path", "cmhub_config_path"}
}
self.config.clear()
self.config.update(saved)
@@ -489,6 +493,7 @@ class GenerateTab(QWidget):
worker.finished.connect(self._on_generate_finished)
worker.cancelled.connect(self._on_generate_cancelled)
self._reset_generate_run_log(tasks, generate_cover)
self._reset_cmhub_balance_label()
thread = run_worker(worker, thread_name="GenerateWorker", start=False)
thread.finished.connect(lambda: self._forget_generate_thread(thread))
self.generate_worker = worker
@@ -640,6 +645,7 @@ class GenerateTab(QWidget):
def _on_generate_progress(self, payload):
self._update_generate_progress(payload)
self._update_cmhub_balance_label(payload)
self._set_status("生成进度:" + self._generate_progress_text(payload))
def _on_generate_row_updated(self, task_id, fields):
@@ -652,6 +658,11 @@ class GenerateTab(QWidget):
self._set_generate_running(False)
self.refresh_tasks()
self._update_generate_progress(payload)
self._update_cmhub_balance_label(payload)
billing_error = payload.get("billing_error")
if billing_error:
self._show_billing_error(billing_error)
return
if payload.get("error"):
self._set_status(f"AI 生成失败:{payload.get('error')}")
return
@@ -661,6 +672,11 @@ class GenerateTab(QWidget):
self._set_generate_running(False)
self.refresh_tasks()
self._update_generate_progress(payload)
self._update_cmhub_balance_label(payload)
billing_error = payload.get("billing_error")
if billing_error:
self._show_billing_error(billing_error)
return
self._set_status("AI 生成已停止:" + self._generate_progress_text(payload))
def _update_generate_progress(self, payload):
@@ -676,6 +692,36 @@ class GenerateTab(QWidget):
self._set_progress_bar(self.title_progress_bar, title_done, total)
self._set_progress_bar(self.cover_progress_bar, cover_done, cover_total)
def _reset_cmhub_balance_label(self):
if self._is_cmhub_backend():
self.cmhub_balance_label.setVisible(True)
self.cmhub_balance_label.setText("cmhub余额:生成后刷新")
else:
self.cmhub_balance_label.setVisible(False)
def _update_cmhub_balance_label(self, payload):
if not self._is_cmhub_backend():
self.cmhub_balance_label.setVisible(False)
return
self.cmhub_balance_label.setVisible(True)
balance = payload.get("points_balance") if isinstance(payload, dict) else None
if balance is None:
if not self.cmhub_balance_label.text():
self.cmhub_balance_label.setText("cmhub余额:未获取")
return
self.cmhub_balance_label.setText(f"cmhub余额:{balance}")
def _is_cmhub_backend(self):
try:
return appconfig.ai_config(self.config).get("backend") == "cmhub"
except Exception:
return False
def _show_billing_error(self, billing_error):
message = billing_error.get("message") or "点数不足,请先充值。"
QMessageBox.warning(self, "AI生成", message)
self._set_status("AI 生成已中止:" + message)
def _set_progress_bar(self, bar, done, total):
maximum = max(1, int(total or 0))
value = min(max(0, int(done or 0)), maximum)
+85 -6
View File
@@ -24,6 +24,10 @@ class GenerateWorker(BaseWorker):
self._account_by_alias = {}
self._task_positions = {}
self._eligible_total = 0
self._last_progress_payload = {}
self._cmhub_points_balance = None
self._billing_error = None
self._billing_stop_requested = False
def execute(self):
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
@@ -72,8 +76,8 @@ class GenerateWorker(BaseWorker):
"on_error": self._on_generation_error,
"generate_cover": generate_cover,
},
on_progress=self.progress.emit,
should_stop=self.should_cancel,
on_progress=self._emit_generate_progress,
should_stop=self._should_stop_generation,
)
except Exception as exc:
error = diagnostics.redact_log_text(str(exc) or exc.__class__.__name__)
@@ -100,30 +104,59 @@ class GenerateWorker(BaseWorker):
payload={"error": error},
exc=exc,
)
if self._cmhub_points_balance is not None:
summary["points_balance"] = self._cmhub_points_balance
if self._billing_error is not None:
summary["billing_error"] = dict(self._billing_error)
summary["ok"] = False
summary["cancelled"] = True
summary["run_id"] = self._run_id
summary["batch_ids"] = batch_ids
status = "cancelled" if summary.get("cancelled") else "done"
level = "warning" if summary.get("cancelled") or summary.get("error") else "info"
status = "failed" if summary.get("billing_error") or summary.get("error") else ("cancelled" if summary.get("cancelled") else "done")
level = "error" if summary.get("billing_error") or summary.get("error") else ("warning" if summary.get("cancelled") else "info")
self._log_run_event(self._format_generate_completion(summary), level=level)
self._finish_run_log(status, summary)
return summary
def _emit_generate_progress(self, payload):
progress = dict(payload or {})
if self._cmhub_points_balance is not None:
progress["points_balance"] = self._cmhub_points_balance
if self._billing_error is not None:
progress["billing_error"] = dict(self._billing_error)
self._last_progress_payload = dict(progress)
self.progress.emit(progress)
def _should_stop_generation(self):
return self.should_cancel() or self._billing_stop_requested
def _emit_row_update(self, task_id, fields):
self.row_updated.emit(int(task_id), dict(fields or {}))
def _on_generation_event(self, payload):
task = payload.get("task")
self._remember_cmhub_metadata(payload)
message = self._format_generation_event(payload)
if not message:
return
self._log_run_event(message, task=task, level=payload.get("level") or "info")
def _remember_cmhub_metadata(self, payload):
metadata = payload.get("metadata")
if not isinstance(metadata, dict):
return
if metadata.get("points_balance") is not None:
self._cmhub_points_balance = metadata.get("points_balance")
self._emit_generate_progress(self._last_progress_payload)
def _format_generation_event(self, payload):
task = payload.get("task")
phase = payload.get("phase") or "generate"
step = payload.get("step") or "unknown"
result = payload.get("result") or "start"
detail = self._short_detail(payload.get("detail"))
if isinstance(payload.get("metadata"), dict):
return self._format_cmhub_billing_event(task, phase, payload.get("metadata"))
if phase == "title":
if result == "start" and step == "title_submit":
return f"[标题] {self._task_progress_label(task)} 开始生成"
@@ -154,6 +187,22 @@ class GenerateWorker(BaseWorker):
return None
return None
def _format_cmhub_billing_event(self, task, phase, metadata):
label = "标题" if phase == "title" else ("图片" if phase == "cover" else "AI")
parts = []
alias = metadata.get("alias") or metadata.get("model_used")
if alias:
parts.append(f"别名 {alias}")
if metadata.get("points_cost") is not None:
parts.append(f"扣点 {metadata.get('points_cost')}")
if metadata.get("points_balance") is not None:
parts.append(f"余额 {metadata.get('points_balance')}")
if metadata.get("call_id"):
parts.append(f"call_id={metadata.get('call_id')}")
if not parts:
return None
return f"[计费] {self._task_plain_label(task)} {label}生成:" + ",".join(str(part) for part in parts)
def _retry_message(self, label, task, payload, detail):
attempt = int(payload.get("attempt", 0) or 0)
attempts = int(payload.get("attempts", 0) or 0)
@@ -192,6 +241,9 @@ class GenerateWorker(BaseWorker):
def _format_generate_completion(self, summary):
progress = self._summary_text(summary)
billing_error = summary.get("billing_error") or {}
if billing_error:
return f"[失败] AI 生成已中止:{billing_error.get('message') or '点数不足,请先充值'},{progress}"
if summary.get("cancelled"):
return f"[停止] AI 生成已停止:{progress}"
if summary.get("error"):
@@ -212,14 +264,40 @@ class GenerateWorker(BaseWorker):
task = payload.get("task")
phase = payload.get("phase") or "generate"
step = payload.get("step") or "unknown"
exception = payload.get("exception")
code = payload.get("code") or getattr(exception, "code", None)
status = payload.get("status") or getattr(exception, "status", None)
error = diagnostics.redact_log_text(payload.get("error") or "未知错误")
if str(code or "") == "insufficient_points":
self._billing_stop_requested = True
message = "点数不足,请先充值。本轮未开始任务将停止。"
self._billing_error = {
"code": "insufficient_points",
"message": message,
"phase": phase,
"task_id": getattr(task, "id", None),
"item_id": getattr(task, "item_id", None),
}
if status is not None:
self._billing_error["status"] = status
self._log_run_event(
f"[计费] {self._task_plain_label(task)} 点数不足,请先充值;本轮未开始任务将停止",
task=task,
level="error",
)
self._emit_generate_progress(self._last_progress_payload)
diagnostic_payload = {"phase": phase, "error": error}
if code is not None:
diagnostic_payload["code"] = str(code)
if status is not None:
diagnostic_payload["status"] = status
self._write_diagnostic_log(
"AI生成任务失败",
level="ERROR",
step=step,
task=task,
payload={"phase": phase, "error": error},
exc=payload.get("exception"),
payload=diagnostic_payload,
exc=exception,
)
def _batch_ids(self, tasks):
@@ -245,6 +323,7 @@ class GenerateWorker(BaseWorker):
"title_concurrency": ai_cfg.get("title_concurrency"),
"image_concurrency": ai_cfg.get("image_concurrency"),
"generate_cover": ai_cfg.get("generate_cover", False),
"backend": ai_cfg.get("backend", "direct"),
},
path=self.db_path,
)