Commit Graph
116 Commits
Author SHA1 Message Date
adminandClaude Sonnet 4.6 0362c7db89 feat(export): 批量导出生成 Excel 后弹窗提示用户 (§17.24)
Excel 成功写入后弹出 QMessageBox,显示文件路径并引导切换
到「AI 穿搭」页直接使用;无有效印花子目录时不弹框。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 09:37:40 +08:00
adminandClaude Sonnet 4.6 a3f4c39821 feat(export): 批量导出后生成 AI 穿搭 Excel,每印花子目录一行 (§17.23)
添加印花批量导出完成后,自动在输出目录生成与时间戳目录同名的 xlsx
文件(docs/02 §6.12);每个有效印花子目录一行,C 列指向该子目录路径,
AI 穿搭页直接选取即可触发目录扇出逻辑(§19.12),无需手工填写 Excel。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 09:33:28 +08:00
admin e83dd07dc0 feat: use image concurrency for outfit directories 2026-06-22 18:10:54 +08:00
admin 00ed41db14 docs: define outfit image concurrency 2026-06-22 18:05:26 +08:00
admin 5b5ee119ad feat: add outfit output directory 2026-06-22 17:49:59 +08:00
admin e19eef5b43 docs: set outfit output directory 2026-06-22 17:45:00 +08:00
admin 74338c383e fix: seed AI model config at runtime 2026-06-22 17:35:56 +08:00
adminandClaude Opus 4.8 9a61a432d9 fix(ai-outfit): 货号(B列)改为可选,空货号不再跳过整行 (§19.14)
用户表的「商品id(货号)」整列为空时,行有效性判定(标题+货号+图三者非空)
把每一行都判为不完整 → read_all_rows 与 load_outfit_tasks 都返回 0:预览下拉
只剩占位符,且「开始生成」也判定无待处理行。货号不进提示词、目录行输出名沿用
源图名,对该用法是多余约束。

- excel_service.py: read_all_rows / load_outfit_tasks 完整性判定改为只要求
  标题 + 衣服图路径 非空,货号可空(状态过滤不变)
- core/ai_outfit.py: 单文件行货号为空时输出名回退 Path(garment).stem
  (货号非空仍用 货号.jpg);目录行不变
- ai_outfit_panel.py: 样本下拉标签货号为空显示「(无货号)」
- tests: 空货号行被 read_all_rows/load_outfit_tasks 收录、单文件空货号按源图名命名
- docs/11 §4(表+规则)/§9(命名回退)、tasks.md §19.14

离屏冒烟:标题填/货号空/C=目录 → 预览下拉有行、生成产出到子目录、E=完成;
全套测试在 Python 3.7 全绿(test_config_service 的失败属并行的出厂模板工作,与本次无关)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:15:05 +08:00
adminandClaude Opus 4.8 abe46c4aeb feat(ai-outfit): C 列支持图片目录 → 多图扇出到同名子目录 (§19.12)
Excel C 列除单张图片外也可填一个目录(d:/images/a/):对目录内每张图各
生成一张穿搭图(N→N),全部存到 输出目录/<目录叶子名>/,文件名沿用源图名;
Excel 仍整行一个状态:D=子目录、E=全成功才「完成」、F=失败张数/原因。

- core/ai_outfit.py: list_directory_images(顶层、扩展名过滤、排序、忽略子目录)、
  make_outfit_subdir_path(不加 _n 后缀)、generate_outfit_image 目录分支
  (新参 request_interval/image_log;逐张跳过已存在→幂等重试、本地节流、发日志、
  聚合成单个 OutfitResult)
- core/models.py: OutfitResult 加 output_paths(目录行各 jpg,供缩略图)
- ai_outfit_panel.py: gen 闭包传 request_interval/image_log;缩略图逐张;
  明细「结果」列显示「子目录(N 张)」;_basename 处理目录末尾分隔符
- tests/test_ai_outfit.py: 扇出/幂等跳过/空目录/缺目录/部分失败/命名过滤等用例
- docs/11 §4.1+§9.1、tasks.md §19.12

