feat(product-suite): add global generation history

This commit is contained in:
chengma
2026-07-17 09:59:55 +08:00
parent f1dc7f28dc
commit e232097ba5
13 changed files with 1409 additions and 52 deletions
+164
View File
@@ -128,6 +128,29 @@ class ImageStudioGenerationRound:
is_legacy: bool
@dataclass(frozen=True)
class ImageStudioHistoryRound:
"""A persisted generation round with enough project context for global history."""
project_id: int
account_alias: str
account_name: Optional[str]
item_id: str
binding_state: str
generation_round_key: Optional[str]
created_at: Optional[str]
updated_at: Optional[str]
job_count: int
slot_count: int
succeeded_count: int
failed_count: int
cancelled_count: int
active_count: int
retry_count: int
is_current: bool
is_legacy: bool
class ImageStudioError(RuntimeError):
"""Raised when the AI image studio service cannot complete an operation."""
@@ -1296,6 +1319,147 @@ def list_generation_rounds(project_id, *, limit=None, offset=0, path=None, conn=
return rounds
def list_global_generation_rounds(
*,
account_query="",
item_query="",
project_id=None,
limit=None,
offset=0,
path=None,
conn=None,
):
"""List persisted generation rounds across active projects without loading assets."""
try:
offset = max(0, int(offset or 0))
except (TypeError, ValueError) as exc:
raise db.DbError("全局历史偏移量无效") from exc
if limit is not None:
try:
limit = int(limit)
except (TypeError, ValueError) as exc:
raise db.DbError("全局历史数量无效") from exc
if limit <= 0:
return []
clauses = ["projects.deleted_at IS NULL"]
params = []
if project_id is not None:
try:
clauses.append("projects.id = ?")
params.append(int(project_id))
except (TypeError, ValueError) as exc:
raise db.DbError("当前商品项目无效") from exc
account_text = str(account_query or "").strip()
if account_text:
pattern = "%%%s%%" % account_text
clauses.append(
"(projects.account_alias LIKE ? OR COALESCE(projects.account_name, '') LIKE ?)"
)
params.extend([pattern, pattern])
item_text = str(item_query or "").strip()
if item_text:
clauses.append("projects.item_id LIKE ?")
params.append("%%%s%%" % item_text)
sql = """
WITH latest_round_jobs AS (
SELECT project_id,
generation_round_key,
generation_slot_index,
MAX(id) AS latest_id
FROM image_studio_jobs
WHERE generation_round_key IS NOT NULL
GROUP BY project_id, generation_round_key, generation_slot_index
),
effective_jobs AS (
SELECT jobs.*
FROM image_studio_jobs AS jobs
LEFT JOIN latest_round_jobs AS latest
ON latest.latest_id = jobs.id
WHERE jobs.generation_round_key IS NULL OR latest.latest_id IS NOT NULL
),
round_attempts AS (
SELECT project_id,
generation_round_key,
COUNT(*) AS attempt_count
FROM image_studio_jobs
GROUP BY project_id, generation_round_key
)
SELECT projects.id AS project_id,
projects.account_alias AS account_alias,
projects.account_name AS account_name,
projects.item_id AS item_id,
projects.binding_state AS binding_state,
jobs.generation_round_key AS generation_round_key,
MIN(jobs.created_at) AS created_at,
MAX(jobs.updated_at) AS updated_at,
COUNT(*) AS job_count,
COUNT(DISTINCT jobs.generation_slot_index) AS slot_count,
MAX(attempts.attempt_count) AS attempt_count,
SUM(CASE WHEN jobs.status = 'succeeded' THEN 1 ELSE 0 END) AS succeeded_count,
SUM(CASE WHEN jobs.status IN ('failed', 'expired') THEN 1 ELSE 0 END) AS failed_count,
SUM(CASE WHEN jobs.status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled_count,
SUM(CASE WHEN jobs.status IN ('pending', 'submitted', 'running') THEN 1 ELSE 0 END) AS active_count,
CASE
WHEN jobs.generation_round_key IS NOT NULL
AND jobs.generation_round_key = projects.current_generation_round_key
THEN 1 ELSE 0
END AS is_current,
CASE WHEN jobs.generation_round_key IS NULL THEN 1 ELSE 0 END AS is_legacy
FROM image_studio_projects AS projects
INNER JOIN effective_jobs AS jobs ON jobs.project_id = projects.id
INNER JOIN round_attempts AS attempts
ON attempts.project_id = jobs.project_id
AND attempts.generation_round_key IS jobs.generation_round_key
WHERE %s
GROUP BY projects.id, jobs.generation_round_key
ORDER BY MIN(jobs.created_at) DESC,
MAX(jobs.id) DESC
""" % " AND ".join(clauses)
if limit is not None:
sql += " LIMIT ? OFFSET ?"
params.extend([limit, offset])
elif offset:
sql += " LIMIT -1 OFFSET ?"
params.append(offset)
with _connection(conn, path) as database:
rows = database.execute(sql, params).fetchall()
rounds = []
for row in rows:
round_key = row["generation_round_key"]
job_count = int(row["job_count"] or 0)
slot_count = int(row["slot_count"] or 0)
rounds.append(
ImageStudioHistoryRound(
project_id=int(row["project_id"]),
account_alias=str(row["account_alias"] or ""),
account_name=row["account_name"],
item_id=str(row["item_id"] or ""),
binding_state=str(row["binding_state"] or ""),
generation_round_key=round_key,
created_at=row["created_at"],
updated_at=row["updated_at"],
job_count=job_count,
slot_count=slot_count,
succeeded_count=int(row["succeeded_count"] or 0),
failed_count=int(row["failed_count"] or 0),
cancelled_count=int(row["cancelled_count"] or 0),
active_count=int(row["active_count"] or 0),
retry_count=(
max(0, int(row["attempt_count"] or 0) - job_count)
if round_key
else 0
),
is_current=bool(row["is_current"]),
is_legacy=bool(row["is_legacy"]),
)
)
return rounds
def list_generation_round_current_jobs(project_id, generation_round_key, path=None, conn=None):
project_id = int(project_id)
if generation_round_key is None: