feat(product-suite): add direct job state machine

This commit is contained in:
chengma
2026-07-20 18:12:44 +08:00
parent 2a2cbad7bd
commit aa000ff5e6
11 changed files with 820 additions and 101 deletions
+115 -6
View File
@@ -33,6 +33,14 @@ JOB_RECOVERY_ACTIONS = {
JOB_RECOVERY_RESUME,
JOB_RECOVERY_REGENERATE,
}
GENERATION_SOURCE_CMHUB = "cmhub"
GENERATION_SOURCE_DIRECT = "direct"
PROVIDER_CMHUB = "cmhub"
PROVIDER_OPENAI_IMAGES_EDITS = "openai_images_edits"
GENERATION_SOURCE_PROVIDERS = {
GENERATION_SOURCE_CMHUB: PROVIDER_CMHUB,
GENERATION_SOURCE_DIRECT: PROVIDER_OPENAI_IMAGES_EDITS,
}
SELECTION_TYPES = {"main", "detail"}
@@ -65,6 +73,9 @@ class ImageStudioAsset:
remote_url: Optional[str]
local_path: Optional[str]
aspect_ratio: Optional[str]
requested_output_size: Optional[str]
rendered_width: Optional[int]
rendered_height: Optional[int]
parent_asset_id: Optional[int]
prompt: Optional[str]
status: str
@@ -95,6 +106,7 @@ class ImageStudioJob:
call_id: Optional[str]
generation_round_key: Optional[str]
generation_slot_index: Optional[int]
run_session_id: Optional[str]
created_at: str
updated_at: str
submitted_at: Optional[str]
@@ -705,6 +717,9 @@ def add_asset(
remote_url=None,
local_path=None,
aspect_ratio=None,
requested_output_size=None,
rendered_width=None,
rendered_height=None,
parent_asset_id=None,
prompt=None,
status=ASSET_STATUS_AVAILABLE,
@@ -720,8 +735,9 @@ def add_asset(
"""
INSERT INTO image_studio_assets
(project_id, kind, remote_url, local_path, aspect_ratio,
requested_output_size, rendered_width, rendered_height,
parent_asset_id, prompt, status, source_order, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
int(project_id),
@@ -729,6 +745,9 @@ def add_asset(
remote_url,
abs_local_path,
aspect_ratio,
str(requested_output_size).strip() if requested_output_size else None,
int(rendered_width) if rendered_width is not None else None,
int(rendered_height) if rendered_height is not None else None,
parent_asset_id,
prompt,
str(status or ASSET_STATUS_AVAILABLE),
@@ -1161,6 +1180,11 @@ def create_job(
path=None,
conn=None,
):
source = str(generation_source or GENERATION_SOURCE_CMHUB).strip().lower()
expected_provider = GENERATION_SOURCE_PROVIDERS.get(source)
provider = str(provider or expected_provider or "").strip().lower()
if expected_provider is None or provider != expected_provider:
raise db.DbError("AI工场任务来源与服务商组合无效")
now = _now()
task_key = str(task_key or _task_key(project_id))
reference_ids = _parse_reference_asset_ids(reference_asset_ids, source_asset_id)
@@ -1200,8 +1224,8 @@ def create_job(
int(project_id),
source_asset_id,
reference_json,
str(generation_source or "cmhub"),
str(provider or "cmhub"),
source,
provider,
str(job_type),
task_key,
str(prompt or ""),
@@ -1664,6 +1688,14 @@ def set_job_submitted(job_id, task_id, *, call_id=None, points_cost=None, points
now = _now()
with _connection(conn, path) as database:
with database:
job = get_job(job_id, conn=database)
if job is None:
raise db.DbError("AI工场任务不存在")
if (
job.generation_source != GENERATION_SOURCE_CMHUB
or job.provider != PROVIDER_CMHUB
):
raise db.DbError("只有默认网关任务可以记录查询任务编号")
database.execute(
"""
UPDATE image_studio_jobs
@@ -1700,6 +1732,7 @@ def update_job_status(
points_balance=None,
recovery_action=None,
increment_attempts=False,
run_session_id=None,
path=None,
conn=None,
):
@@ -1726,6 +1759,7 @@ def update_job_status(
recovery_action = COALESCE(?, recovery_action),
output_asset_id = COALESCE(?, output_asset_id),
points_balance = COALESCE(?, points_balance),
run_session_id = COALESCE(?, run_session_id),
attempts = attempts + ?,
finished_at = CASE WHEN ? THEN ? ELSE finished_at END,
updated_at = ?
@@ -1737,6 +1771,7 @@ def update_job_status(
recovery_action,
output_asset_id,
points_balance,
str(run_session_id).strip() if run_session_id else None,
1 if increment_attempts else 0,
1 if terminal else 0,
now,
@@ -1751,13 +1786,35 @@ def list_resumable_jobs(path=None, conn=None, project_id=None, include_failed_do
if include_failed_downloads:
clauses = [
"task_id IS NOT NULL",
"generation_source = ?",
"provider = ?",
"recovery_action = ?",
"status IN (?, ?, ?, ?)",
]
params = [JOB_RECOVERY_RESUME, "submitted", "running", "failed", "cancelled"]
params = [
GENERATION_SOURCE_CMHUB,
PROVIDER_CMHUB,
JOB_RECOVERY_RESUME,
"submitted",
"running",
"failed",
"cancelled",
]
else:
clauses = ["status IN (?, ?)", "task_id IS NOT NULL", "recovery_action = ?"]
params = ["submitted", "running", JOB_RECOVERY_RESUME]
clauses = [
"status IN (?, ?)",
"task_id IS NOT NULL",
"generation_source = ?",
"provider = ?",
"recovery_action = ?",
]
params = [
"submitted",
"running",
GENERATION_SOURCE_CMHUB,
PROVIDER_CMHUB,
JOB_RECOVERY_RESUME,
]
if project_id is not None:
clauses.append("project_id = ?")
params.append(int(project_id))
@@ -1767,6 +1824,58 @@ def list_resumable_jobs(path=None, conn=None, project_id=None, include_failed_do
return _fetch_all(database, sql, params, ImageStudioJob)
def fail_stale_direct_jobs(*, active_run_session_ids=(), path=None, conn=None):
"""Fail only confirmed stale synchronous direct jobs during application startup."""
active_ids = {
str(value).strip()
for value in (active_run_session_ids or ())
if str(value).strip()
}
with _connection(conn, path) as database:
rows = database.execute(
"""
SELECT * FROM image_studio_jobs
WHERE generation_source = ?
AND provider = ?
AND status = 'running'
ORDER BY id
""",
(GENERATION_SOURCE_DIRECT, PROVIDER_OPENAI_IMAGES_EDITS),
).fetchall()
stale_ids = [
int(row["id"])
for row in rows
if str(row["run_session_id"] or "").strip() not in active_ids
]
if not stale_ids:
return []
now = _now()
with database:
database.executemany(
"""
UPDATE image_studio_jobs
SET status = 'failed',
error = ?,
recovery_action = ?,
finished_at = ?,
updated_at = ?
WHERE id = ?
""",
[
(
"程序中断,无法确认生成结果,请手动重新生成",
JOB_RECOVERY_REGENERATE,
now,
now,
job_id,
)
for job_id in stale_ids
],
)
return [get_job(job_id, conn=database) for job_id in stale_ids]
def replace_selections(project_id, selection_type, asset_ids: Iterable[int], path=None, conn=None):
selection = str(selection_type)
if selection not in SELECTION_TYPES: