fix(ai-outfit): 停止生成打断当前目录行 + 「停止中…」反馈 (§19.24)

修「开始生成→几秒后停止→两个按钮都灰几十秒像卡死」:非死锁,是温和停止
在等当前目录行收尾,而 _generate_directory_outfit 一次性 submit 全部图片、
shutdown(wait=True) 等全跑完,停止信号进不到该循环;且 _stop 后无反馈。

- outfit_batch:run() 经 _accepts_should_stop 给 generate_func 传 should_stop
  (= _stop_event.is_set),2-arg 才传、1-arg 仍兼容(含既有 mock)
- ai_outfit:generate_outfit_image/_generate_directory_outfit 增 should_stop;
  目录循环改有界提交——始终最多 image_concurrency 张在飞,每张完成后、提交
  下一张前查 should_stop,已停止则停止提交、在飞收尾即返回(结果注明
  「已停止,N 张未生成」,已生成的带 output_paths)
- 面板:_OutfitWorker.gen(task, should_stop) 透传;_stop() 把停止按钮改
  「停止中…」;_set_running(False) 复位「停止生成」

测试:目录行 should_stop 触发后只生成到停止点、返回「已停止」结果;2-arg
generate_func 收到 should_stop;既有用例(1-arg mock、目录并发/部分失败)保持绿。
全套 py37 通过(test_config_service 的 packaging 模板失败属并行 §19.13,无关)。
离屏冒烟:stop 截断 run 且 runner 及时返回;面板停止按钮「停止中…」→结束复位。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:46:49 +08:00
co-authored by Claude Opus 4.8
parent 61d8e20d86
commit 7313c8a443
7 changed files with 136 additions and 18 deletions
+22
View File
@@ -264,6 +264,28 @@ class TestAiOutfitCore(unittest.TestCase):
self.assertEqual(len(result.output_paths), 2)
self.assertFalse((out / "a" / "img2.jpg").exists())
def test_generate_directory_stops_remaining_on_should_stop(self):
"""§19.24: should_stop 触发后不再提交剩余图片,返回「已停止」结果。"""
from core.ai_outfit import generate_outfit_image
d = self._make_dir_with_images("a") # 3 images, image_concurrency=1
out = self.tmp / "out"
client = _RecordingClient(self._image_bytes())
# stop as soon as the first image has been generated
def should_stop():
return client.calls >= 1
result = generate_outfit_image(
self._dir_task(d), "话术", out, model_config={},
api_client=client, should_stop=should_stop)
self.assertFalse(result.success)
self.assertEqual(client.calls, 1) # only 1 generated; rest not submitted
self.assertIn("已停止", result.error)
self.assertIn("2 张未生成", result.error)
self.assertEqual(len(result.output_paths), 1)
def test_generate_directory_uses_image_concurrency(self):
from core.ai_outfit import generate_outfit_image
+17
View File
@@ -67,6 +67,23 @@ class TestOutfitBatchRunner(unittest.TestCase):
self.assertEqual(summary.failure_count, 0)
self.assertEqual(progress, [(1, 2, 2), (2, 2, 3)])
def test_two_arg_generate_func_receives_should_stop(self):
"""§19.24: 2-arg generate_func 收到 should_stop callable;1-arg 仍兼容。"""
from core.outfit_batch import OutfitBatchOptions, OutfitBatchRunner
seen = []
def generate(task, should_stop):
seen.append(callable(should_stop) and should_stop() is False)
return _result(task, True)
OutfitBatchRunner(
[_task(2), _task(3)], generate_func=generate,
options=OutfitBatchOptions(concurrency=1),
).run()
self.assertEqual(seen, [True, True]) # both rows got a working should_stop
def test_retry_until_success(self):
from core.outfit_batch import OutfitBatchOptions, OutfitBatchRunner