离屏冒烟:3 图目录 + mock API → output/<dir>/ 3 jpg、D=子目录、E=完成、缩略图 3 张;
全套 12 测试文件在 Python 3.7 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:03:04 +08:00
admin 89ead379d2 feat: add AI model config template 2026-06-22 16:57:11 +08:00
adminandClaude Opus 4.8 c59a348d67 feat(ai-outfit): button palette matching the print module (§19.11)
Panel had no stylesheet -> all buttons rendered native gray. Add
setObjectName('aiOutfitPanel') + _apply_styles() with QSS scoped to
#aiOutfitPanel (so dialogs keep native buttons): primary blue for 开始生成
(aiStartBtn, mirrors 开始批量导出/打开文件夹), quiet red for 停止生成 and the 话术
删除 (aiStopBtn/aiPromptDeleteBtn, mirrors 删除), secondary gray for the rest.
docs/11 §10.5 + tasks §19.11. Offscreen grab verified; full suite (12) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:56:25 +08:00
adminandClaude Opus 4.8 d962b60816 feat(ai-outfit): clear 'no pending rows' message + keep preview on generate (§19.10)
- _on_finished(total==0) now shows _show_no_pending_message: counts 完成/失败
  via read_all_rows and tells the user how to redo (clear E column / retry
  failed), instead of the silent '完成 0,失败 0'.
- _on_tasks_loaded no longer calls _fill_sample_combo(tasks), so generating on
  an all-完成 sheet no longer empties the preview dropdown (read_all_rows owns it).
- Offscreen-verified both paths; full suite (12 files) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:17:43 +08:00
adminandClaude Opus 4.8 2643b19686 docs(ai-outfit): empty-tasks prompt + keep preview on generate (§10.4 / §19.10)
After a whole sheet is 完成, clicking 开始生成 silently shows '0 待处理 /
完成 0' and also re-empties the preview sample dropdown. Document two fixes:
(1) a clear 'no pending rows' message (N done skipped / M failed retryable /
clear E column to redo), (2) _on_tasks_loaded must not reset the preview combo
(read_all_rows owns it). Add §10.4 + §19.10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:14:57 +08:00
adminandClaude Opus 4.8 1c9ee36488 fix(ai-outfit): preview sample rows ignore Excel status (§19.9)
Add excel_service.read_all_rows (every valid data row, status-independent) and
use it in _reload_sample_rows for the preview dropdown, so previewing still
works after the whole sheet is 完成. Generation still uses load_outfit_tasks.
+1 excel test (read_all_rows includes 完成/失败, skips incomplete); offscreen
all-完成 sheet now yields sample rows; full suite (12) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:09:56 +08:00
adminandClaude Opus 4.8 cf1096ad79 docs(ai-outfit): preview sample rows ignore status (§10.3 / §19.9)
Bug: after a whole Excel finishes, re-selecting it shows an empty preview
sample-row dropdown because _reload_sample_rows uses load_outfit_tasks (which
skips 完成 rows). Preview should list all valid data rows regardless of status.
Document the cause + fix (new excel_service.read_all_rows for preview; generate
still uses load_outfit_tasks). Update §7 + add §10.3 / §19.9.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:05:01 +08:00
admin 3a0f5b58d8 chore: compact outfit run panel 2026-06-22 14:57:47 +08:00
admin b1dfd63c2e chore: rename outfit prompt labels 2026-06-22 14:36:38 +08:00
adminandClaude Opus 4.8 25b18ea342 feat(ai-outfit): multiple named prompt templates (§19.8)
- config_service: outfit_prompts.json load/save (utf-8-sig read, no-BOM write,
  corrupt/empty -> migrate legacy outfit_prompt.txt or seed 默认; always >=1;
  _normalize_prompts drops invalid). app_config gains outfit_prompt_name.
- ai_outfit_panel: 通用话术 group gets a template dropdown + 新建/另存为/重命名/
  删除 (keeps 插入标题/保存). Switching loads the template + refreshes preview;
  dirty edits prompt Save/Discard/Cancel before switching/new/save-as (cancel
  reverts the combo); unique names; can't delete the last one; selection
  persisted via config_changed; 开始生成 stores the editor into the template.
- tests: +6 config cases (14 total); offscreen CRUD/dirty/rename/delete verified;
  full suite (12 files) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:12:34 +08:00
