feat: polish generate run logs
This commit is contained in:
+108
-11
@@ -2,7 +2,18 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as _dt
|
||||
import re
|
||||
|
||||
from .widgets import *
|
||||
|
||||
|
||||
_USER_LOG_URL_RE = re.compile(r"https?://[^\s,,;;))\]]+", re.IGNORECASE)
|
||||
_USER_LOG_PATH_RE = re.compile(
|
||||
r"(?i)(/api(?:/v\d+)?/[^\s,,;;))\]]*|/generated/images/[^\s,,;;))\]]*)"
|
||||
)
|
||||
|
||||
|
||||
class GenerateWorker(BaseWorker):
|
||||
"""Generate titles and covers for eligible collected or failed generation tasks."""
|
||||
|
||||
@@ -28,8 +39,12 @@ class GenerateWorker(BaseWorker):
|
||||
self._cmhub_points_balance = None
|
||||
self._billing_error = None
|
||||
self._billing_stop_requested = False
|
||||
self._run_started_at_text = ""
|
||||
self._run_started_monotonic = None
|
||||
|
||||
def execute(self):
|
||||
self._run_started_at_text = self._format_local_time()
|
||||
self._run_started_monotonic = time.monotonic()
|
||||
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
||||
account_by_alias = {
|
||||
str(account.alias).strip(): account
|
||||
@@ -57,7 +72,7 @@ class GenerateWorker(BaseWorker):
|
||||
if generate_cover:
|
||||
if appconfig.ai_backend(self.config) == "cmhub":
|
||||
concurrency = ai.cmhub_image_concurrency_plan(ai_cfg)
|
||||
start_message = "[开始] 本轮生成 {total} 条:标题{title_total},图片{cover_total};标题并发{title_concurrency},图片并发{image_concurrency},cmhub实际生图并发{request_concurrency},下载并发{download_concurrency}".format(
|
||||
start_message = "[开始] 本轮生成 {total} 条:标题{title_total},图片{cover_total};标题并发{title_concurrency},图片并发{image_concurrency},cmhub实际生图并发{request_concurrency},下载并发{download_concurrency};开始时间 {started_at}".format(
|
||||
total=len(eligible),
|
||||
title_total=component_totals["title_total"],
|
||||
cover_total=component_totals["cover_total"],
|
||||
@@ -65,20 +80,23 @@ class GenerateWorker(BaseWorker):
|
||||
image_concurrency=concurrency["configured_image_concurrency"],
|
||||
request_concurrency=concurrency["request_concurrency"],
|
||||
download_concurrency=concurrency["download_concurrency"],
|
||||
started_at=self._run_started_at_text,
|
||||
)
|
||||
else:
|
||||
start_message = "[开始] 本轮生成 {total} 条:标题{title_total},图片{cover_total};标题并发{title_concurrency},图片并发{image_concurrency}".format(
|
||||
start_message = "[开始] 本轮生成 {total} 条:标题{title_total},图片{cover_total};标题并发{title_concurrency},图片并发{image_concurrency};开始时间 {started_at}".format(
|
||||
total=len(eligible),
|
||||
title_total=component_totals["title_total"],
|
||||
cover_total=component_totals["cover_total"],
|
||||
title_concurrency=ai_cfg.get("title_concurrency", 1),
|
||||
image_concurrency=ai_cfg.get("image_concurrency", 1),
|
||||
started_at=self._run_started_at_text,
|
||||
)
|
||||
else:
|
||||
start_message = "[开始] 本轮生成 {total} 条:本轮仅生成标题,不生成图片;标题{title_total};标题并发{title_concurrency}".format(
|
||||
start_message = "[开始] 本轮生成 {total} 条:本轮仅生成标题,不生成图片;标题{title_total};标题并发{title_concurrency};开始时间 {started_at}".format(
|
||||
total=len(eligible),
|
||||
title_total=component_totals["title_total"],
|
||||
title_concurrency=ai_cfg.get("title_concurrency", 1),
|
||||
started_at=self._run_started_at_text,
|
||||
)
|
||||
self._log_run_event(start_message)
|
||||
try:
|
||||
@@ -113,8 +131,9 @@ class GenerateWorker(BaseWorker):
|
||||
"cancelled": self.should_cancel(),
|
||||
"generate_cover": generate_cover,
|
||||
}
|
||||
user_error = self._user_log_detail(error) or "未知错误"
|
||||
self._log_run_event(
|
||||
f"[失败] AI 生成运行失败:{error}",
|
||||
f"[失败] AI 生成运行失败:{user_error}",
|
||||
level="error",
|
||||
)
|
||||
self._write_diagnostic_log(
|
||||
@@ -179,7 +198,13 @@ class GenerateWorker(BaseWorker):
|
||||
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"))
|
||||
detail = self._user_log_detail(
|
||||
payload.get("detail"),
|
||||
phase=phase,
|
||||
step=step,
|
||||
code=payload.get("code"),
|
||||
status=payload.get("status"),
|
||||
)
|
||||
if isinstance(payload.get("metadata"), dict):
|
||||
return self._format_cmhub_billing_event(task, phase, payload.get("metadata"))
|
||||
if phase == "title":
|
||||
@@ -206,7 +231,7 @@ class GenerateWorker(BaseWorker):
|
||||
if result == "start" and step == "cover_submit":
|
||||
return f"[图片] {self._task_progress_label(task)} 开始生成"
|
||||
if result == "success" and step == "cover_request":
|
||||
return f"[图片] {self._task_progress_label(task)} {detail or 'cmhub 已返回,耗时未知'}"
|
||||
return f"[图片] {self._task_progress_label(task)} {detail or 'cmhub 已返回图片,耗时未知'}"
|
||||
if result == "warning" and step == "cover_download":
|
||||
return f"[图片] {self._task_progress_label(task)} {detail or '图片下载较慢'}"
|
||||
if result == "success" and step == "cover_download":
|
||||
@@ -214,7 +239,7 @@ class GenerateWorker(BaseWorker):
|
||||
if result == "success" and step == "cover_save":
|
||||
return f"[图片] {self._task_progress_label(task)} 本地保存完成,{detail or '耗时未知'}"
|
||||
if result == "success" and step == "db_write":
|
||||
suffix = f",已保存 {detail}" if detail else ""
|
||||
suffix = ",已保存到本地" if detail else ""
|
||||
return f"[图片] {self._task_progress_label(task)} 成功{suffix}"
|
||||
if result == "retry":
|
||||
return self._retry_message("图片", task, payload, detail)
|
||||
@@ -285,16 +310,88 @@ class GenerateWorker(BaseWorker):
|
||||
return text[:1997] + "..."
|
||||
return text
|
||||
|
||||
def _user_log_detail(self, detail, phase=None, step=None, code=None, status=None):
|
||||
if detail is None:
|
||||
return ""
|
||||
raw = diagnostics.redact_log_text(str(detail)).replace("\r", " ").replace("\n", " ").strip()
|
||||
lowered = raw.lower()
|
||||
code_text = str(code or "").strip().lower()
|
||||
if code_text == "insufficient_points" or "点数不足" in raw:
|
||||
return "点数不足,请先充值"
|
||||
if code_text == "unauthorized" or "api key 无效" in raw.lower():
|
||||
return "cmhub API Key 无效,请去⑤设置重填"
|
||||
if code_text in {"model_not_allowed", "no_pricing_rule"} or "模型别名" in raw or "模型配置不可用" in raw:
|
||||
return "cmhub 模型别名不可用,请去⑤设置刷新别名并保存"
|
||||
if code_text == "content_blocked" or "内容安全" in raw:
|
||||
return "cmhub 内容安全策略拒绝本次生成"
|
||||
if code_text == "rate_limited" or "rate_limited" in lowered or "请求过于频繁" in raw:
|
||||
return "cmhub 请求过于频繁,请稍后重试"
|
||||
if code_text == "connect_timeout" or "连接 cmhub 超时" in raw:
|
||||
return "连接 cmhub 超时,请检查网络或稍后重试"
|
||||
if code_text == "read_timeout" or "等待 cmhub 返回超时" in raw:
|
||||
return "等待 cmhub 返回超时,本条已失败;可稍后重试"
|
||||
if (
|
||||
code_text == "not_found"
|
||||
or "not_found" in lowered
|
||||
or "接口不存在" in raw
|
||||
or str(status or "") == "404"
|
||||
):
|
||||
return "cmhub 网关接口不可用,请检查⑤设置中的 Base URL,或联系服务方确认网关版本"
|
||||
if code_text == "upstream_error" or "upstream_error" in lowered or "上游" in raw:
|
||||
return "cmhub 上游生成失败,请稍后重试"
|
||||
if "下载 cmhub 图片失败" in raw:
|
||||
return "下载 cmhub 图片失败,请检查网络后稍后重试"
|
||||
|
||||
text = raw.replace("image_url", "图片")
|
||||
text = text.replace("返回 图片", "返回图片")
|
||||
text = _USER_LOG_URL_RE.sub("[链接已隐藏]", text)
|
||||
text = _USER_LOG_PATH_RE.sub("[接口路径已隐藏]", text)
|
||||
text = text.replace("GET [链接已隐藏]", "请求 cmhub")
|
||||
text = text.replace("POST [链接已隐藏]", "请求 cmhub")
|
||||
if len(text) > 180:
|
||||
return text[:177] + "..."
|
||||
return text
|
||||
|
||||
def _format_generate_completion(self, summary):
|
||||
progress = self._summary_text(summary)
|
||||
billing_error = summary.get("billing_error") or {}
|
||||
finished_at = self._format_local_time()
|
||||
elapsed = self._format_run_elapsed()
|
||||
suffix = f";{self._completion_time_label(summary)} {finished_at},总用时 {elapsed}"
|
||||
if billing_error:
|
||||
return f"[失败] AI 生成已中止:{billing_error.get('message') or '点数不足,请先充值'},{progress}"
|
||||
message = self._user_log_detail(billing_error.get("message") or "点数不足,请先充值") or "点数不足,请先充值"
|
||||
return f"[失败] AI 生成已中止:{message},{progress}{suffix}"
|
||||
if summary.get("cancelled"):
|
||||
return f"[停止] AI 生成已停止:{progress}"
|
||||
return f"[停止] AI 生成已停止:{progress}{suffix}"
|
||||
if summary.get("error"):
|
||||
return f"[失败] AI 生成失败:{summary.get('error')},{progress}"
|
||||
return f"[完成] AI 生成完成:{progress}"
|
||||
error = self._user_log_detail(summary.get("error")) or "未知错误"
|
||||
return f"[失败] AI 生成失败:{error},{progress}{suffix}"
|
||||
return f"[完成] AI 生成完成:{progress}{suffix}"
|
||||
|
||||
def _completion_time_label(self, summary):
|
||||
if summary.get("billing_error") or summary.get("error"):
|
||||
return "失败时间"
|
||||
if summary.get("cancelled"):
|
||||
return "停止时间"
|
||||
return "完成时间"
|
||||
|
||||
def _format_local_time(self):
|
||||
return _dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def _format_run_elapsed(self):
|
||||
if self._run_started_monotonic is None:
|
||||
return "0秒"
|
||||
return self._format_duration(time.monotonic() - self._run_started_monotonic)
|
||||
|
||||
def _format_duration(self, seconds):
|
||||
total = max(0, int(seconds or 0))
|
||||
hours, remainder = divmod(total, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
if hours:
|
||||
return f"{hours}小时{minutes:02d}分{seconds:02d}秒"
|
||||
if minutes:
|
||||
return f"{minutes}分{seconds:02d}秒"
|
||||
return f"{seconds}秒"
|
||||
|
||||
def _summary_text(self, summary):
|
||||
title_total = summary.get("title_total", summary.get("total", 0))
|
||||
|
||||
Reference in New Issue
Block a user