- GUI 自动化
- MCP 服务
- AI 应用
【免费下载链接】Peekaboo
Peekaboo is a macOS CLI & optional MCP server that enables AI agents to capture screenshots of applications, or the entire system, with optional visual question answering through local or remote AI models.
本篇指南以 Peekaboo 仓库的 docs/modern-swift.md 为骨架,结合仓库内真实的 Swift 源码、SwiftLint/SwiftFormat 配置与测试代码展开。读者将掌握 Peekaboo 在 Swift 6.2 严格并发模式下的编码约定:视图状态如何放置、可观察模型如何组织、阻塞调用如何与异步边界隔离、以及在重构时如何守住"已发布 API 与线格式"的兼容底线。适合任何要为 Peekaboo 提交 Swift/SwiftUI 改动(无论是 macOS 应用、CLI 还是 Core 各模块)的开发者阅读。
一、总则:Swift 6.2 约定、显式self与工具链基线
Peekaboo 的 Swift 代码遵循Swift 6.2 约定,并强制在成员引用处使用显式self。文档明确要求:提交改动前必须运行仓库自带的 SwiftFormat 与 SwiftLint,同时不得为了某个重构能编译而擅自抬高部署目标——这条规则在 docs/platform-support.md 中有完整的基线矩阵。
1.1 工具链与部署目标基线
从仓库的包清单可以确认,所有 SwiftPM 包一律声明swift-tools-version: 6.2,例如:
- Apps/CLI/Package.swift
- Apps/Mac/Package.swift
- Core/PeekabooAutomationKit/Package.swift
- Core/PeekabooCore/Package.swift
- Core/PeekabooFoundation/Package.swift
platform-support.md 给出的基线为:
| 表面 | 最低要求 | 说明 |
|---|---|---|
| 已发布的 CLI 与 MCP 包 | macOS 15.0+ | Apps/CLI/Package.swift声明.macOS(.v15),npm 包包装同一 CLI 二进制 |
| macOS 应用 | macOS 15.0+ | Xcode 工程设置MACOSX_DEPLOYMENT_TARGET = 15.0 |
| 公共 SwiftPM 库 | macOS 14.0+(包元数据) | 根Package.swift导出PeekabooFoundation、PeekabooProtocols、PeekabooAutomationKit、PeekabooBridge |
| CLI 源码构建 | macOS 15.0+、Swift 6.2+ 工具链 | 维护者与 CI 使用 Xcode 26.x 或更新版本 |
因此,任何重构都应"就着现有地板"完成,而不是把 deployment target 抬高一档来换取编译器放行。
1.2 格式化与静态检查配置
仓库根目录的 .swiftformat 与 .swiftlint.yml 定义了唯一权威配置:
- SwiftFormat 使用
--swiftversion 6.2、--self insert(插入self,适配 Swift 6 并发要求的成员引用)、--maxwidth 120、4 空格缩进、before-first的参数换行风格、MARK: - %t + %p的类型/扩展标记; - SwiftLint 的
included覆盖Apps与Core,排除.build、DerivedData、**/Generated、**/Resources、Package.swift等;根配置通过parent_config: .swiftlint-rules.yml继承规则集(Apps/CLI下另有独立的 .swiftlint.yml 用于 CLI 目标); - 少数"必须保持原样"的文件被显式排除在格式化之外,例如
Core/PeekabooCore/Sources/PeekabooCore/Extensions/NSArray+Extensions.swift。
这条链路的用意很明确:格式与 lint 不是可选项,而是提交前必须通过的入口检查。
二、状态与视图:@State、@Binding、@Observable与@Bindable的摆放规则
文档给出的状态管理纪律可以概括为一句话:让状态靠近它的所有者。视图局部值放@State,可写值经@Binding下传,共享依赖走已有 environment 或初始化器注入。
2.1 视图局部状态:@State
Apps/Mac/Peekaboo/Features/Settings/SettingsWindow.swift 是教科书式的例子:
@State private var selectedTab: PeekabooSettingsTab = .general @State private var monitoringPermissions = false同一文件中还出现了典型的"一次性检测状态"(@State private var hasAttemptedOllamaDetection = false),说明@State只承载视图生命周期内、无共享需求的值。
2.2 可写值下传:@Binding
GeneralSettingsView把开关行拆成独立组件时,通过@Binding传递写权限:
@Binding var showInDock: Bool @Binding var isOn: Bool这正对应文档"pass writable values through@Binding"的约定——子视图只拿到"读写通道",不拥有数据本体。
2.3 可观察模型:@Observable+@MainActor
文档允许新代码使用 Observation 框架,同时要求:UI 持有的模型隔离到@MainActor,由拥有它的视图通过@State持有。仓库的落地非常一致:
- Apps/Mac/Peekaboo/Core/Settings.swift 注释明确写着 "This class uses the modern @Observable pattern for SwiftUI integration";
- Apps/Mac/Peekaboo/Core/ConversationSession.swift 是标准模板:
@Observable @MainActor final class SessionStore { var sessions: [ConversationSession] = [] var currentSession: ConversationSession? ... }- 同一目录下的
PeekabooAgent.swift、Permissions.swift、ProviderCredentialCoordinator.swift,以及 Features 中的AutomationTargetTracker.swift都采用相同的@Observable模式。
2.4 子视图取绑定:@Bindable
当子视图需要把可观察模型的属性变成绑定(例如切换开关)时,使用@Bindable。SettingsWindow.swift中典型的局部写法:
@Environment(PeekabooSettings.self) private var settings var body: some View { @Bindable var settings = self.settings Form { SettingsToggleRow(..., isOn: $settings.launchAtLogin) ... } }这里$settings.launchAtLogin正是由@Bindable提供的属性绑定。整套组合——@State持有、@Bindable取绑定、@Binding下传——就是文档所述"Keep state near its owner"在 Peekaboo 中的具体形态。
2.5 何时抽模型、何时抽视图
文档还有两条微妙的边界:
- 抽模型:当某段状态或行为需要被多处共享时,从视图里提炼出来;
- 抽视图:当某个子树有了清晰职责时再抽视图;
- 小视图不必配套模型:不要为了"对称"给每个小组件硬造一个 model 类型。
此外,已发布的ObservableObject契约要原样保留,除非这次改动明确迁移了它的调用方。这与下文"兼容性红线"一脉相承。
三、渲染与异步工作:.task、.task(id:)与TimelineView
3.1 渲染代码不得夹带 I/O
文档要求"rendering code free of I/O and repeated expensive work"。这是 SwiftUI 性能的基本盘:body只负责声明视图结构,真正的异步加载放.task。
3.2.task与.task(id:)的选用
.task:绑定视图生命周期的异步工作,视图消失即取消;.task(id:):当某个具体输入变化时应重启任务。
仓库中的真实用法在 SettingsWindow.swift:
.task(id: self.settings.ollamaBaseURL) { await self.refreshOllamaModels() }当用户修改 Ollama 服务地址时,模型列表的探测任务会随id变化自动重启——这正是"用id表达重启语义"的典型场景。对应的refreshOllamaModels()被标注为@MainActor,与@Observable模型的隔离保持一致。
3.3 时间驱动的呈现:TimelineView与事件流
文档给出了取舍原则:新代码的时间驱动呈现优先用TimelineView(如果其调度语义契合功能);当事件流本身就是真相源时,保留事件流。也就是说,不要为了"看起来新"而用TimelineView去重实现一个本就基于流订阅的界面。
3.4 取消与失败要分开处理
"Handle cancellation separately from a user-visible failure"——取消是调用方主动结束等待,不应被包装成一次面向用户的错误提示。这与下一节并发边界的原则互为表里。
四、并发与服务边界:尊重 actor 隔离,不为编译而作弊
4.1 禁止用nonisolated(unsafe)/@unchecked Sendable堵编译器
文档的态度非常明确:尊重 actor 隔离,而不是用nonisolated(unsafe)或@unchecked Sendable来"静音"编译器。当类型确实需要用锁或队列保护共享状态时:
- 所有对该状态的访问必须留在同步边界内;
- 必须在注释里说明为什么这个 unchecked 一致性是健全的(sound)。
仓库中那些合法的@unchecked Sendable都有明确理由。例如 ElementDetectionTimeoutRunner.swift 中的AXObservationWorkerPool:它用NSLock保护lanes字典,所有读写都经过self.lock.withLock { ... }(见其enqueue、complete、evictIdleLanesIfNeeded方法),因此@unchecked Sendable是有同步机制兜底的。同理,ScreenCaptureKitOwnerLease.swift 用NSLock保护nonisolated(unsafe) static var的进程描述符注册表,每个可变静态状态旁边都配一把专用锁。
4.2 actor 与跨边界值:优先Sendable结构体
跨 actor 与传输边界传递的数据应尽量是类型化的、Sendable的值。仓库中大量 actor 与异步服务之间交换的是显式Sendable结构体,例如:
- ActionVerifier.swift 的
ActionDescriptor/VerificationResult; - AgentCompatibilityTypes.swift 的
AgentExecutionResult; - AgentToolMCPBridge.swift 的
actor AgentToolMCPImageStore。
actor EventHandler(PeekabooAgentService+Execution.swift)则展示了"让状态归 actor、跨 actor 只传值"的经典布局。
4.3 关键洞察:async不会让阻塞调用变非阻塞
文档点出了 Peekaboo 并发模型中最反直觉的一条:把阻塞性 Accessibility 或 socket 调用包进async函数,并不会让它变成非阻塞。Accessibility RPC 与 socket 读写在原生层面是不协作取消的,取消只能结束调用方等待,不能中断后台原生工作。
因此 Peekaboo 在这些边界使用有界的原生 worker 与传输队列,并完整保留"入场(admission)、截止时间(deadline)、清理(cleanup)"规则。最完整的实例是 ElementDetectionTimeoutRunner.swift:
run(seconds:operation:)用withCheckedThrowingContinuation装配一个"截止时间状态机",主操作放Task { @MainActor ... },而超时计时器刻意放在Task.detached——注释明确写着 "The timer must not inherit MainActor",防止同步的 MainActor 操作靠饿死 deadline 任务来"假成功";runDetached(...)把非变异 AX 工作投递到per-process 串行 lane(AXObservationWorkerPool按pid + processStartIdentity建 lane,每个 lane 一个DispatchQueue),并支持maximumPendingOperationCount限流与空闲 lane 驱逐(上限 64);- 每个 AX 调用有软/硬两级超时(DetachedAXWindowEnumerationTiming:硬超时减去 0.01~0.25s 的完成宽限得到软超时),超时后立即恢复调用方,绝不 join 仍在飞行中的 AX RPC。
这就是"async函数不会让阻塞调用非阻塞"的正解:在原生边界保留有界 worker 和专用队列,把"等待"与"原生工作"彻底分离。
4.4 结构化并发与 detached task 的所有权
文档规定:子工作的生命周期属于调用方时,优先用结构化并发;Task.detached必须有明确的生命周期与取消所有者。上述超时任务是有意脱离调用方生命周期(它必须独立于 MainActor 运行)的少数例外,且每个 detached task 都在onCancel中通过state.cancel()收尾。
4.5 模块职责边界
- 生产自动化逻辑放在AutomationKit;
- Agent / MCP 适配放在AgentRuntime;
- 共享可视组件放在UICore 或 Visualizer。
从目录结构看,Core/PeekabooAutomationKit(自动化原语)、Core/PeekabooCore/Sources/PeekabooAgentRuntime(Agent 运行时)、Core/PeekabooUICore/Core/PeekabooVisualizer(UI 与可视化)正是这条分层的物理体现。
五、验证纪律:在所有权边界测试,不靠 sleep
5.1 测试应落在行为的"所属边界"
文档的验证原则是"Exercise behavior at its owning boundary":能注入时钟、服务、临时存储的接缝处就注入;用可观察的完成等待结果,而不是sleep一段估算时长。
仓库测试大量使用时钟注入来避免睡眠:Core/PeekabooAutomationKit中的ScreenCaptureKitCaptureGate、ScreenCaptureKitWindowPlanCache等实现都以ContinuousClock.Instant作为截止时间输入(见 ScreenCaptureKitCaptureGate.swift 与 ScreenCaptureKitWindowPlanCache.swift),测试据此可以精确控制"现在时刻"来验证过期与超时路径。
5.2 已发布 API 与线格式的测试必须保留
重构时,已发布的 API 与已确定线格式(wire format)的测试不得删除。这条规则在 docs/dev/lint-compatibility.md 中有严格登记:
DesktopTargetEvidenceAdapter.evidence(processIdentifier:...)(DesktopTargetEvidenceAdapter.swift)因六个独立输入是 v4.2.2 发布契约,持有唯一一个function_parameter_count豁免,且由版本选择的 SwiftPM 消费者 fixture(scripts/test-swiftpm-consumer.sh)做编译验证;- 三个用
@available(*, deprecated)上下文故意调用 v4.2.2 已废弃公共 API 的测试声明(PeekabooBridgeAppleScriptCompatibilityTests、InMemorySnapshotManagerTests的两个 legacy initializer 用例)被点名保留,豁免范围只限这三处声明本身,不授权整文件注释、编译警告抑制或兼容垫片。
5.3 提交前的验证流程
文档给出的最终检查清单是:
- 构建受影响的包并运行相关测试;
- UI 行为改动还要运行构建出的应用、对照受控 fixture 验证可见结果——
SwiftUI previews只辅助布局,不能证明生命周期、权限或自动化正确性; - 遵循 docs/building.md 与
docs/testing/下的相应契约完成检查。
六、总结:一份可执行的"改动前 Checklist"
把整份规范浓缩成提交前的自查清单:
- 工具链为 Swift 6.2,改动不抬高任何包的 deployment target(对照 docs/platform-support.md);
- 成员引用显式写
self,代码通过仓库 .swiftformat 与 .swiftlint.yml 检查; - 视图局部值用
@State,可写值用@Binding下传,共享依赖走 environment/初始化器注入; - 新可观察模型用
@Observable+@MainActor,由@State持有;子视图绑定用@Bindable;既有ObservableObject公共契约未动; - 渲染代码无 I/O;异步工作挂
.task/.task(id:),取消与用户可见失败分开处理; - 没有用
nonisolated(unsafe)/@unchecked Sendable堵编译器;锁保护的共享状态全部访问都在同步边界内,unchecked 一致性有注释论证; - 阻塞的 AX/socket 调用走有界原生 worker 与传输队列,worker 的入场/截止时间/清理规则原样保留;
- 子工作优先结构化并发;detached task 有明确生命周期与取消所有者;
- 测试落在所有权边界,用注入的时钟/服务/临时存储,用可观察完成代替 sleep;
- 已发布 API 与线格式测试完整保留,lint 豁免符合 docs/dev/lint-compatibility.md 的登记范围;
- 构建受影响包、跑相关测试;UI 改动再用受控 fixture 跑真实应用验证。
这份指南既是新人快速进入 Peekaboo 的编码入口,也是老改动合入前唯一的"合规裁判"。遵循它,你的改动才能同时通过编译器、SwiftLint、测试与发布契约四道关卡。
- GUI 自动化
- MCP 服务
- AI 应用
【免费下载链接】Peekaboo
Peekaboo is a macOS CLI & optional MCP server that enables AI agents to capture screenshots of applications, or the entire system, with optional visual question answering through local or remote AI models.
相关推荐
Amaze File Manager 的 AI 代码贡献指南:从提交纪律到测试验证的完整开发规范
Amaze File Manager 的 AI 代码贡献指南:从提交纪律到测试验证的完整开发规范 本文基于仓库根目录的 CLAUDE.md https://li
移动开发妙言 MiaoYan Swift 开发规范实战指南:UI 框架边界、线程纪律与 CJK 文本处理
妙言 MiaoYan Swift 开发规范实战指南:UI 框架边界、线程纪律与 CJK 文本处理 本篇技术指南围绕妙言(MiaoYan)开源仓库中的 .clau
桌面应用CLIJSHint 与 Redux:状态管理库的代码规范
JSHint 与 Redux:状态管理库的代码规范 你是否曾在 Redux 项目中遇到过这些问题:Action 命名混乱导致调试困难?Reducer 函数中出现
Lint静态分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考