adminandClaude Opus 4.8 072b546f5f docs(ai-outfit): spec prompt templates (multi, named) — §7.2 / §19.8
Upgrade the single prompt to multiple named templates (switch/new/save-as/
rename/delete/save, remember last). Decisions: all-custom (seed one 默认),
dirty-check confirm on switch, include rename. Storage: outfit_prompts.json +
outfit_prompt_name in app_config, migrate the old outfit_prompt.txt. Add §7.2
design + §19.8 task (not yet implemented).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:52:10 +08:00
adminandClaude Opus 4.8 c1cbef26ef feat: remember & restore the last-selected workflow tab
Add last_tab to DEFAULT_CONFIG; main_window restores it in _restore_preferences
(only enabled/implemented tabs 0/1; setCurrentIndex before connecting the persist
slot to avoid an echo write) and persists via _on_tab_persist on currentChanged.
Mirrors the existing last_template/last_batch_mode pattern. Document the key in
docs/02 §9. Offscreen-verified (restore 0/1, disabled 2 -> 0, persist); suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:44:09 +08:00
adminandClaude Opus 4.8 d851978634 feat(ai-outfit): info popup when user switches resolution/model (§19.7)
Connect QComboBox.activated (user-only) on the 分辨率 and AI 模型 dropdowns to
an info-only QMessageBox: resolution shows the per-resolution timeout +
next-run-only; model shows its api_type + billing/effect caveat + next-run-only.
Track last value so re-selecting the same item doesn't pop; programmatic sets
(apply_config/_fill_sample_combo) never pop. Offscreen-verified; suite (12) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:31:56 +08:00
adminandClaude Opus 4.8 38bae8166c docs(ai-outfit): spec resolution/model switch info popup (§10.2 / §19.7)
User switching the 分辨率 or AI 模型 dropdown shows an info-only QMessageBox
(no block, no revert) about the impact: resolution -> per-resolution timeout +
next-run-only; model -> api_type + billing/effect may differ + next-run-only.
Trigger via QComboBox.activated (user-only) so programmatic sets don't pop.
Add §10.2 design + §19.7 task (not yet implemented).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:26:52 +08:00
adminandClaude Opus 4.8 093e8f7f90 feat(ai-outfit): apply approved relayout to the panel (§19.3)
- 数据源/输出 become inline label+path+browse rows (_inline_path_row);
  removed the left group boxes to free vertical space.
- 通用话术 + 最终提示词预览 enlarged and stretch with window height.
- 生成设置 grid -> 3 columns (2 rows); AI 模型 dropdown moved under it in the
  right column. Splitter sizes left360/center/right400; right minWidth 360.
- Verified offscreen against the mockup (real Qt grab); full suite (12) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:08:25 +08:00
adminandClaude Opus 4.8 8e6c347d91 docs(ai-outfit): relayout AI panel mockup (3-col settings, model on right)
- 生成设置 -> 3 cols x 2 rows; AI 模型 dropdown moved under it on the right.
- 数据源/输出 become inline label+path rows; bigger 通用话术 + 最终提示词预览
  (preview now shows the appended output requirements). Right column widened
  to ~400 (left ~360). Update §10 and re-render ui-ai-outfit.png.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:01:23 +08:00
adminandClaude Opus 4.8 d4082ec477 feat(ai-outfit): auto-append output requirements to prompt (§19.6)
- core/ai_outfit: OUTPUT_REQUIREMENTS constant + build_output_requirements();
  render_prompt(template, task, resolution=None) appends the block when a
  resolution is given; generate_outfit_image passes the current resolution.
- ai_outfit_panel: inline preview now uses render_prompt/build_output_requirements
  with the selected resolution and refreshes when the resolution combo changes,
  so the preview equals what is actually sent.
- tests: +3 cases (tail with resolution, none without, helper empty/filled);
  updated the success test for the appended tail. Full suite (12 files) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:40:28 +08:00
adminandClaude Opus 4.8 031c420f33 docs(ai-outfit): spec auto-appended output requirements (§7.1 / §19.6)
Final prompt = user 话术 + an auto-appended 「批量生成输出要求」block ported
from the old tool (参考解析度 dynamic = resolution setting; 固定 1:1 / 结合标题与
参考图 / 不可跑版 fixed). Simplified wording, always appended, preview must
include it. Add §7.1 design + §19.6 task (not yet implemented).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:35:11 +08:00
adminandClaude Opus 4.8 b122bd20b1 docs(ai-outfit): add ai_models.json sample (placeholder keys)
Template covering all four api_type variants (chat/gemini/images/images_edits).
Copy to ~/.cmbot/config/ai_models.json and fill real values; the real file
(with keys) stays out of git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:13:49 +08:00
adminandClaude Opus 4.8 d75b228303 fix(ai-outfit): stop combos from clipping the left column (§10.1)
Constrain the 样本行 and AI 模型 combos (AdjustToMinimumContentsLengthWithIcon
+ minimumContentsLength + Ignored size policy) so long items elide instead of
inflating the column's min width. Left scroll h-policy AlwaysOff -> AsNeeded as
a safety net; trim splitter initial sizes to fit the default window.

