fix(product-suite): allow completed source image removal
This commit is contained in:
+67
-10
@@ -871,11 +871,68 @@ def remove_asset_if_unused(asset_id, path=None, conn=None):
|
||||
return asset
|
||||
|
||||
|
||||
def remove_original_assets_if_unused(project_id, asset_ids, path=None, conn=None):
|
||||
"""Atomically remove unreferenced original assets from one project.
|
||||
def _job_requires_original_assets(status, recovery_action):
|
||||
"""Return whether a job can still need its frozen source/reference assets."""
|
||||
|
||||
if status in {"pending", "submitted", "running"}:
|
||||
return True
|
||||
return recovery_action in {JOB_RECOVERY_RESUME, JOB_RECOVERY_REGENERATE}
|
||||
|
||||
|
||||
def _original_asset_delete_blocker(database, project_id, asset_ids):
|
||||
"""Return a user-facing blocker for deleting project original assets, if any."""
|
||||
|
||||
selected_ids = {int(asset_id) for asset_id in asset_ids}
|
||||
placeholders = ",".join("?" for _ in selected_ids)
|
||||
selection = database.execute(
|
||||
f"""
|
||||
SELECT 1 FROM image_studio_selections
|
||||
WHERE project_id = ? AND asset_id IN ({placeholders})
|
||||
LIMIT 1
|
||||
""",
|
||||
[int(project_id), *selected_ids],
|
||||
).fetchone()
|
||||
if selection is not None:
|
||||
return "选中的图片已被终选引用,请先从终选中移除后再删除"
|
||||
|
||||
jobs = database.execute(
|
||||
"""
|
||||
SELECT id, status, recovery_action, source_asset_id, reference_asset_ids
|
||||
FROM image_studio_jobs
|
||||
WHERE project_id = ?
|
||||
ORDER BY id
|
||||
""",
|
||||
(int(project_id),),
|
||||
).fetchall()
|
||||
for job in jobs:
|
||||
status = str(job["status"] or "")
|
||||
recovery_action = str(job["recovery_action"] or "")
|
||||
if status not in JOB_STATUSES or recovery_action not in JOB_RECOVERY_ACTIONS:
|
||||
return "生成任务状态无效,无法确认图片引用,不能删除"
|
||||
try:
|
||||
reference_ids = _parse_reference_asset_ids(
|
||||
job["reference_asset_ids"],
|
||||
job["source_asset_id"],
|
||||
)
|
||||
except db.DbError:
|
||||
return "生成任务参考图快照无效,无法确认图片引用,不能删除"
|
||||
if not _job_requires_original_assets(status, recovery_action):
|
||||
continue
|
||||
source_asset_id = job["source_asset_id"]
|
||||
source_selected = (
|
||||
source_asset_id is not None and int(source_asset_id) in selected_ids
|
||||
)
|
||||
if source_selected or selected_ids.intersection(reference_ids):
|
||||
return "选中的图片仍被可继续处理的生成任务引用,请先完成、停止或放弃该任务后再删除"
|
||||
return None
|
||||
|
||||
|
||||
def remove_original_assets_if_unused(project_id, asset_ids, path=None, conn=None):
|
||||
"""Atomically remove original assets that no runnable job or selection needs.
|
||||
|
||||
Completed, non-retryable jobs remain as history but do not block removal.
|
||||
Local files are intentionally retained. If any requested asset is invalid or
|
||||
referenced, no asset row is removed.
|
||||
still needed, no asset row is removed.
|
||||
"""
|
||||
|
||||
project_id = int(project_id)
|
||||
@@ -906,13 +963,13 @@ def remove_original_assets_if_unused(project_id, asset_ids, path=None, conn=None
|
||||
if set(by_id) != set(ordered_ids):
|
||||
raise db.DbError("选中的商品原图不存在或不属于当前项目")
|
||||
|
||||
referenced_ids = [
|
||||
asset_id
|
||||
for asset_id in ordered_ids
|
||||
if asset_reference_counts(asset_id, conn=database)["total"]
|
||||
]
|
||||
if referenced_ids:
|
||||
raise db.DbError("选中的图片正在被生成任务或终选引用,不能移除")
|
||||
blocker = _original_asset_delete_blocker(
|
||||
database,
|
||||
project_id,
|
||||
ordered_ids,
|
||||
)
|
||||
if blocker:
|
||||
raise db.DbError(blocker)
|
||||
|
||||
database.execute(
|
||||
f"""
|
||||
|
||||
@@ -465,7 +465,7 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # AI 生成的新
|
||||
- T-658d 后逐图主图 checkbox 右侧显示可换行中文 helper,说明多SKU应逐图生成、同商品多角度应作为参考图一同提交。常规生成确认框从最终 specs 计算主图/参考图数量;未开启逐图主图且可用原图超过8张时,明确第1张主图加前7张参考图的上限和忽略数量。确认总点数仍只按 job 请求数计算,不因单请求图片数量重复计算。
|
||||
- T-647 后商品套图中的「AI帮写」不是②标题生成的复用入口:它使用 `vision_alias` 调 `POST /api/v1/analyze/images`,把当前项目中1至8张可用本地原图按 `source_order` 在一次请求中发送;超过8张时明确只使用前8张,原图勾选状态不改变该输入集合。视觉提示词将这些图片定义为同一商品项目的多角度、细节、包装或场景证据集,要求产出一份商品级联合分析,不按“图1/图2”逐图罗列;结果按商品概述、可确认卖点、适用人群与场景、套图画面要求、待确认或避免编造的信息组织,并把可见差异明确为待确认项,不强行合并为事实。单图上限10MiB、总计上限32MiB,缺图、未下载、超限或未配置别名时不发请求且保留用户现有卖点。读取等待固定120秒、连接等待沿用设置;读超时或网络中断只提示“结果未确认”,不自动重发。成功只显示图片张数、扣点和余额,不显示路径、base64、接口URL、完整提示词或上游原始响应;取消只在请求前/返回后协作生效,不强杀网络线程。
|
||||
- `product_suite.render_product_suite_prompt()` 是弹窗预览和真实生成的唯一 renderer;`build_job_specs()` 建立本轮 specs 前只读取一次用户模板并冻结,每个 job 保存最终完整 prompt,运行中修改模板只影响下一轮。新模板使用 `{生成目标}`,其值为内置分类固定目标描述或含实际名称的“生成自定义分类图片:分类名称。”;同时保留 `{套图名称}`、`{补充描述}` 及平台/地区/语言/比例、可选商品ID/主参考图序号、参考图规则、商品卖点和四个只读规则。旧模板必须同时包含 `{套图名称}` 与 `{补充描述}` 才能按兼容路径继续使用;必需变量缺失、未知/未闭合花括号、只读规则变量未独占一行都视为无效。模板校验仅针对模板文本;替换值是业务数据,商品卖点与要求、分类名等可原样包含 `{}`,不再二次作为变量解析。`product_suite_reference_rule()` 是 `{参考图规则}` 的唯一事实来源,变量值都以 `参考图规则:` 开头:逐图主图开启时为单图主参考规则;未开启时固定为“使用第一張上傳圖作為主商品圖,其餘圖片只作為參考。”,不随实际参考图数量变化。提示词弹窗与 job 共用 renderer;实际提交的参考图数量仍由冻结的 `reference_asset_ids` 决定。默认模板独占一行,不额外重复标签或序号。四个只读规则覆盖尺寸与长图(含禁止多宫格拼接)、政治标识、价格和尺码;商品主体一致性与禁止编造并入参考图规则。比例仍同时传入 `image_studio_generation.run_jobs(aspect_ratio=...)`,进入 cmhub 请求与输出资产元数据。
|
||||
- `image_studio_projects.suite_settings_json` 持久化套图设置,旧数据库由 `db.init_db()` 原位补列,默认 `{}`;`draft_prompt` 继续保存卖点文本。`image_studio_assets` 中有效商品原图最多16张,历史 missing 记录不占有效名额;手工原图不会因再次同步蝦皮 URL 被误标 missing。商品套图原图列表的批量勾选只保存在当前 `SuiteTaskState` 对应的界面上下文,不写库;批量移除由 `remove_original_assets_if_unused()` 一次校验项目归属、原图类型和 job/终选引用,并在单个 SQLite 事务中删除资产行、连续重排 `source_order`。服务不删除本地文件或蝦皮线上图片,任一资产校验失败时整批回滚。
|
||||
- `image_studio_projects.suite_settings_json` 持久化套图设置,旧数据库由 `db.init_db()` 原位补列,默认 `{}`;`draft_prompt` 继续保存卖点文本。`image_studio_assets` 中有效商品原图最多16张,历史 missing 记录不占有效名额;手工原图不会因再次同步蝦皮 URL 被误标 missing。商品套图原图列表的批量勾选只保存在当前 `SuiteTaskState` 对应的界面上下文,不写库;批量移除由 `remove_original_assets_if_unused()` 一次校验项目归属、原图类型、终选直接引用及 job 的主图/参考图快照,并在单个 SQLite 事务中删除资产行、连续重排 `source_order`。`pending/submitted/running` 或仍可 `resume/regenerate` 的任务继续保护其原图;`succeeded + recovery_action=none` 及其他已放弃终态只保留历史、不阻断删除。删除后 SQLite 外键将历史 job 的 `source_asset_id` 和输出图 `parent_asset_id` 置空,job 状态、提示词、扣点、轮次和生成结果保留;参考图 JSON 快照不改写。快照无法解析时保守阻断。服务不删除本地文件或蝦皮线上图片,任一资产校验失败时整批回滚。
|
||||
- T-636 起,`image_studio_projects` 增加 `binding_state`(`draft` / `bound`)和稳定 `storage_key`。既有项目迁移为 `bound`,并以原 `item_id` 回填 `storage_key`;项目目录改用 `storage_key`,因此临时草稿绑定正式商品 ID 后不移动目录、不改写已有资产路径。草稿内部使用 `draft_<uuid>` 作为仅数据库可见的非空 `item_id`,GUI 输入框始终保持空白,用户日志和 cmhub 提示词只使用“临时草稿”或“未绑定商品”,不得暴露该内部值。
|
||||
- 在商品套图中,已选账号但未填写商品 ID 时允许输入卖点、导入、拖入或粘贴本地图片。非空卖点在现有防抖稳定后会创建一个可恢复临时草稿并保存到 `image_studio_projects.draft_prompt`;首次有效图片导入也会创建草稿。空白卖点、取消选择和全部导入失败不保留空草稿。只有卖点的草稿同样属于可恢复业务内容;卖点清空且没有资产/job 时仍可按空草稿规则清理。草稿可管理本地图片、AI 帮写、生成套图、查看历史和打开结果目录,但在创建 worker、启动 Chrome 或执行 CDP 前禁止「拉取蝦皮主图」。输入合法数字商品 ID 后,经确认原地绑定同一个 `project_id`;资产、job、selection、提示词、套图设置和 `storage_key` 均保持不变。若同账号目标 ID(含软删除项目)已存在则拒绝覆盖或合并。
|
||||
- 商品套图拉取蝦皮主图使用独立内存 `pull_run_token` 覆盖 URL 读取和本轮后台下载。运行中按钮提供「停止并保留 / 停止并清除本次新增 / 继续拉取」;停止采用协作式安全边界,不强杀线程、Chrome 或已发出的 CDP/HTTP 请求。选择清除时只移除本轮新增、属于当前项目且未被 job/selection 引用的远程原图,并恢复拉取前已有原图的状态和顺序;本地手动导入图片、拉取前已有图片、用户文件和蝦皮线上图片不删除。拉取期间仅关闭商品原图的内部拖拽排序,避免同步刷新与用户排序竞争,文件导入、预览和其他非冲突操作仍保持可用;`reorder_originals()` 也会防御性拒绝迟到排序信号并按数据库顺序刷新。旧 token 的 URL/下载迟到结果不得覆盖新一轮状态。
|
||||
|
||||
+1
-1
@@ -445,7 +445,7 @@ export_generation_round(project_id, generation_round_key, parent_dir, path=None,
|
||||
- T-649 后 `list_global_history_accounts()` 只返回含 job 且未软删除项目的历史账号别名,供历史窗口补齐已删除账号的可选项。`list_global_generation_rounds(account_alias=...)` 以 `projects.account_alias = ?` 精确筛选;传入该参数时优先于旧兼容参数 `account_query`,后者仅保留给既有调用的模糊查询。当前账号显示文本来自账号管理,历史缺失账号只显示明确的历史标识,UI 不通过显示文本反查筛选值。
|
||||
- 拉取蝦皮原主图只读:复用 `editor.open_product(..., bring_to_front=False)` 和 `editor.read_product_image_urls()`,不上传、不拖拽、不点击更新。
|
||||
- 原图下载走 `image_studio_images` 的公网 URL、大小、Content-Type、重定向和 PIL 解码校验;只在用户单击时落盘。
|
||||
- `remove_original_assets_if_unused()` 会先校验整批原图的项目归属、资产类型及 job/终选引用,再在单个事务中删除资产行并连续重排 `source_order`;任一图片不可删除时整批不变,本地源文件和蝦皮线上图片始终保留。
|
||||
- `remove_original_assets_if_unused()` 会先校验整批原图的项目归属、资产类型、终选直接引用,以及 job 的 `source_asset_id/reference_asset_ids` 快照;运行中或仍可 `resume/regenerate` 的任务阻断删除,`succeeded + recovery_action=none` 等不可再执行的历史 job 不阻断。参考图快照无效时保守阻断。删除在单个事务中完成并连续重排 `source_order`;历史 job/输出 asset 的外键关联按 SQLite 置空,状态、提示词、轮次和生成结果保留。任一图片不可删除时整批不变,本地源文件和蝦皮线上图片始终保留。
|
||||
- T-679a 后,商品套图 job 固定持久化来源,不以运行时设置反推:默认网关为 `generation_source="cmhub"`、`provider="cmhub"`,沿用托管异步 `task_key/task_id/status/call_id/points_cost/points_balance` 及 submit/poll/download;自定义网关为 `generation_source="direct"`、`provider="openai_images_edits"`,`task_id` 必须为空,复用 T-678 的同步多图编辑请求,固定 `n=1`,收到图片字节即保存本地资产,不轮询、不写远程 URL、不自动重试。任何来源/提供方组合不精确匹配的 job 都拒绝执行,不能借 task ID 串到 cmhub。
|
||||
- direct job 进入 `running` 时保存仅用于本次进程的 `run_session_id`。只有应用启动阶段持有本地单进程恢复租约时,才将不属于活动会话的遗留 direct `running` job 标为失败,提示“程序中断,无法确认生成结果,请手动重新生成”;切换项目、刷新和恢复 cmhub 任务绝不触发该清理。同步请求返回图片后即使用户刚点击停止,也必须保存为成功;停止只取消未开始 job。连接/读取超时或进程中断的 direct job 不自动补发、不进入继续查询。
|
||||
- `image_studio_assets.aspect_ratio` 始终保存用户选择值;新生成资产另保存可空的 `requested_output_size`、`rendered_width`、`rendered_height`。比例映射为 `1:1 -> 1024x1024`、`3:4/9:16 -> 1024x1536`、`4:3/16:9 -> 1536x1024`,后四种由运行结果返回近似比例标记。旧资产字段保持 `NULL`;初始化迁移只附加字段且可重复执行。
|
||||
|
||||
+1
-1
@@ -218,7 +218,7 @@
|
||||
- 项目仍以 `账号别名 + 商品ID` 唯一,复用 `image_studio_projects/assets/jobs`。`suite_settings_json` 保存平台、国家地区、语言、比例、逐图主图模式和分类数量;卖点文本继续使用 `draft_prompt`,用户停止输入约 500ms 后自动保存,切换任务、关闭任务或程序前同步补保存。未创建项目时只保留在当前任务内存,不因输入文字自动创建临时草稿。
|
||||
- 「商品卖点与要求」关闭内部横向和纵向滚动条,输入框按完整换行内容自然增高;文本删除后可缩回最低高度,页面过长时统一由左侧 `suiteConfigScroll` 滚动。
|
||||
- 商品原图最多16张。前6个槽位固定显示主图与参考1~5;列表关闭内部滚动条,按可用宽度换行并自然向下展开,由左侧配置区统一滚动。支持文件选择、外部拖入、剪贴板粘贴和列表内排序;第1张以「主图」文字、粗体和语义色标识,拖动任意图片到第一位即可设为主图,排序成功提示“已更新商品原图顺序,第一张图片为主图”。历史失效远程图不占有效名额;第1张是主参考图。
|
||||
- 每张真实原图左上角提供独立勾选框,标题行显示「已选 N 张 / 全选 / 反选」;添加占位图不参与选择。勾选只在当前任务界面内临时保留,普通刷新和排序按资产 ID 保留,切换任务或删除成功后清空。右键或 Delete 可批量移除,确认框说明准确数量、主图变化及非破坏性边界;生成中或勾选项仍在下载时整批阻断。移除只删除当前项目的本地资产记录,不删除本地源文件或蝦皮线上图片。
|
||||
- 每张真实原图左上角提供独立勾选框,标题行显示「已选 N 张 / 全选 / 反选」;添加占位图不参与选择。勾选只在当前任务界面内临时保留,普通刷新和排序按资产 ID 保留,切换任务或删除成功后清空。右键或 Delete 可批量移除,确认框说明准确数量、主图变化及非破坏性边界;生成中或勾选项仍在下载时整批阻断。删除会继续保护终选直接引用、运行中和可继续查询/重试 job 的主图及参考图;已成功且不可重试的历史 job 不阻断删除,删除后可按剩余原图开始新一轮生成。移除只删除当前项目的本地资产记录,不删除本地源文件或蝦皮线上图片。
|
||||
- 「拉取蝦皮主图」复用只读 CDP,读取 URL 后由最多2个下载 worker 后台落盘;不改标题/封面、不拖拽、不点击更新。拉取、下载期间其余界面和其他任务仍可操作,但商品原图列表仅锁定内部拖拽排序,并用 tooltip 说明“正在拉取蝦皮主图,完成后可调整图片顺序”;文件拖入、预览等非冲突操作不因此整页禁用。
|
||||
- 套图按分类配置生成图片,固定分类顺序为白底图、场景图、模特场景图、细节说明图、卖点图,之后是自定义分类;默认数量为白底图1、场景图2、模特场景图0、细节说明图0、卖点图2。新增分类默认数量为0,用户未主动配置时不增加生成任务或点数消耗。自定义分类名称非空、无空格、最多10字且不可重名。逐图主图开启后,白底图只生成一次,其余分类按每张有效原图展开。
|
||||
- 套图提示词设置弹窗默认模板首行使用“生成目标:{套图名称},{生成目标}”。五个内置分类使用统一固定目标描述;自定义分类使用“生成自定义分类图片:分类名称。”,实际分类名称必须进入最终提示词。旧模板若仍使用 `{套图名称}` 与 `{补充描述}` 会继续读取和渲染,不静默覆盖用户文件;新模板与旧模板都由同一个 renderer 提供预览和 job prompt。模板变量只在替换前校验,商品卖点与要求、分类名等业务文本允许原样包含 `{}`,不会再次按变量解析;发生其他渲染异常时显示「无法生成商品套图」,不创建 job、worker、价格查询或网关请求。
|
||||
|
||||
+8
-2
@@ -3,7 +3,7 @@ id: T-689
|
||||
title: 商品套图已完成历史任务不阻断原图删除
|
||||
phase: 7
|
||||
deps: [T-631, T-658b]
|
||||
status: TODO
|
||||
status: DONE
|
||||
created: 2026-07-22
|
||||
---
|
||||
|
||||
@@ -65,4 +65,10 @@ created: 2026-07-22
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 待实施。
|
||||
- 2026-07-22 完成。
|
||||
- `remove_original_assets_if_unused()` 改用商品原图专用引用判定,不再由通用 `asset_reference_counts()` 的“所有历史 job 一律阻断”语义决定;通用资产删除和拉取停止清理逻辑保持不变。
|
||||
- 删除前会校验终选直接引用,以及所有 job 的主图和 `reference_asset_ids` 快照。`pending/submitted/running`、或仍可 `resume/regenerate` 的失败/停止任务继续阻断;`succeeded + recovery_action=none` 和其它不可再执行的历史 job 不再阻断第二轮生成前的原图调整。
|
||||
- 已完成历史原图删除后,SQLite 外键将 job 的 `source_asset_id` 和生成图 `parent_asset_id` 置空;历史 job 的状态、提示词、扣点、轮次、生成结果和参考图 JSON 快照保持可查。本地源文件与蝦皮线上图片均不删除。
|
||||
- 参考图快照解析失败或 job 状态异常时采取保守阻断;批量删除继续保持全有或全无,并给出“可继续处理的生成任务”或“终选”中文原因。
|
||||
- 已同步 `docs/04-architecture.md`、`docs/routes.md` 与 `docs/api.md`。新增服务层测试覆盖成功历史的主图/参考图可删、六种运行或可恢复状态的主图/参考图阻断、参考图快照无效阻断,以及历史外键置空后生成结果保持。
|
||||
- 验证通过:`py -3.10 -m unittest tests.test_image_studio tests.test_product_suite tests.test_product_suite_gui`(105项)、`py -3.10 -m unittest discover -s tests`(672项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`。离屏 PySide6 输出字体目录/插件告警,但无测试失败。
|
||||
|
||||
@@ -574,6 +574,162 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_remove_original_assets_allows_completed_history_source_and_reference(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
path=db_path,
|
||||
)
|
||||
source = image_studio.add_asset(
|
||||
project.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
source_order=1,
|
||||
path=db_path,
|
||||
)
|
||||
reference = image_studio.add_asset(
|
||||
project.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
source_order=2,
|
||||
path=db_path,
|
||||
)
|
||||
output = image_studio.add_asset(
|
||||
project.id,
|
||||
"generated_main",
|
||||
parent_asset_id=source.id,
|
||||
path=db_path,
|
||||
)
|
||||
job = image_studio.create_job(
|
||||
project.id,
|
||||
source_asset_id=source.id,
|
||||
reference_asset_ids=[reference.id],
|
||||
path=db_path,
|
||||
)
|
||||
job = image_studio.update_job_status(
|
||||
job.id,
|
||||
"succeeded",
|
||||
output_asset_id=output.id,
|
||||
path=db_path,
|
||||
)
|
||||
self.assertEqual(image_studio.JOB_RECOVERY_NONE, job.recovery_action)
|
||||
|
||||
removed = image_studio.remove_original_assets_if_unused(
|
||||
project.id,
|
||||
[source.id, reference.id],
|
||||
path=db_path,
|
||||
)
|
||||
|
||||
self.assertEqual([source.id, reference.id], [asset.id for asset in removed])
|
||||
self.assertIsNone(image_studio.get_asset(source.id, path=db_path))
|
||||
self.assertIsNone(image_studio.get_asset(reference.id, path=db_path))
|
||||
saved_job = image_studio.get_job(job.id, path=db_path)
|
||||
saved_output = image_studio.get_asset(output.id, path=db_path)
|
||||
self.assertIsNone(saved_job.source_asset_id)
|
||||
self.assertEqual("[%d]" % reference.id, saved_job.reference_asset_ids)
|
||||
self.assertIsNone(saved_output.parent_asset_id)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_remove_original_assets_blocks_active_and_retryable_source_or_reference(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
path=db_path,
|
||||
)
|
||||
statuses = ("pending", "submitted", "running", "failed", "expired", "cancelled")
|
||||
for index, status in enumerate(statuses, 1):
|
||||
source = image_studio.add_asset(
|
||||
project.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
source_order=index * 2 - 1,
|
||||
path=db_path,
|
||||
)
|
||||
reference = image_studio.add_asset(
|
||||
project.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
source_order=index * 2,
|
||||
path=db_path,
|
||||
)
|
||||
job = image_studio.create_job(
|
||||
project.id,
|
||||
source_asset_id=source.id,
|
||||
reference_asset_ids=[reference.id],
|
||||
path=db_path,
|
||||
)
|
||||
if status in {"submitted", "running"}:
|
||||
job = image_studio.set_job_submitted(
|
||||
job.id,
|
||||
"task-%d" % index,
|
||||
path=db_path,
|
||||
)
|
||||
if status == "running":
|
||||
job = image_studio.update_job_status(job.id, "running", path=db_path)
|
||||
elif status in {"failed", "expired", "cancelled"}:
|
||||
job = image_studio.update_job_status(
|
||||
job.id,
|
||||
status,
|
||||
recovery_action=image_studio.JOB_RECOVERY_REGENERATE,
|
||||
path=db_path,
|
||||
)
|
||||
self.assertEqual(status, job.status)
|
||||
with self.assertRaisesRegex(db.DbError, "可继续处理的生成任务引用"):
|
||||
image_studio.remove_original_assets_if_unused(
|
||||
project.id,
|
||||
[reference.id],
|
||||
path=db_path,
|
||||
)
|
||||
self.assertIsNotNone(image_studio.get_asset(reference.id, path=db_path))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_remove_original_assets_blocks_invalid_reference_snapshot(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
db.init_db(db_path)
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
path=db_path,
|
||||
)
|
||||
source = image_studio.add_asset(
|
||||
project.id,
|
||||
image_studio.ASSET_KIND_ORIGINAL,
|
||||
path=db_path,
|
||||
)
|
||||
job = image_studio.create_job(
|
||||
project.id,
|
||||
source_asset_id=source.id,
|
||||
path=db_path,
|
||||
)
|
||||
job = image_studio.update_job_status(job.id, "succeeded", path=db_path)
|
||||
conn = db.connect(db_path)
|
||||
try:
|
||||
with conn:
|
||||
conn.execute(
|
||||
"UPDATE image_studio_jobs SET reference_asset_ids = ? WHERE id = ?",
|
||||
("{坏快照", job.id),
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
with self.assertRaisesRegex(db.DbError, "参考图快照无效"):
|
||||
image_studio.remove_original_assets_if_unused(
|
||||
project.id,
|
||||
[source.id],
|
||||
path=db_path,
|
||||
)
|
||||
self.assertIsNotNone(image_studio.get_asset(source.id, path=db_path))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_sync_original_asset_urls_is_idempotent_and_marks_missing(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
|
||||
Reference in New Issue
Block a user