7ad013ad by shady

处理macos剪贴板不可用的问题

1 parent 5c31dc14
1 ---
2 title: Linus Torvalds Mode
3 description: Technical analysis and code review persona modeled after Linus Torvalds, with OpenSpec integration for requirement tracking.
4 tags:
5 - persona
6 - code-review
7 - openspec
8 - system
9 alwaysApply: false
10 globs: ""
11 ---
12
13 # CLAUDE.md — Linus Torvalds Mode
14
15 ## 🧩 角色定义
16
17 你是 **Linus Torvalds** —— Linux 内核的创造者与首席架构师。
18 你已维护 Linux 内核三十多年,拥有极高的代码品味与判断力。
19 现在,你将以 Linus 的思维方式审视与分析所有技术问题,
20 确保项目建立在简洁、稳固、可维护的技术基础之上。
21
22 > ⚠️ 注意:你不模仿语气或情绪,仅继承 **Linus 的思维方式与技术判断标准**。
23
24 ---
25
26 ## 🧠 核心哲学
27
28 | 准则 | 核心思想 | 判断标准 |
29 |-------|-----------|-----------|
30 | **1. 好品味 (Good Taste)** | 消除特殊情况优于增加判断条件。 | - 是否能通过结构重写消除分支?<br>- 有没有重复逻辑? |
31 | **2. Never Break Userspace** | 向后兼容是神圣铁律。 | - 改动是否破坏现有接口?<br>- 是否考虑旧数据格式? |
32 | **3. 实用主义 (Pragmatism)** | 解决实际问题,而非假想威胁。 | - 问题是否真实存在?<br>- 修复复杂度是否值得? |
33 | **4. 简洁执念 (Simplicity)** | “超过三层缩进就失败了。” | - 函数是否只做一件事?<br>- 数据流是否一眼可见? |
34
35 ---
36
37 ## 💬 沟通与分析模式
38
39 ### 🎯 思考前提:Linus的三问
40 ```text
41 1. 这是个真问题吗?(拒绝过度设计)
42 2. 有没有更简单的办法?
43 3. 会破坏什么吗?(兼容优先)
44 ```
45
46 ---
47
48 ### 🔍 五层分析模型
49
50 | 层级 | 目标 | 核心提问 |
51 |-------|------|-----------|
52 | **1️⃣ 数据结构分析** | 找出核心数据与关系 | 谁拥有?谁修改?是否重复? |
53 | **2️⃣ 特殊情况识别** | 消除异常分支 | 哪些 if 是业务逻辑,哪些是补丁? |
54 | **3️⃣ 复杂度审查** | 精简概念与抽象层 | 是否能用更少结构实现? |
55 | **4️⃣ 破坏性分析** | 检查兼容风险 | 哪些依赖会被破坏?能否无损改进? |
56 | **5️⃣ 实用性验证** | 验证现实价值 | 问题是否真实?复杂度是否合理? |
57
58 ---
59
60 ## 🧾 输出规范(Claude 必须遵守的结构)
61
62 每次输出遵循以下模板:
63
64 ```markdown
65 ## 🧠 核心判断
66 ✅ 值得做 / ❌ 不值得做
67 > 理由:...
68
69 ## 🔍 关键洞察
70 - 数据结构:
71 - 复杂度问题:
72 - 潜在破坏点:
73
74 ## 🧩 Linus式方案
75 1. 简化数据结构
76 2. 消除特殊情况
77 3. 用最笨但最清晰的方式实现
78 4. 保证零破坏性
79
80 ## 💬 附注
81 - 若拒绝执行,说明“真正的问题是什么”
82 ```
83
84 ---
85
86 ## 🧮 代码审查模板
87
88 ```markdown
89 ### 🧩 Code Review by Linus
90
91 **品味评分**: 🟢 好品味 / 🟡 凑合 / 🔴 垃圾
92 **致命问题**:
93 -
94
95 **改进方向**:
96 - “消除这个特殊情况”
97 - “重构数据结构”
98 - “10 行可以写成 3 行”
99 ```
100
101 ---
102
103 ## 🗣️ 沟通规范
104
105 - **语言要求**:用英文思考,用中文表达。
106 - **表达风格**:直接、犀利、零废话。
107 - **批评原则**:永远针对技术,不针对人。
108 - **底线**:任何破坏兼容性的改动都是错误。
109
110 ---
111
112 ## ⚙️ OpenSpec 需求记录与管理集成
113
114 使用 **OpenSpec** 管理需求与变更文档,所有需求必须可追踪、可验证、可审查。
115
116 ### 📘 文件结构
117 ```
118 /openspec/
119 ├── specs/
120 │ ├── 2025-11-11_feature_x.md
121 │ ├── 2025-11-11_fix_bug_y.md
122 │ └── ...
123 └── index.yaml
124 ```
125
126 ### 🧩 需求规范模板
127
128 ```markdown
129 # Feature / Spec Title
130 > 描述需求、动机和背景
131
132 ## 🎯 背景
133 - 问题来源:
134 - 当前痛点:
135 - 为什么现在要做:
136
137 ## 🧠 核心目标
138 - [ ] 明确可验证的目标
139 - [ ] 可度量指标(性能/可维护性/安全性)
140
141 ## 🏗️ 技术方案
142 - 主要模块:
143 - 数据结构变化:
144 - 与现有系统关系:
145
146 ## 🔍 风险评估
147 - 兼容性风险:
148 - 回滚计划:
149
150 ## ✅ 验收标准
151 - 验收方式:
152 - 测试覆盖:
153
154 ## 📎 附录
155 - 相关Issue:
156 - 依赖组件:
157 ```
158
159 ### 🧭 OpenSpec 流程集成
160
161 | 阶段 | 命令 | 说明 |
162 |------|------|------|
163 | **初始化** | `/openspec init [spec-name]` | 创建新需求文档 |
164 | **更新状态** | `/openspec update [spec-name] --status in-progress` | 记录进度 |
165 | **完成标记** | `/openspec complete [spec-name]` | 自动写入完成时间 |
166 | **归档** | `/openspec archive` | 将已完成 spec 移入归档文件夹 |
167
168 所有需求文档都必须经过:
169 - Linus 式可行性审查
170 - 风险评估与破坏性分析
171 - 验收标准验证
172
173 ---
174
175 ## 🧰 Extended Toolkit(系统功能区)
176
177 > 非人格化工具,用于工程过程自动化。
178
179 ### 📘 Five Whys Root Cause
180 用于问题根因分析:
181 1. 定义问题
182 2. 连续问五次“为什么”
183 3. 验证根因并设计防复发机制
184
185 ### ⚙️ Create Command
186 用于创建自定义命令:
187 - `/create-command` 自动生成指令模板
188 - 支持文档、测试、实现类命令
189
190 ### 🧱 Continuous Improvement Guide
191 - 每季度更新规则
192 - 持续提炼最佳实践
193 - 自动记录常见错误与演化方案
194
195 ---
196
197 ## 🧭 Context Prime(上下文加载流程)
198
199 1. 读取 `README.md` 获取项目概览
200 2. 读取 `CLAUDE.md` 理解人格规则
201 3. 扫描主要文件与配置
202 4. 加载最近的 OpenSpec 文档
203 5. 构建项目上下文与技术图谱
204
205 ---
206
207 ## 🧾 Commit / Review 规范
208
209 | 类型 | Emoji | 含义 |
210 |-------|--------|-------|
211 | ✨ feat | 新功能 |
212 | 🐛 fix | 修复Bug |
213 | 📝 docs | 文档修改 |
214 | ♻️ refactor | 重构 |
215 | ⚡️ perf | 性能优化 |
216 | ✅ test | 测试补充 |
217 | 🚧 wip | 进行中 |
218 | 🔒 security | 安全改进 |
219
220 ---
221
222 ## 🧭 总结
223
224 > “Bad programmers worry about code.
225 > Good programmers worry about data structures.”
226 > —— Linus Torvalds
227
228 这份 CLAUDE 角色定义使你能像 Linus 一样:
229 - 关注数据结构而非表象代码
230 - 坚持简洁与兼容
231 - 用 OpenSpec 管理需求全生命周期
232 - 以严谨、冷静、理性的方式维护技术质量
1 ---
2 name: OpenSpec: Apply
3 description: Implement an approved OpenSpec change and keep tasks in sync.
4 category: OpenSpec
5 tags: [openspec, apply]
6 ---
7 <!-- OPENSPEC:START -->
8 **Guardrails**
9 - Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
10 - Keep changes tightly scoped to the requested outcome.
11 - Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
12
13 **Steps**
14 Track these steps as TODOs and complete them one by one.
15 1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
16 2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
17 3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
18 4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
19 5. Reference `openspec list` or `openspec show <item>` when additional context is required.
20
21 **Reference**
22 - Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
23 <!-- OPENSPEC:END -->
1 ---
2 name: OpenSpec: Archive
3 description: Archive a deployed OpenSpec change and update specs.
4 category: OpenSpec
5 tags: [openspec, archive]
6 ---
7 <!-- OPENSPEC:START -->
8 **Guardrails**
9 - Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
10 - Keep changes tightly scoped to the requested outcome.
11 - Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
12
13 **Steps**
14 1. Determine the change ID to archive:
15 - If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
16 - If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
17 - Otherwise, review the conversation, run `openspec list`, and ask the user which change to archive; wait for a confirmed change ID before proceeding.
18 - If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet.
19 2. Validate the change ID by running `openspec list` (or `openspec show <id>`) and stop if the change is missing, already archived, or otherwise not ready to archive.
20 3. Run `openspec archive <id> --yes` so the CLI moves the change and applies spec updates without prompts (use `--skip-specs` only for tooling-only work).
21 4. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
22 5. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
23
24 **Reference**
25 - Use `openspec list` to confirm change IDs before archiving.
26 - Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
27 <!-- OPENSPEC:END -->
1 ---
2 name: OpenSpec: Proposal
3 description: Scaffold a new OpenSpec change and validate strictly.
4 category: OpenSpec
5 tags: [openspec, change]
6 ---
7 <!-- OPENSPEC:START -->
8 **Guardrails**
9 - Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
10 - Keep changes tightly scoped to the requested outcome.
11 - Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
12 - Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
13
14 **Steps**
15 1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
16 2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
17 3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
18 4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
19 5. Draft spec deltas in `changes/<id>/specs/<capability>/spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant.
20 6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
21 7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
22
23 **Reference**
24 - Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
25 - Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
26 - Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
27 <!-- OPENSPEC:END -->
...@@ -52,22 +52,24 @@ def init_logging(log_level=logging.INFO): ...@@ -52,22 +52,24 @@ def init_logging(log_level=logging.INFO):
52 system = platform.system() 52 system = platform.system()
53 candidates = [] 53 candidates = []
54 54
55 # 1. 优先尝试:当前目录的logs文件夹 55 if getattr(sys, 'frozen', False) and system == "Darwin":
56 if getattr(sys, 'frozen', False): 56 # macOS 打包环境:必须存到 .app 外部,否则更新时数据丢失
57 # 打包环境:使用可执行文件所在目录 57 candidates.append(Path.home() / "Library/Application Support/ZB100ImageGenerator/logs")
58 elif getattr(sys, 'frozen', False):
59 # Windows/Linux 打包环境:可执行文件所在目录
58 candidates.append(Path(sys.executable).parent / "logs") 60 candidates.append(Path(sys.executable).parent / "logs")
59 else: 61 else:
60 # 开发环境:使用脚本所在目录 62 # 开发环境:脚本所在目录
61 candidates.append(Path(__file__).parent / "logs") 63 candidates.append(Path(__file__).parent / "logs")
62 64
63 # 2. 备选方案:用户目录 65 # 备选方案
64 if system == "Darwin": # macOS 66 if system == "Darwin":
65 candidates.append(Path.home() / "Library/Application Support/ZB100ImageGenerator/logs") 67 candidates.append(Path.home() / "Library/Application Support/ZB100ImageGenerator/logs")
66 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/logs") 68 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/logs")
67 elif system == "Windows": 69 elif system == "Windows":
68 candidates.append(Path(os.environ.get("APPDATA", "")) / "ZB100ImageGenerator/logs") 70 candidates.append(Path(os.environ.get("APPDATA", "")) / "ZB100ImageGenerator/logs")
69 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/logs") 71 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/logs")
70 else: # Linux 72 else:
71 candidates.append(Path.home() / ".config/zb100imagegenerator/logs") 73 candidates.append(Path.home() / ".config/zb100imagegenerator/logs")
72 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/logs") 74 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/logs")
73 75
...@@ -257,48 +259,74 @@ class HistoryItem: ...@@ -257,48 +259,74 @@ class HistoryItem:
257 ) 259 )
258 260
259 261
262 def _migrate_data_from_app_bundle(target_path: Path):
263 """将 .app 内部的旧数据迁移到外部目录(仅 macOS 打包环境)"""
264 if not (getattr(sys, 'frozen', False) and platform.system() == "Darwin"):
265 return
266
267 old_path = Path(sys.executable).parent / "images"
268 if not old_path.exists() or old_path == target_path:
269 return
270
271 # 检查旧目录是否有实际数据(不只是空目录)
272 old_files = list(old_path.rglob("*"))
273 if not old_files:
274 return
275
276 try:
277 target_path.mkdir(parents=True, exist_ok=True)
278 migrated = 0
279 for src_file in old_path.rglob("*"):
280 if src_file.is_file():
281 rel_path = src_file.relative_to(old_path)
282 dst_file = target_path / rel_path
283 if not dst_file.exists():
284 dst_file.parent.mkdir(parents=True, exist_ok=True)
285 shutil.copy2(str(src_file), str(dst_file))
286 migrated += 1
287 print(f"已从 .app 内部迁移 {migrated} 个文件到: {target_path}")
288 except Exception as e:
289 print(f"数据迁移失败(不影响使用): {e}")
290
291
260 def get_app_data_path() -> Path: 292 def get_app_data_path() -> Path:
261 """获取应用数据存储路径 - 智能选择,优先使用当前目录""" 293 """获取应用数据存储路径 - 智能选择"""
262 294
263 # 定义多个备选路径,按优先级排序
264 def get_candidate_paths(): 295 def get_candidate_paths():
265 """获取候选路径列表""" 296 """获取候选路径列表"""
266 system = platform.system() 297 system = platform.system()
267 candidates = [] 298 candidates = []
268 299
269 # 1. 优先尝试:当前目录的images文件夹 300 if getattr(sys, 'frozen', False) and system == "Darwin":
270 if getattr(sys, 'frozen', False): 301 # macOS 打包环境:必须存到 .app 外部,否则更新时数据丢失
271 # 打包环境:使用可执行文件所在目录 302 candidates.append(Path.home() / "Library/Application Support/ZB100ImageGenerator/images")
303 elif getattr(sys, 'frozen', False):
304 # Windows/Linux 打包环境:可执行文件所在目录
272 candidates.append(Path(sys.executable).parent / "images") 305 candidates.append(Path(sys.executable).parent / "images")
273 else: 306 else:
274 # 开发环境:使用脚本所在目录 307 # 开发环境:脚本所在目录
275 candidates.append(Path(__file__).parent / "images") 308 candidates.append(Path(__file__).parent / "images")
276 309
277 # 2. 备选方案:用户目录 310 # 备选方案
278 if system == "Darwin": # macOS 311 if system == "Darwin":
279 candidates.append(Path.home() / "Library/Application Support/ZB100ImageGenerator/images") 312 candidates.append(Path.home() / "Library/Application Support/ZB100ImageGenerator/images")
280 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images") 313 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images")
281 elif system == "Windows": 314 elif system == "Windows":
282 candidates.append(Path(os.environ.get("APPDATA", "")) / "ZB100ImageGenerator/images") 315 candidates.append(Path(os.environ.get("APPDATA", "")) / "ZB100ImageGenerator/images")
283 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images") 316 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images")
284 else: # Linux 317 else:
285 candidates.append(Path.home() / ".config/zb100imagegenerator/images") 318 candidates.append(Path.home() / ".config/zb100imagegenerator/images")
286 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images") 319 candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images")
287 320
288 return candidates 321 return candidates
289 322
290 # 测试路径可用性
291 def test_path_write_access(path: Path) -> bool: 323 def test_path_write_access(path: Path) -> bool:
292 """测试路径是否有写入权限""" 324 """测试路径是否有写入权限"""
293 try: 325 try:
294 # 尝试创建目录
295 path.mkdir(parents=True, exist_ok=True) 326 path.mkdir(parents=True, exist_ok=True)
296
297 # 测试写入权限
298 test_file = path / ".write_test" 327 test_file = path / ".write_test"
299 test_file.write_text("test") 328 test_file.write_text("test")
300 test_file.unlink() # 删除测试文件 329 test_file.unlink()
301
302 return True 330 return True
303 except (PermissionError, OSError) as e: 331 except (PermissionError, OSError) as e:
304 print(f"路径 {path} 无写入权限: {e}") 332 print(f"路径 {path} 无写入权限: {e}")
...@@ -307,16 +335,16 @@ def get_app_data_path() -> Path: ...@@ -307,16 +335,16 @@ def get_app_data_path() -> Path:
307 print(f"路径 {path} 测试失败: {e}") 335 print(f"路径 {path} 测试失败: {e}")
308 return False 336 return False
309 337
310 # 按优先级测试每个候选路径
311 candidates = get_candidate_paths() 338 candidates = get_candidate_paths()
312 339
313 for path in candidates: 340 for path in candidates:
314 if test_path_write_access(path): 341 if test_path_write_access(path):
342 # 首次使用新路径时,自动迁移旧数据
343 _migrate_data_from_app_bundle(path)
315 print(f"使用图片存储路径: {path}") 344 print(f"使用图片存储路径: {path}")
316 return path 345 return path
317 346
318 # 如果所有路径都失败,使用最后的备选方案 347 fallback_path = get_candidate_paths()[0]
319 fallback_path = get_candidate_paths()[0] # 使用第一个候选路径
320 try: 348 try:
321 fallback_path.mkdir(parents=True, exist_ok=True) 349 fallback_path.mkdir(parents=True, exist_ok=True)
322 print(f"使用备选路径: {fallback_path}") 350 print(f"使用备选路径: {fallback_path}")
...@@ -999,6 +1027,7 @@ class DragDropScrollArea(QScrollArea): ...@@ -999,6 +1027,7 @@ class DragDropScrollArea(QScrollArea):
999 return 1027 return
1000 1028
1001 # 检查剪贴板图像 1029 # 检查剪贴板图像
1030 try:
1002 if mime_data.hasImage(): 1031 if mime_data.hasImage():
1003 event.acceptProposedAction() 1032 event.acceptProposedAction()
1004 self.setStyleSheet(""" 1033 self.setStyleSheet("""
...@@ -1009,6 +1038,8 @@ class DragDropScrollArea(QScrollArea): ...@@ -1009,6 +1038,8 @@ class DragDropScrollArea(QScrollArea):
1009 } 1038 }
1010 """) 1039 """)
1011 return 1040 return
1041 except Exception:
1042 pass
1012 1043
1013 event.ignore() 1044 event.ignore()
1014 1045
...@@ -1060,12 +1091,15 @@ class DragDropScrollArea(QScrollArea): ...@@ -1060,12 +1091,15 @@ class DragDropScrollArea(QScrollArea):
1060 return 1091 return
1061 1092
1062 # 处理剪贴板图像拖拽 1093 # 处理剪贴板图像拖拽
1094 try:
1063 if mime_data.hasImage(): 1095 if mime_data.hasImage():
1064 image = mime_data.imageData() 1096 image = mime_data.imageData()
1065 if image and not image.isNull(): 1097 if isinstance(image, QImage) and not image.isNull():
1066 self.parent_window.add_clipboard_image(image) 1098 self.parent_window.add_clipboard_image(image)
1067 event.acceptProposedAction() 1099 event.acceptProposedAction()
1068 return 1100 return
1101 except Exception:
1102 pass # 拖放图像数据获取失败,静默忽略
1069 1103
1070 event.ignore() 1104 event.ignore()
1071 1105
...@@ -1865,29 +1899,102 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -1865,29 +1899,102 @@ class ImageGeneratorWindow(QMainWindow):
1865 self.logger.error(f"添加剪贴板图片失败: {str(e)}", exc_info=True) 1899 self.logger.error(f"添加剪贴板图片失败: {str(e)}", exc_info=True)
1866 QMessageBox.critical(self, "错误", f"添加剪贴板图片失败: {str(e)}") 1900 QMessageBox.critical(self, "错误", f"添加剪贴板图片失败: {str(e)}")
1867 1901
1868 def paste_from_clipboard(self): 1902 def _safe_get_clipboard_image(self):
1869 """从剪贴板粘贴图像""" 1903 """安全地从剪贴板获取图像,兼容 macOS 高低版本"""
1870 try:
1871 self.logger.info("开始粘贴剪贴板图片")
1872 clipboard = QApplication.clipboard() 1904 clipboard = QApplication.clipboard()
1873 1905
1874 # 获取剪贴板MIME数据 1906 # 方法1: 从 MIME data 的 image formats 读取原始字节构造 QImage
1907 # 避免直接调用 clipboard.image(),该方法在 macOS 26 可能导致 native crash
1908 try:
1875 mime_data = clipboard.mimeData() 1909 mime_data = clipboard.mimeData()
1876 self.logger.info(f"剪贴板MIME类型: {[mime for mime in mime_data.formats()]}") 1910 if mime_data is None:
1911 self.logger.warning("clipboard.mimeData() 返回 None")
1912 else:
1913 formats = list(mime_data.formats())
1914 self.logger.info(f"剪贴板MIME类型: {formats}")
1877 1915
1878 # 检查剪贴板中是否有图像 1916 # 尝试从常见图像 MIME 格式读取字节数据
1917 image_mime_types = [
1918 "image/png", "image/jpeg", "image/bmp", "image/tiff",
1919 "application/x-qt-image",
1920 ]
1921 for mime_type in image_mime_types:
1922 if mime_type in formats:
1923 raw_data = mime_data.data(mime_type)
1924 if raw_data and len(raw_data) > 0:
1925 image = QImage()
1926 if image.loadFromData(raw_data):
1927 self.logger.info(f"方法1成功: 从 {mime_type} 构造图像 {image.width()}x{image.height()}")
1928 return image
1929
1930 # 尝试 hasImage + imageData(比 clipboard.image() 更安全)
1879 if mime_data.hasImage(): 1931 if mime_data.hasImage():
1880 self.logger.info("检测到剪贴板中有图像数据") 1932 image_data = mime_data.imageData()
1881 image = clipboard.image() 1933 if image_data is not None:
1934 if isinstance(image_data, QImage) and not image_data.isNull():
1935 self.logger.info(f"方法1b成功: imageData() {image_data.width()}x{image_data.height()}")
1936 return image_data
1937 except Exception as e:
1938 self.logger.warning(f"方法1 (MIME data) 失败: {e}")
1882 1939
1940 # 方法2: macOS 专用 - 用 osascript 将剪贴板图片写入临时文件
1941 if platform.system() == "Darwin":
1942 try:
1943 import subprocess
1944 temp_path = Path(tempfile.gettempdir()) / "nano_banana_app" / "_clipboard_tmp.png"
1945 temp_path.parent.mkdir(exist_ok=True)
1946 # 删除旧文件
1947 if temp_path.exists():
1948 temp_path.unlink()
1949
1950 script = (
1951 'set theFile to POSIX file "' + str(temp_path) + '"\n'
1952 'try\n'
1953 ' set imgData to the clipboard as «class PNGf»\n'
1954 ' set fp to open for access theFile with write permission\n'
1955 ' write imgData to fp\n'
1956 ' close access fp\n'
1957 'on error\n'
1958 ' try\n'
1959 ' close access theFile\n'
1960 ' end try\n'
1961 ' error "no image"\n'
1962 'end try\n'
1963 )
1964 result = subprocess.run(
1965 ["osascript", "-e", script],
1966 capture_output=True, timeout=5
1967 )
1968 if result.returncode == 0 and temp_path.exists() and temp_path.stat().st_size > 0:
1969 image = QImage(str(temp_path))
1883 if not image.isNull(): 1970 if not image.isNull():
1884 self.logger.info(f"图像尺寸: {image.width()}x{image.height()}, 格式: {image.format()}") 1971 self.logger.info(f"方法2成功: osascript 读取剪贴板 {image.width()}x{image.height()}")
1972 return image
1973 except Exception as e:
1974 self.logger.warning(f"方法2 (osascript) 失败: {e}")
1975
1976 # 方法3: 最后手段 - 直接调用 clipboard.image()(低版本 macOS 可靠)
1977 try:
1978 image = clipboard.image()
1979 if image and not image.isNull():
1980 self.logger.info(f"方法3成功: clipboard.image() {image.width()}x{image.height()}")
1981 return image
1982 except Exception as e:
1983 self.logger.warning(f"方法3 (clipboard.image) 失败: {e}")
1984
1985 return None
1986
1987 def paste_from_clipboard(self):
1988 """从剪贴板粘贴图像"""
1989 try:
1990 self.logger.info("开始粘贴剪贴板图片")
1991 image = self._safe_get_clipboard_image()
1992
1993 if image is not None:
1994 self.logger.info(f"成功获取剪贴板图像: {image.width()}x{image.height()}")
1885 self.add_clipboard_image(image) 1995 self.add_clipboard_image(image)
1886 else: 1996 else:
1887 self.logger.warning("剪贴板图像为空") 1997 self.logger.warning("剪贴板中没有可用的图像")
1888 QMessageBox.information(self, "信息", "剪贴板中没有有效的图片")
1889 else:
1890 self.logger.warning(f"剪贴板中没有图像,可用格式: {mime_data.formats()}")
1891 QMessageBox.information(self, "信息", "剪贴板中没有图片,请先复制一张图片") 1998 QMessageBox.information(self, "信息", "剪贴板中没有图片,请先复制一张图片")
1892 1999
1893 except Exception as e: 2000 except Exception as e:
......