Measured: left inner minimumSizeHint 829px -> 200px (viewport ~398), no clip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:08:36 +08:00
adminandClaude Opus 4.8 7dc907eb78 docs(ai-outfit): record left-column clipping bug + analysis (§10.1)
Left column content is clipped on the right (looks covered by the center).
Measured offscreen: the left QScrollArea has h-scroll AlwaysOff while its
inner content demands 829px min width vs a ~428px viewport. The driver is the
样本行 QComboBox (minimumSizeHint ~743px from its long '第N行·货号·标题' item),
which inflates the preview group. Documented root cause + fix direction
(constrain combo size policy; left scroll AsNeeded).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:04:24 +08:00
adminandClaude Opus 4.8 11dd062f45 feat(ai-outfit): two settings per row in the right column
Replace the one-per-row QFormLayout with a 2-column QGridLayout (3x2:
并发/间隔 · 冷却/重试 · 分辨率/质量), matching the mockup's 2-col 生成设置.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:53:15 +08:00
adminandClaude Opus 4.8 db28a39ebc feat(ai-outfit): inline prompt preview + move settings to right (layout A)
- Drop the prompt-preview dialog; add a live inline preview pinned at the
  bottom of the left column: sample-row dropdown + read-only rendered text,
  refreshing on prompt edit / sample change, with a missing-{title} hint.
