1. 10 May, 2026 7 commits
    • QML ListView 默认 interactive 但不显示 ScrollBar — 用户用滚轮 / 拖拽
      能滚动但看不到滚动条,体验不直观。
      
      补:
        - HistoryTab.historyList: ScrollBar.vertical (AsNeeded)
        - MainWindow.taskList (sidebar): ScrollBar.vertical (AsNeeded)
      
      ImageGenTab promptArea / StyleDesignerTab 字段区都是 ScrollView 自带 ScrollBar,
      不受影响。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • # 1. 历史详情":repeat: 重做"按钮(在 ":clipboard: 复制" 左侧)
      
      行为:把历史项的 prompt + 参考图 + 宽高比 + 尺寸 + 模式回填到图片生成 tab,
      切到 tab 0,**不回填生成图**(用户要新结果)。便于微调提示词后重新生成。
      
      bridges/history.py:
        + Signal redoRequested(payload)
        + Slot redoToImageGen(timestamp):load_history_item_fast → 反查 mode →
          过滤已删参考图 → emit payload
      
      qml_poc/qml/HistoryTab.qml:
        详情面板 prompt header 加 SecondaryButton ":repeat: 重做"
        enabled: 已选中历史项
      
      qml_poc/qml/ImageGenTab.qml:
        Connections target: history 接 redoRequested → 回填字段 + 切 tab 0
        状态文字: "● 已载入历史,可微调提示词后重新生成"
      
      # 2. image_generator.py → image_generator.py.txt
      
      老代码先不删,改 .txt 后缀:
        - Python 不再加载(new 主入口 main_qml.py 早已不依赖它,
          grep 全项目无 'from image_generator' 或 'import image_generator')
        - PyInstaller 不会扫 .txt
        - git mv 保留全部历史,需要扒老逻辑时直接打开看
        - task #19 真正"删掉"动作留作后续,避免一次性删后才发现还有 bug 要扒
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • QML 默认 TextField / TextArea 不带右键菜单(只能 Ctrl+V/C/X),跟旧 QWidget
      版的体验差。补:
      
      ThemedTextField (复用组件,覆盖登录用户名 + 款式词库添加输入):
        TapHandler acceptedButtons:RightButton 弹 Menu,4 项:
          剪切 / 复制 / 粘贴 / 全选
        密码框 (echoMode === TextInput.Password) 时 剪切 / 复制 disabled,
        防泄漏;粘贴 / 全选 仍可用(密码管理器粘贴常用)
      
      ImageGenTab.qml promptArea (TextArea):
        同样的 Menu + TapHandler,因为 promptArea 不通过 ThemedTextField 走
      
      注:HistoryTab 详情 prompt 只读区已有 ":clipboard: 复制" 按钮覆盖复制需求;
      StyleDesignerTab 的 Prompt 预览同样只读,旁边有字数显示,先不改。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 旧 reset_category_library + open_fullsize_view(双击大图)等几个零散点,
      盘 image_generator.py 时漏了 — 现在把 reset_category 这个最实用的补上。
      
      QML wiring:
        - StyleDesignerTab 加 Menu fieldCtxMenu(targetCategory 属性)+
          MessageDialog 二次确认
        - 每个字段 CaptionLabel 加 MouseArea 监听右键,弹 fieldCtxMenu
          标签文字加 "  ▾" 提示有右键菜单
        - 走桥层 jewelry.resetCategory(category)(task #12 已就位)
      
      注:
        - reset_all_library 老早就有顶部 ":arrows_counterclockwise: 恢复默认词库" 按钮接 jewelry.resetAll
        - open_fullsize_view (双击大图弹内嵌 viewer) 当前用 Qt.openUrlExternally
          打开系统查看器替代,UX 不同但功能等价
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 承认上次盘点不全 — 只看了 image_generator.py,没看 task_queue.py 的
      TaskQueueWidget,这两个旧功能漏了:
      
      # 1. 左键任务项 → 回填到对应 tab(旧 _on_task_item_clicked + _load_task_to_main_window)
      
      task_queue.py:
        Task 加 result_path 字段,已完成任务保留生成图绝对路径供 sidebar 回显
      
      bridges/imagegen.py:
        - submitTask 拆出 _do_submit;新增 submitStyleTask(task_type=STYLE_DESIGN)
          StyleDesignerTab.submit 改调 submitStyleTask,让 sidebar 知道任务来源
        - _on_completed 写回 task.result_path(=生成图 history 路径),不依赖 bytes
      
      bridges/taskqueue.py:
        + Signal taskLoadRequested(payload)
        + Slot loadTask(task_id) → 拿 task → 反查 mode(model_id → 极速/慢速)
          + 过滤已删参考图 + 已完成才带 resultPath → emit payload
      
      qml_poc/qml/MainWindow.qml:
        sidebar delegate 左键 → taskQueue.loadTask;右键 → 弹 Menu(不直接取消)
      
      qml_poc/qml/ImageGenTab.qml:
        Connections 接 taskQueue.taskLoadRequested,type=image_gen 时回填
        prompt / refImages / aspect / size / mode / resultPath,并切到 tab 0
      
      qml_poc/qml/StyleDesignerTab.qml:
        Connections 接同信号,type=style_design 时回填 assembledPrompt + resultPath,
        切到 tab 1(注:8 字段 ComboBox 无法反序列化回填,旧版 prompt_preview 行为一致)
      
      # 2. 右键弹菜单(旧 _show_context_menu,不是直接取消)
      
      之前一版我做的"右键直接取消"是错的。改成右键 popup Menu,包含 "取消任务" /
      "取消任务(运行中)" MenuItem,用户点击 MenuItem 才真取消。
      status 不是 pending/running 时 MenuItem disabled。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 补 _safe_get_clipboard_image 老代码的 macOS 分支:
        - macOS 上 mimeData.imageData() / clipboard.image() / application/x-qt-image
          反序列化会触发 NSPasteboard→NSImage 转换,部分场景下直接 native crash,
          Python 层捕获不到(只看到应用闪退)
        - 解决:macOS 上跳过这些路径,改用 osascript 让系统把剪贴板 PNG 数据
          写到 temp 文件,再 QImage(path) 读盘 — 完全绕开 Qt clipboard API
      
      ImageGenBridge._extract_clipboard_image 改造:
        - 路径 B raw bytes:macOS 上 mime 候选不含 application/x-qt-image
        - macOS 上路径 B 拿不到 → 直接 osascript(不再走 C1/C2)
        - 非 macOS 不变:B raw → C1 imageData → C2 clipboard.image
      
      新增 _extract_via_osascript:
        - AppleScript 「set imgData to the clipboard as «class PNGf»」
        - subprocess.run(['osascript', '-e', script], timeout=5)
        - 写到 {tempdir}/nano_banana_app/_clipboard_tmp.png 再 QImage 读盘
        - 无图 / 超时 / osascript 不存在等失败场景都返回 None 不抛
      
      Windows / Linux 行为完全不变(is_mac=False 走原 3 层 fallback)。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • # 启动序列(与旧 main() 1:1 对齐)
      
      新增 core/runtime.py 抽 6 个启动期工具:
        - flush_logs / cleanup_clipboard_tempfiles
        - get_crash_log_path / enable_crash_diagnostics
        - log_system_info / init_logging(RotatingFileHandler 5MB×5)
      
      main_qml.py 完整 8-phase boot:
        Phase 0 enable_crash_diagnostics(faulthandler / excepthook / Qt msgHandler)
        Phase 1 init_logging → logs/app.log
        Phase 2 log_system_info(OS/Python/PySide6/Qt/Pillow/google-genai 版本)
        Phase 2.5 cleanup_clipboard_tempfiles(24h+ 旧文件)
        Phase 3 config 路径 + frozen 时从 bundle 拷贝 + sync_bundled_api_key
        Phase 4 QGuiApplication + 窗口图标(zb100_windows.ico / zb100_mac.icns)
        Phase 4.5 preflight_check(失败弹错退出,不让用户进登录页才撞错)
        Phase 5 业务核心 + Phase 5.5 init_audit_logger 单例
        Phase 6 桥层 + 信号串联
        Phase 7 QML 装载
        Phase 8 aboutToQuit hook → audit shutdown flush
      
      # 完整 logtrace(异常带 stack 落到 app.log)
      
      bridges/* + core/* 所有 try/except 统一改:
        logger.error(f"...: {e}") → logger.exception("...")
        logger.warning(f"...: {e}") → logger.exception("...")
      共 24 处。Qt 警告 / fatal / segfault 也通过 enable_crash_diagnostics
      落到 app.log 和 crash_log.txt。
      
      # 桥层补齐遗漏的功能(对照旧 ImageGeneratorWindow / LoginDialog)
      
      ImageGenBridge:
        - validateImageFile/validateImageFiles:扩展名 + ≤10MB + QPixmap 完整性,
          与旧 validate_image_file 等价;normalizeFileUrls 内置同样校验
        - toggleSavedPrompt / isSavedPrompt:收藏切换(已收藏点了取消,与旧
          toggle_favorite + check_favorite_status 等价)
      
      HistoryBridge:
        - clearAll:shutil.rmtree(base_path) + 重建空目录 + reset model
          对应旧 clear_history
      
      AuthBridge:
        - _get_public_ip:登录后同步拿公网 IP(3 个 API 兜底各 3s timeout),
          audit log_login 现在带 public_ip,对应旧 get_public_ip
      
      # QML wiring
      
      ImageGenTab:
        - 收藏按钮: :star: 收藏 / ✓ 已收藏 toggle,根据 promptArea.text + savedPrompts 实时切
        - addUrlsValidated 统一"加 + 校验 + 反馈":用户拖 5 张图 3 张超 10MB 时
          显示"已添加 2 张,丢弃 3 张(不支持 / 超 10MB / 损坏)"
        - 粘贴也走 validateImageFiles,路径 A 拿用户文件时拦截超大
      
      HistoryTab:
        - 顶部加 :wastebasket:️ 清空 按钮(带 MessageDialog 确认)
        - 列表项右键菜单(QtQuick.Dialogs Menu):在文件管理器中打开 / 复制提示词 / 删除此项
        - 单条删除也走 MessageDialog 确认
      
      # 验证
      
      启动序列 app.log 完整:
        Phase 0..8 全部正确执行,窗口图标已设,audit logger 启动,QML 装载成功。
        系统信息日志记录 PySide6 6.10.1 / Qt 6.10.1 / Pillow 11.1.0 / google-genai 1.52.0。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
  2. 09 May, 2026 15 commits
    • 回顾旧 StyleDesignerTab,task #15a 漏了一批按钮,现在补全:
      
      顶部工具行(左侧卡片):
        - :game_die: 随机:跳过 lockedFields,每个非锁定字段随机取一个 option
        - :arrows_counterclockwise: 恢复默认词库:调 jewelry.resetAll()
        - 重置字段:清空 formData
      
      字段行(每个 ComboBox 后跟 3 个 36px 按钮):
        - :heavy_plus_sign: 添加词条:弹 Dialog 输入新值 → jewelry.addItem(category, value)
        - :wastebasket:️ 删除当前词条:删 ComboBox 当前选中项
        - :unlock: / :lock: 字段锁:locked 字段 ComboBox disabled,标签变 textTertiary,:game_die: 跳过
      
      操作行加 :floppy_disk: 下载图片(复用 ImageGenTab 的 SaveFileDialog 逻辑 + imageGen.saveFile)
      
      ComboBox 宽度修复:
        Layout.preferredWidth: 240 + Layout.maximumWidth: 240 + Layout.fillWidth: true
        锁住宽度,避免随机后内容长度(如"小爪层戒臂(如莲花夹层设计)")撑大 RowLayout
        造成视觉跳动。displayText 由 ThemedComboBox.contentItem 的 elide 处理。
      
      视觉验证:QML_DEBUG_TAB=1 进入款式设计 tab,三排顶部按钮 + 8 行字段
      (每行 ComboBox + :heavy_plus_sign: + :wastebasket:️ + :unlock:)+ 操作行(生成 + 下载)全就位,UI 无回归。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • qml_poc/qml/StyleDesignerTab.qml (新):
        - 左侧 360px 卡片:8 字段 ComboBox 表单(jewelry.categories Repeater)
          每个字段头插 "(不选)" 让用户能清空
          onActivated → updateField(category, value) → 触发 reassemble
        - 右侧上半 Prompt 预览:实时调 jewelry.previewPrompt(formData)
          Text 只读展示 + 字数统计
        - 右侧操作行:生成图片 PrimaryButton (调 imageGen.submitTask
          默认 1:1 / 2K / 慢速模式 Pro 模型) + 重置字段 + 状态指示器
        - 右侧下半预览:lastResultPath Image,双击系统查看器打开
        - 监听 imageGen 4 个信号但只对 currentTaskId 匹配的做反应(taskId
          唯一可区分图片生成 tab vs 款式设计 tab,互不冲突)
        - 监听 jewelry.libraryChanged → optionsRepeater.reloadAll
          (task #15b 词库管理对话框触发时刷新 ComboBox model)
      
      MainWindow.qml: 款式设计占位 Item → StyleDesignerTab {}
      
      未做(task #15b):
        - 词库管理子对话框(每个类别能增删词条)
        - jewelry.addItem / removeItem / resetAll Slot 都已就位,缺 UI
      
      视觉验证:QML_DEBUG_TAB=1 直接进款式设计 tab,8 字段 ComboBox + Prompt
      预览(PromptAssembler 在空表单时给 fallback "一款高端精品珠宝戒指设计…
      高端珠宝渲染" 52 字)+ 生成按钮全就位,UI 无回归。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • core/history.py:
        HistoryListModel.roleNames() 暴露 4 个 Qt 内置 role 给 QML:
        display / decoration / toolTip / timestamp (UserRole)
        旧 QListView 用 int role 不受影响
      
      bridges/history.py:
        Slot thumbnailPath(timestamp) → str
        返回 thumb.jpg 本地路径(按需用 PIL 生成 240px JPEG),缩略图缺失回退原图
      
      qml_poc/qml/HistoryTab.qml (新):
        - 左侧 340px 卡片:列表 (history.model)
          * delegate 76px 高:60×60 缩略图 + timestamp + prompt 双行摘要
          * 选中态 accent 蓝边框 + accentSubtle 浅蓝底
          * hover 提示完整 toolTip
        - 右侧详情卡片:
          * timestamp + createdAt + "在文件管理器中打开" + "删除"
          * 360px 大图(双击系统查看器打开)
          * GridLayout 4 列元信息(宽高比/尺寸/模型)
          * prompt ScrollView(长文本可滚)
        - Component.onCompleted + onCountChanged 自动选中第一条
        - itemRemoved 信号 → 选中失效时回到空态
      
      qml_poc/main_qml.py:
        AppState 加 QML_DEBUG_TAB env var (0/1/2) 控制启动 tab 索引,方便 PoC 调试
      
      MainWindow.qml: 历史记录占位 Item → HistoryTab {}
      
      视觉验证:QML_DEBUG_TAB=2 直接进历史 tab,2 条已存历史正确渲染(缩略图 +
      详情大图 + 元信息 + prompt 都对),UI 无回归。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • MainWindow.qml sidebar 改造:
      - header: 加 "N 进行中" 计数(绑定 taskQueue.runningCount + pendingCount,0 时隐藏)
      - ListView model: 硬编码 ListModel → taskQueue.model(_TaskListModel QAbstractListModel)
      - 空态占位: "暂无任务"(visible: count === 0)
      - delegate 改造,56px 高 ColumnLayout 三行:
        1. prompt 摘要(>14 字省略)
        2. 状态彩色文字(pending=橙 / running=蓝 / completed=绿 / failed=红) + 耗时
        3. 进行中任务的细进度条(高 2px,绑 progress 0-1)
      - delegate 用 required property 声明 6 个 role:taskId / prompt / status / progress / statusText / elapsed
      - 右键点击 pending/running 任务 → taskQueue.cancelTask(taskId)
      
      至此 生成 → sidebar → 历史 闭环完整:
      - 用户在图片生成 tab 点 "生成图片" → ImageGenBridge.submitTask
      - TaskQueueManager 信号 → TaskQueueBridge._on_task_added → upsert 进 model
      - sidebar ListView 自动渲染新行,状态文字 + 进度条同步
      - 完成后 ImageGenBridge → HistoryBridge.addNew,历史 tab 也会增量
      
      视觉验证:QML_AUTO_LOGIN=1 启动主窗口,空态正确显示"暂无任务",UI 无回归。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • ImageGenBridge.saveFile(src, dest) → bool
        shutil.copy2 wrap,源文件不存在 / 写盘失败时记日志返回 False
      
      ImageGenTab.qml:
        - SaveFileDialog (FileDialog SaveFile mode + defaultSuffix png)
        - "下载图片" enabled by lastResultPath !== "",onClicked 弹保存对话框
        - onAccepted 调 imageGen.saveFile,成功状态绿色 "● 已保存到 <path>"
        - 预览 Image 包 MouseArea,onDoubleClicked Qt.openUrlExternally("file:///" + path)
      
      至此 task #14 完整闭环:
        #14a 核心生成(prompt → submit → 进度 → 预览)
        #14b 参考图录入(添加 / 粘贴 / 拖拽 + 缩略图删除)
        #14c 提示词收藏 / 删除(持久化 config.json)
        #14d 下载图片 + 双击预览打开系统查看器
      
      视觉验证:QML_AUTO_LOGIN=1 启动主窗口,UI 完整无回归,下载按钮在无生成图时正确灰。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • config_util.py 加 save_config(path, config) → bool
        原子写:tmp 文件 + replace,失败记日志返回 False(不抛异常)
      
      ImageGenBridge:
        - 构造函数加 saved_prompts / config_path 参数
        - Property savedPrompts: list[str]
        - Slot addSavedPrompt(prompt): 去重 + 插入头部 + 持久化 + 信号
        - Slot removeSavedPrompt(prompt): 移除 + 持久化 + 信号
        - _persist_saved_prompts: load_config_safe → 改 saved_prompts → save_config
      
      main_qml.py: 装 saved_prompts + config_path 给 ImageGenBridge
      
      ImageGenTab.qml:
        - ":star: 收藏" enabled by promptArea.text.trim().length > 0,onClicked add
        - "删除" enabled by savedPrompts.length > 0,删除当前 ComboBox 选中项
        - 快速选择 ComboBox model: imageGen.savedPrompts (响应 savedPromptsChanged 自动刷)
        - onActivated 仅在 currentText 非空时填到 promptArea
      
      视觉验证:QML_AUTO_LOGIN=1 启动主窗口,ComboBox 已显示从 config.json 读到的
      "主石换成闪耀的祖母绿",收藏按钮在 prompt 空时正确灰。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • ImageGenBridge 加 2 个 Slot:
      - pasteFromClipboard() → str
        从 QGuiApplication.clipboard() 拿 image,存到 tempdir/nano_banana_app/clipboard_*.png
        (与旧 _cleanup_clipboard_tempfiles 同目录,启动期统一 24h 清理)
        失败返回 ""
      - normalizeFileUrls(urls list) → list
        把 QML DropArea 给的 file:/// QUrl 列表转本地路径,过滤非图扩展名
        (.png/.jpg/.jpeg/.webp/.bmp)
      
      ImageGenTab.qml:
      - import QtQuick.Dialogs 引入 FileDialog (现代 QML6 module)
      - "添加图片" → addImageDialog.open() (multi-select 图片)
      - ":clipboard: 粘贴图片" → imageGen.pasteFromClipboard(),无图时状态变橙色"剪贴板没有图片"
      - 拖拽区 DropArea 接 text/uri-list,containsDrag 时边框变 accent + 底色变 accentSubtle
        drop 后调 imageGen.normalizeFileUrls(drop.urls) 拿本地路径
      - 已选图缩略图 Flow:
        ScrollView + Repeater 96×96 圆角缩略图 + 右上角红色 × 删除按钮
      - addRefPath/addRefPaths/removeRefAt 三个辅助函数处理状态去重和增量
      
      未做(task #14c/d):
      - 提示词收藏 / 删除 saved_prompts 持久化
      - 下载图片 + 双击预览打开系统查看器
      
      视觉验证:QML_AUTO_LOGIN=1 启动主窗口,参考图区按钮全 enabled,UI 无回归。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • ImageGenTab.qml wiring 改造:
      - 加状态 properties: refImages / currentTaskId / lastResultPath / statusText / statusColor
      - TextArea 取 placeholder,删硬编码 "风景画" 占位
      - 模式/宽高比/尺寸 三个 ComboBox 给 id,submit() 时取 currentText
      - 操作行 "生成图片" PrimaryButton onClicked → tab.submit()
        - submit 内部空 prompt 阻断 + 重复点击防护 + 调 imageGen.submitTask
        - 任务进行中按钮文字 "生成中…",结束自动恢复
      - Connections 监听 imageGen 4 个信号:
        - taskSubmitted → "● 已提交"
        - taskProgress → "● 正在生成…"
        - taskCompleted → 拿 resultPath 更新 lastResultPath,"● 已完成"
        - taskFailed → "● <error>",红色
      - 预览 Rectangle 内嵌 Image (file:/// + lastResultPath, cache:false 强制重读)
        - 无图片时显示占位文字,有图时直接展示
      
      未做(task #14b/c/d 后续 commit):
      - 添加图片 / 粘贴图片 按钮(QFileDialog + QClipboard,桥要补 Slot)
      - :star: 收藏 + 删除 saved_prompts 持久化
      - 下载图片 按钮 / 双击预览打开系统查看器
      
      视觉验证:QML_AUTO_LOGIN=1 启动主窗口,所有控件渲染无回归。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • LoginScreen.qml wiring 改造:
      - appState.login → auth.login (走 DatabaseManager.authenticate / pymysql)
      - 删 || "demo" 兜底,密码空时 UI 阻断
      - submitting 状态:登录中 username/password/checkbox/button 全 disabled,按钮文字 "登录中…"
      - 监听 auth.loginFailed → errorLabel 红字展示后端返回的具体原因
        ("用户名或密码错误" / "无法连接到服务器" 等,DatabaseManager 已统一中文化)
      - usernameField 不再硬编码 "chaijin",改 placeholder
      
      冒烟测试(直接调 Python 测真 db):
      - chaijin/wrongpwd → False,loginFailed 信号"用户名或密码错误"
      - chaijin/a160827 → True,loggedIn=True,currentUser='chaijin'
      QML 启动登录页渲染正常(task13_login.png 已验证)。
      
      注:
      - "记住用户名/密码" checkbox UI 保留但暂未持久化,task #18 系统集成时接 config_util 落盘
      - pymysql 同步阻塞主线程最多 5s(connect_timeout),暂不影响 UX;
        若 db 慢成痛点 task #18 时再改 worker 异步
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • bridges/
        auth.py        AuthBridge       login/logout + currentUser/loggedIn (PoC 模式接受任意非空)
        imagegen.py    ImageGenBridge   submitTask + 信号转发 (TaskQueueManager → QML)
        history.py     HistoryBridge    暴露 HistoryListModel + refresh/getItem/deleteItem
        taskqueue.py   TaskQueueBridge  自带 _TaskListModel (QAbstractListModel + roleNames)
        jewelry.py     JewelryBridge    词库增删 + previewPrompt
        _icons.py      build_placeholder_icon (旧 ImageGeneratorWindow.create_placeholder_icon
                       是实例方法,桥层独立一份)
      
      main_qml.py 改造:
        - 顶部加载 config.json + 实例化 core 业务 (HistoryManager / JewelryLibraryManager /
          TaskQueueManager) + 启 audit_logger(有 db_config 时)
        - 5 个桥通过 setContextProperty 注入:appState / auth / imageGen / history /
          taskQueue / jewelry
        - imageGen.taskCompleted → history.addNew(timestamp) 串起新生成图自动入历史
        - AppState 保留作 currentTab 等 UI 状态(task #13 后 loggedIn 字段删,QML 改用 auth.loggedIn)
      
      冒烟测试:
        - 5 桥全 import OK,依赖注入构造 OK
        - PoC 模式 login('test','x') 通过 / login('','') 拒绝
        - history.refresh() 加载 2 条历史成功
        - jewelry.previewPrompt({...}) 正确组装中文 prompt
        - QML PoC 启动,登录页 + 主窗口(QML_AUTO_LOGIN=1)渲染完整无视觉回归
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 5082 行的怪兽塞了 12 个类(数据库/历史/Worker/词库/UI),现在按职责拆开:
      
        core/paths.py       get_app_data_path + save_png_with_validation + 启动迁移
        core/database.py    DatabaseManager + hash_password
        core/history.py     HistoryItem + HistoryListModel + HistoryManager
        core/generation.py  ImageGenerationWorker + Gemini 模型常量
        core/jewelry.py     DEFAULT_JEWELRY_LIBRARY + JewelryLibraryManager + PromptAssembler
      
      image_generator.py 5082 → 3781 行,剩下全是 QWidget UI 类
      (LoginDialog / DraggableThumbnail / DragDropScrollArea / ImageGeneratorWindow /
      StyleDesignerTab + utils + main),task #19 QML 全量切换后整体删除。
      
      外部消费者改 import:
        task_queue.py / temp_clean.py: from image_generator → from core.generation
        image_generator.py 顶部 from core.* 引入,LoginDialog 等内部代码无感知
      
      冒烟测试:image_generator/task_queue/core 各自 import 通过,类身份正确。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • - Main.qml 登录态 height 880 → 940, minimumHeight 760 → 820
      - 参考图 Card preferredHeight 170 → 230, minimum 160 → 200
      - 视觉:参考图 / 提示词+设置 / 预览 ≈ 230 / 290 / 240,比例协调
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • - 参考图 Card  preferredHeight 200 → 170
      - 提示词+设置行 preferredHeight 320 → 290
      - 预览 Card  minimumHeight 200 → 240(拿剩余空间,视觉更舒展)
      - AppState: QML_AUTO_LOGIN=1 时默认登录态,调试时不必每次重启都过登录页
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • - 参考图 Card  preferredHeight 240 → 200
      - 提示词+生成设置行 preferredHeight 360 → 320
      - 生成设置卡内 spacing 16 → 12, group 内 6 → 4
      - caption "生成模式/宽高比/图片尺寸" 完整显示, 预览卡占满底部
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 新分支 feat/ui-qml-poc 从 master 起,独立于 feat/ui-redesign-apple-theme。
      PySide6 + QtQuick 路线,业务后端不动,只换 UI 层。
      
      文件:
      - qml_poc/main_qml.py         入口(QQmlApplicationEngine + AppState QObject)
      - qml_poc/qml/Main.qml        ApplicationWindow,按 loggedIn 切换尺寸 + 子页
      - qml_poc/qml/Theme.qml       Singleton 设计令牌(24 色 + 尺寸 + 字号 + 跨平台字体栈)
      - qml_poc/qml/qmldir          模块声明(singleton Theme)
      - qml_poc/qml/LoginScreen.qml 登录页(标题 + 副标题 + 输入 + 复选 + pill 按钮 + 回车提交)
      - qml_poc/qml/MainWindow.qml  主窗口(下划线式 TabBar + 任务队列 sidebar + StackLayout)
      - qml_poc/qml/ImageGenTab.qml 图片生成 tab UI(参考图卡 + 提示词卡 + 生成设置卡 + 操作 + 预览)
      - qml_poc/qml/components/     Card / PrimaryButton / SecondaryButton / ThemedTextField / ThemedComboBox / CaptionLabel
      
      UI 改进:
      - caption + combo 用 ColumnLayout 6px 间距;Card 内 12px spacing
      - TabBar 改下划线式(不是凸起 tab)
      - 主按钮 pill 圆角 980 + Apple Blue + ColorAnimation 120ms hover/pressed
      - 输入框焦点 2px 蓝边动效
      - 复选框 Apple Blue 实色 + 白色 ✓
      - 卡片圆角 12px + 1px 极淡边框
      
      跨平台:QtQuick 跨 Mac/Windows/Linux;字体栈 Qt.platform.os 自动选 SF Pro/Segoe UI。
      
      后续:业务桥层 + 4 个 tab 业务接入 + 打包 + 切主入口(任务清单 #12-#19)。
      
      Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
      柴进 committed
  3. 27 Apr, 2026 3 commits
    • 继上次只修 hot path 之后, 把残留的所有冷热路径都改成 raw json
      直接操作, 彻底消灭 load_history_index 内的 stat 循环.
      
      修改:
      1. _migrate_paths_once: 启动时一次性路径归一化, 抽样 5 条 60% 阈值
         决定是否需要全量迁移, 之后所有路径都是当前 base_path 下的有效绝对路径
      2. load_history_index: 简化为 raw read + from_dict + sort, 0 stat
         N=513 时只需几 ms (旧版 ~60ms)
      3. delete_history_item: 改 raw json filter
      4. _cleanup_old_records: 改 raw json sort + slice
      5. 删除已无调用方的 _save_history_index / _fix_history_path
      
      至此 hot path (生成完成, 点击历史项) 与 cold path (删除, 启动清理)
      都不再触发 N 次 stat. 唯一保留全量读的 load_history_index 也只有
      refresh_history 一处真实调用方.
      柴进 committed
    • 第二阶段发布后用户日志显示: 启动 8s 后 5s 内连续 5 次
      load_history_index 全扫 513 条 + 513 次 stat, 主线程累计阻塞,
      随后日志戛然而止 (典型 jetsam SIGKILL 指纹).
      
      定位到两条阶段 1+2 没修的 hot path:
      
      1. get_history_item(timestamp): 点击历史项查看详情时触发,
         原走 load_history_index() + 线性查找. 改为直接调
         load_history_item_fast() (读单个 metadata.json), O(1).
      
      2. _update_history_index(history_item): 每次生成完图都调,
         原走 load_history_index() 全扫 + from_dict + 路径修正 + 整表写回.
         改为直接对 raw json list dict 操作, 无 stat / 无 from_dict.
      
      改完之后 hot path 完全不再触发 _fix_history_path 循环.
      柴进 committed
    • 阶段 1: 生成完成不再 refresh_history() 全量重建 360+ 个 widget,
      改走 prepend_history_item() O(1) 增量插入.
      
      阶段 2 Step 1: QListWidget -> QListView + HistoryListModel:
      - Model 仅持有 list[str] timestamps + OrderedDict LRU 缓存
      - 视口可见行才触发 data() 加载缩略图,与历史总条数解耦
      - delete/clear 走 model 增量,不再触发全量刷新
      
      修复 macOS 上长时间运行被 jetsam SIGKILL 的崩溃路径
      (2026-04-24 单日 14 次闪退,无 Traceback / faulthandler 栈).
      柴进 committed
  4. 21 Apr, 2026 5 commits
    • 自洽问题: 原先 REQUIRED_TABLES 里列了 nano_banana_app_config,
      但 _check_version 明明有"读不到就放行"的 fail-safe。
      同一张表一边说"必须存在",一边说"不存在也没事",逻辑矛盾。
      
      结果是: 如果 migration 还没在生产 DB 跑, 1.1.0 客户端启动就会在
      "表存在性校验"硬挂, 弹"应用启动失败,请联系 @柴进",
      完全绕不过 fail-safe。
      
      修正: app_config 作为"版本门禁"的后端存储不是启动必需品。
      - 没表/没记录 → fail-safe 放行, 版本门禁临时关闭
      - 有表+有记录 → 正常做版本校验
      - migration 可以任意时刻跑, 不阻塞发布节奏
      柴进 committed
    • 机制:
      config_util.sync_bundled_api_key(user_config_path) 启动时调用:
      - frozen 态才生效 (开发态不干涉)
      - 打包 config.json 的 api_key 和用户目录的比较
      - 不一致时只覆盖 api_key 这一个字段
      - saved_prompts / last_user / saved_password_hash / db_config 全部保留
      - 多重 fail-safe: 打包 config 缺失/解析失败/api_key 空/用户 config 解析失败 → 全都不动
      
      新 Gemini Key 已写入 repo 的 config.json。两周过渡计划:
      - D0 (本次发布): 新 Key 随 bundled config 分发,升级用户启动瞬间自动切到新 Key;
        老用户暂不升级,config.json 里还是老 Key,Google Cloud 两把 Key 并行
      - D14: Google Cloud Console 作废老 Key;
        已升级用户零感知,没升级用户此时才 401,被迫升级
      
      注: 企业内部软件,有用户名密码保护,Key 内置到 repo 可接受
      柴进 committed
    • 机制:
      - 新建 version.py (APP_VERSION = 1.1.0) 作为单一真相源
      - 新建 migration: nano_banana_app_config KV 表
        初始化 min_client_version=1.0.0, download_url=飞书文档
      - preflight 在 DB 表/字段校验通过后加一步版本校验:
        读 app_config -> 对比本地 APP_VERSION
        过旧返回 VERSION_TOO_OLD::<min>|<url> 前缀
        fail-safe: 读不到配置/解析失败 -> 放行 (避免 DBA 误操作全体挂掉)
      - 新增 handle_version_too_old: 明文弹窗 + "打开下载页" 按钮
        用 QDesktopServices.openUrl 调系统默认浏览器 (跨 Win/Mac)
      - image_generator 启动处按 is_version_error 分发:
        版本过旧走明文升级提示, 其他错误保留原脱敏路径
      
      Why:
      以后想淘汰任一老版本,只需:
        UPDATE nano_banana_app_config SET config_value='X.Y.Z'
         WHERE config_key='min_client_version'
      不再需要轮换 API Key (一次性核爆 -> 精细版本控制)
      柴进 committed
    • 根因: audit_logger.py / config_util.py / preflight.py 这三个启动
      必需模块从未被 git 追踪过. Windows 构建机上这些文件在本地磁盘,
      PyInstaller 能找到, 所以 Win 包正常; 但 Mac 拉代码后根目录缺这
      三个文件, PyInstaller 的 Analysis 找不到 import 链, 构建要么失败
      要么运行时 ImportError.
      
      本次补齐:
      - audit_logger.py — 审计日志单例 (NDJSON 本地队列 + 异步 MySQL 上传)
      - config_util.py  — 跨平台配置路径解析与安全加载
      - preflight.py    — 启动门禁 (config/DB/schema 校验)
      - database_schema.sql — 运维参考
      - migrations/2026-04-21_add_audit_log_columns.sql — 审计表迁移
      - .gitignore      — 屏蔽 .venv/build/dist/logs/__pycache__ 等,
                          防止以后再漏推业务代码时被大量噪声淹没
      柴进 committed
    • 本次改动分三块,合并一个提交:
      
      1. 运行中任务取消 (方案 A 软取消)
         - cancel_task 扩展支持 RUNNING: 标记 CANCELLED,脱钩 _current_worker,立刻 _process_next()
         - _on_task_completed/_on_task_failed 开头加 status == CANCELLED 自检,丢弃废 worker 回调
         - 右键菜单对 PENDING/RUNNING 都显示"取消任务"
         - _update_summary 新增"已取消"状态;_cleanup_old_tasks 纳入 CANCELLED 清理
      
      2. 任务栏点击回显修复
         - 根因: TaskQueueWidget 创建时没传 parent,self.parent_window 永远 None,回显全部静默失败
         - 根因: 两套重名方法互相覆盖,生效版用了 prompt_input / add_reference_image 等不存在的属性
         - 修复: 删除重复定义;回填改用主窗口真实属性 (prompt_text / uploaded_images + update_image_preview / aspect_ratio / image_size / display_image)
         - 款式设计 tab 也完整支持 prompt / 宽高比 / 尺寸 / 结果图回显
      
      3. Flash 独占宽高比 + 模式兼容校验
         - 新增 1:4 / 4:1 / 1:8 / 8:1 四个极速模式独占比例
         - 款式设计 tab 宽高比补齐到和图片生成 tab 一致
         - FLASH_ONLY_ASPECT_RATIOS 常量作为单一真相源
         - 双向实时校验:
           * 选 Flash-only 比例 + 慢速模式 → 问是否切到极速,拒绝则回滚比例
           * 极速 + Flash-only 比例 → 切慢速 → 问是否坚持切换,坚持则比例回落 1:1
         - 提交入口保留校验作为 defense in depth
      柴进 committed
  5. 15 Apr, 2026 6 commits
    • 之前只有两个图标并排, 用户看不出来要拖拽. 现在在 DMG 挂载打开时:
      - 中间一根灰箭头从 .app 指向 Applications
      - 底部中文提示 "将左侧应用拖到右侧 Applications 即完成安装"
      - 窗口/图标/箭头三者坐标对齐
      
      实现:
      - 用 PIL 在 staging 阶段生成 600x400 PNG, 放 .background/bg.png
        (dot 前缀让 Finder 默认隐藏)
      - AppleScript 里 set background picture 用 HFS path (冒号分隔)
      - 图标位置 {150,200} / {450,200} 与背景图箭头两端对齐
      
      PIL 字体查找: PingFang (默认中文) -> STHeiti -> Helvetica -> 兜底,
      在打包机上都会命中至少一个.
      
      Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 双击挂载后, Finder 窗口显示两个图标并排, 用户直接把
      ZB100ImageGenerator.app 拖到 Applications 快捷方式上即可安装,
      不用讲解路径, 符合 macOS 标准分发体验.
      
      实现:
      - 先 staging: 把 .app 放入临时目录, 同级建 Applications 软链
      - hdiutil create UDRW 可写镜像
      - 挂载 -> AppleScript 设置窗口大小/图标位置/视图样式
      - 卸载 -> hdiutil convert UDZO 压缩成只读分发包
      - osascript 失败不中断 (防 Finder 自动化权限未授予), DMG 仍可用
      
      Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 背景: lehe 机器启动崩是因为 SMB 传输吃掉了 PIL/.dylibs -> __dot__dylibs
      内层 symlink, 导致 libtiff dlopen 断链.
      
      修复: 构建脚本新增 hdiutil create 步骤打包 DMG (UDZO 压缩). DMG 是
      HFS+ 镜像, symlink 完整保留, 用户挂载 -> 拖拽到 Applications 即用.
      
      以后发 DMG, 不要直接拷贝 .app 目录.
      
      Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
      柴进 committed
    • lehe 那台 macOS 26 新包仍崩, 诊断发现不是打包问题:
        Frameworks/libtiff.6.dylib -> PIL/.dylibs/libtiff.6.dylib (broken)
      
      PyInstaller 6.x 实际结构:
        PIL/__dot__dylibs/  <真目录>
        PIL/.dylibs -> __dot__dylibs  <内层 symlink, 被 NAS 吃掉了>
        Frameworks/libtiff.6.dylib -> PIL/.dylibs/libtiff.6.dylib  <外层 symlink>
      
      内层 symlink 丢失 → 整条链断. 其他 macOS 26 机器能跑是因为挂载
      NAS 的协议/客户端不同, 保 symlink.
      
      记进 spec 注释, 下次再有人打包踩这坑直接看到结论.
      
      Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 上一版 spec (4097b529) 在 macOS 26 构建出的包仍崩在同一位置:
        dlopen: Library not loaded: @rpath/libtiff.6.dylib
      
      修改:
      - 引入 PyInstaller 官方 collect_dynamic_libs('PIL', destdir='.')
        作为主策略, 显式枚举 .dylibs/.libs 作为兜底
      - 每步打印详细信息: PIL 安装路径、找到的文件列表、
        最终 binaries 合并结果
      - 最后如果 len == 0 直接打印警告, 免得构建成功但运行时才崩
      
      下次构建输出里找 [spec] 开头的行, 就能看清是哪步出了问题:
        - collect_dynamic_libs 返回空? -> PIL 没 bundled dylibs
        - .dylibs/ 不存在? -> Pillow 装的不是 wheel
        - 有文件但 bundle 里没有? -> PyInstaller 传入后续处理的问题
      
      Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 构建流程 (修复 macOS 26 上 libtiff.6.dylib 未打包闪退):
      - ZB100ImageGenerator.spec 成为跨平台构建配置唯一真相源
      - 显式枚举 PIL/.dylibs/.libs 下的原生库并平铺到 bundle 根目录,
        匹配 PyInstaller 把 _imaging.so 的 @rpath 改写成 @loader_path/..
        (= Contents/Frameworks/) 的预期位置
      - build_mac_universal.sh / build_windows.bat 不再删除 *.spec,
        调用简化为 `pyinstaller ZB100ImageGenerator.spec`
      - 旧的 --collect-all PIL 方案保留了 .dylibs/ 目录结构, 跟 rpath
        预期位置不匹配, 治标不治本
      
      诊断日志 (定位 refresh_history 的 SIGKILL 位置):
      - 新增 _flush_logs() 模块级工具, 在可疑阶段边界强制刷盘
      - load_history_index: 每 20 条打一次路径修正进度
      - refresh_history: clear/load/loop 三段独立计时, 每 20 条打渲染进度
      - 下次 macOS 卡死被 SIGKILL 时能精确定位死亡行
      
      昨天 138ec9fa 的 thumb.jpg 缓存挡住了内存压力型 SIGKILL, 但今天
      115 条时 refresh_history 仍卡 8 秒后被 WindowServer 强杀 (日志
      只打了 "开始刷新" 就再无输出). 真正的架构修复 (取消 clear+rebuild
      全量重绘) 留待下次定位清楚后再做.
      
      Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
      柴进 committed
  6. 14 Apr, 2026 2 commits
    • 根因:
      - refresh_history 每次调用都 history_list.clear() + 重建全部条目
      - 每条加载 2K PNG 原图做 QPixmap (~16MB 解码) 再缩到 120x120
      - 106 条历史 × 16MB = ~1.7GB 瞬时内存峰值
      - macOS 内存压力触发 SIGKILL,不可被 faulthandler 捕获
      - 日志里只有 "开始刷新" 没有 "加载到 N 条",确认死在 clear()/load 之间
      
      修复:
      - HistoryManager 新增 thumb_path_for + get_or_create_thumbnail:
        用 PIL 生成 240x240 JPEG 缓存到 <record_dir>/thumb.jpg,
        基于 mtime 判断是否重新生成
      - save_generation 保存原图后顺便生成缩略图 (失败不影响主流程)
      - refresh_history 只加载 thumb.jpg, 内存峰值从 1.7GB → 6MB (280x 下降)
      - 缩略图生成失败用占位图兜底, 不回退加载原图 (防回到危险路径)
      - 首次升级会为 106 条存量一次性补生成 thumb.jpg (PIL 串行 ~32MB 峰值, 安全)
      
      附加:
      - refresh_history 增加 "刷新完成" 收尾日志, 便于下次若再崩定位死亡位置
      
      跨平台: 纯 PIL + pathlib + Qt 标准 API, Windows 零回归
      
      Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
      柴进 committed
    • 粘贴闪退 (macOS 26):
      - _safe_get_clipboard_image 在 Darwin 上禁用 mimeData.imageData() /
        clipboard.image() / application/x-qt-image 三条 native crash 路径,
        统一走 image/* 原始字节 + osascript PNGf 兜底
      - DragDropScrollArea.dropEvent 的拖入图像分支同步做平台分流
      - Windows/Linux 路径完全保留,零回归
      
      长时间运行闪退:
      - init_logging 改用 RotatingFileHandler (5MB × 5),避免日志无限增长
      - 启动时清理超过 24 小时的 clipboard_*.png 遗留临时文件
      
      Gemini 返回空图片:
      - response_modalities 加上 TEXT,允许模型回传拒绝理由
      - response.parts 增加 None 保护,修复日志里 20+ 次
        'NoneType object is not iterable' 异常
      - 错误上浮 finish_reason + 模型说明到 QMessageBox
      
      缩略图拖拽重排:
      - 新增 DraggableThumbnail + THUMB_REORDER_MIME 内部协议
      - 缩略图可拖动调整顺序,reorder_image 正确处理左右移动的索引
      
      Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
      柴进 committed
  7. 19 Mar, 2026 2 commits