- Sample rows load from the chosen Excel (on browse, on startup from the
  remembered path, and from the run's loaded tasks).
- Move 生成设置 out of the left column into the right run column
  (_create_settings_group), so left = 数据源 + 话术 + 预览.
- Matches docs d79f881. Full suite green; offscreen window smoke passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:49:21 +08:00
adminandClaude Opus 4.8 d79f881ec7 docs(ai-outfit): inline prompt preview + settings on right (layout A)
- Replace the prompt-preview dialog with an inline, live preview pinned at
  the bottom of the left column (sample-row dropdown, title substituted).
- Move 生成设置 from the left into the right run column (2-col), so left
  becomes a focused 数据源 + 话术 + 预览 authoring column.
- Update §7 (inline preview) and §10 (left/right column contents) + re-render
  the mockup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:35:52 +08:00
adminandClaude Opus 4.8 572c2cf6dd feat(ai-outfit): drop product_id from prompt UI + default template
Match the doc change (8a4ebe6): remove the 插入货号 button (keep only
插入标题) and drop 货号 {product_id} from DEFAULT_OUTFIT_PROMPT. product_id
still drives Excel column B and the output filename, not the AI prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:17:28 +08:00
adminandClaude Opus 4.8 8a4ebe685b docs(ai-outfit): drop product_id from prompt, keep only insert-title
Prompt offers only {title}; product_id is used for Excel column B and the
output filename (货号.jpg), not for AI generation. Update §7/§10 and the
mockup (remove the 插入货号 chip + {product_id} from the sample prompt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:16:04 +08:00
adminandClaude Opus 4.8 1526254486 feat(ai-outfit): AI 穿搭 UI tab (§19.3) + config plumbing (§19.4)
- main_window: wrap workflow in a QStackedWidget (page 0 = print, page 1 =
  AI outfit); enable tab 2「AI 穿搭」, switch pages on tab change.
- app/widgets/ai_outfit_panel.py: three-column page per docs/11 §10 —
  left settings (Excel/output/model/prompt editor+save+insert+preview
  dialog/batch options), center (recent-results thumbnails + detail
  table), right (progress/stats/start/stop/export failures/log).
- Threading: QThread + _OutfitWorker(QObject) wraps OutfitBatchRunner;
  queued signals refresh UI, each row written back to Excel on the worker
  thread; finish summary + failure-list CSV export.
- config_service: load_ai_models()/load_outfit_prompt()/save_outfit_prompt()
  + outfit_* keys in app_config; panel persists via config_changed signal.
- tests/test_config_service.py: 8 cases for the new helpers. Full suite
  (12 files) green on Python 3.7; offscreen MainWindow smoke test passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:52:37 +08:00
adminandClaude Opus 4.8 2d408d9593 docs: replace 使用说明.docx with cmbot自动化工具.docx
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:38:05 +08:00
adminandClaude Opus 4.8 5ca615f7ae fix(ai-outfit): per-resolution timeout + _to_int default fallback
- §8 dynamic timeout: add resolution_timeout() (512/1K/2K/4K ->
  180/240/360/600s); generate() uses it for the POST and image download.
  timeout_seconds now defaults to 0 = auto-by-resolution; an explicit
  positive value overrides. Validation allows 0, rejects negatives.
- _to_int now returns the supplied default on parse failure (was 0), so a
  bad connect_timeout_seconds falls back to 30 instead of failing as 0.
- Tests: +5 (resolution map, auto vs override timeout, connect fallback).
- tasks.md §19.1/§19.2: tick unit-test item, note runner is pure-logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 18:03:52 +08:00
admin 5b57a4d39a feat: add outfit batch runner 2026-06-18 17:52:05 +08:00
admin 28fcc018bc feat: add outfit generation core 2026-06-18 17:43:35 +08:00
admin fe1dc7778e feat: add ai image service 2026-06-18 17:40:56 +08:00
admin 10238d3da3 feat: add outfit excel service 2026-06-18 17:35:00 +08:00
admin 382797cfea feat: add outfit task models 2026-06-18 17:30:18 +08:00
admin 5fb2f1c8e0 chore: add ai outfit dependencies 2026-06-18 17:26:16 +08:00
adminandClaude Opus 4.8 a7db9444bd docs: track AI outfit module in tasks.md (§19)
Promote AI 穿搭 from the deferred list (§18) to an active §19: design
+ mockup done (19.0), implementation staged per docs/11 §14 (19.1-19.5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:13:34 +08:00
adminandClaude Opus 4.8 31733ed9c9 docs: add AI outfit UI mockup, align style with print module
- New docs/ui-ai-outfit.html + ui-ai-outfit.png: 「2 AI 穿搭」page
  mockup, same chrome/palette/components as docs/ui-v1 (blue-only).
- Center = recent-results thumbnail strip (finished outputs only) +
  detail table; dropped the live single-image preview to avoid the
  concurrent "which row" ambiguity.
- §7: 保存话术 persists template via config_service; 预览最终提示词 is a
  button opening a read-only QDialog rendered from a sample row.
- §8: concurrency is Python threads (ThreadPoolExecutor); Qt only
  signals back to the main thread to refresh UI; default 1 for API limit.
- §10: three-column layout, results-strip rationale, settings 3-col.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:11:56 +08:00
adminandClaude Opus 4.8 c82f870910 docs: add AI outfit module design (docs/11-ai-outfit.md)
Design for the "2 AI 穿搭" tab: Excel as the (fixed) data source → convert rows
to OutfitTask → garment image + prompt → AI image API → person-wearing-clothes
JPG → write result path back to Excel. Ports the old project's API client /
batch / retry into cmbot's app/core/services layering, reusing config/log/
~/.cmbot/output. Also fixes the 旧ai穿搭项目.md §6 recommendation (keep Excel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:30:26 +08:00
admin 0afceba79c feat: add vision prompt script 2026-06-18 15:21:49 +08:00
adminandClaude Opus 4.8 83d0f37d3e docs: add user manual (使用说明.docx) + generator script
scripts/gen_user_manual.py builds docs/使用说明.docx with python-docx (re-run to
regenerate). Covers install/launch, UI overview, workflow, materials, preview
fine-tuning, templates, batch modes/queue, export structure, settings + online
update, data locations, and FAQ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 14:39:29 +08:00
adminandClaude Opus 4.8 421ae78035 feat: default export dir to <install root>\合并后的图片 when packaged
get_output_dir() now returns <install root>\合并后的图片 (parent of app\) for
frozen builds — next to Launcher.exe, easy to find, not replaced on update.
Falls back to <data_dir>/output if the install root is read-only; dev mode
unchanged. The export panel already uses get_output_dir(), so its default
follows automatically.

- tests/test_file_service.py: +3 (dev, frozen install-root, fallback)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 14:29:09 +08:00
adminandClaude Opus 4.8 3c4617a27b docs: default export dir to <install root>\合并后的图片
Default output moves from ~/.cmbot/output (buried in the user profile) to a
合并后的图片\ folder next to Launcher.exe — easy to find and not replaced on
update (NOT under app\, which is swapped each update). Falls back to
~/.cmbot/output if the install root is read-only; user can still pick any dir.
docs/10 §4/§5, docs/02 config, docs/07 §7.5; tasks 17.22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 14:26:04 +08:00