1. 项目概述:一个AI编码工具的“中央控制台”
如果你和我一样,日常开发中同时用着Cursor、Claude Code,可能还有GitHub Copilot或者一些本地部署的模型工具,那你一定对下面这个场景不陌生:为了给某个项目配置AI助手的行为,你需要在项目根目录下创建一个.cursorrules文件;同时,为了确保Claude Code在同一个项目里也能遵循类似的规范,你又得去维护一个CLAUDE.md。这还没完,你可能会在~/.cursor目录下放一些全局的钩子脚本,在~/.config/claude里存一些自定义技能模板。几天后,当你切换到另一个项目,或者想复用某个配置时,记忆就开始模糊了——那个好用的代码审查规则模板到底放在哪个工具的哪个目录下了?
这种配置的碎片化和管理上的心智负担,正是我开发Relay这个macOS应用的初衷。Relay本质上是一个本地的、图形化的“配置管理中心”,它自动扫描并聚合了你系统中所有主流AI编码工具(目前主要支持Cursor、Claude Code及其相关生态)的配置文件。无论是项目级的规则文件(如.cursorrules),还是用户全局的配置、钩子、技能定义,它都能在一个简洁的三栏视图中呈现给你。你不用再记忆路径,不用在终端里反复cd和cat,更不用手动在不同工具间同步相似的配置内容。它的目标很单纯:让你管理AI助手的“知识”和“行为准则”,像管理代码本身一样清晰、高效。
Relay完全免费并开源,基于Swift和SwiftUI构建,这确保了它在macOS上的原生体验和性能。它不收集任何数据,所有操作都在本地完成,你的配置隐私得到充分保障。接下来,我会详细拆解它的设计思路、实现细节,并分享在开发这样一个“元工具”(管理工具的工具)过程中积累的经验和踩过的坑。
2. 核心设计思路与架构解析
2.1 解决的核心痛点:配置散落与认知断层
在深入代码之前,我们先明确问题域。现代AI编码工具通常采用一种“配置即约定”的模式,其配置层级大致可分为:
- 全局配置:位于用户主目录下的隐藏文件夹中,如
~/.cursor、~/.config/claude。这里存放着影响该工具在所有项目中行为的设置、自定义命令、全局钩子脚本等。 - 项目级配置:位于具体项目根目录下的特定文件,如
.cursorrules、CLAUDE.md、AGENTS.md。这些文件定义了AI在该项目上下文中的行为规范、代码风格、禁止操作等。 - 模板与预设:用户可能会积累一些常用的规则模板,例如“严格的代码审查模式”、“快速原型开发模式”。这些模板目前缺乏统一的管理,往往以文本片段的形式散落在各处。
痛点由此产生:配置分散导致查找困难;格式相似但工具独立导致同步成本高(在Cursor里改了个规则,可能忘了在Claude的配置里也更新);缺乏全局视角使得你很难回答“我当前所有AI工具加起来,被赋予了哪些知识和限制?”。
Relay的设计哲学是“聚合与可视化”。它不试图取代任何AI工具本身的配置系统,而是作为一个只读(或辅助编辑)的“仪表盘”,将这些分散的配置源聚合起来,提供一个统一的查看、编辑与管理入口。这类似于IDE将不同语言、不同构建工具的配置文件在一个项目视图中统一管理。
2.2 技术选型:为什么是Swift/SwiftUI?
作为一个专注macOS的桌面应用,选型几乎是必然的:
- Swift/SwiftUI:这是开发现代macOS原生应用的标杆组合。Swift提供了高性能和内存安全,SwiftUI则用声明式语法极大地简化了UI构建。对于Relay这种数据驱动、视图相对标准的工具类应用,SwiftUI的
List、NavigationSplitView(用于实现三栏布局)、TextEditor等组件能事半功倍。更重要的是,它能天然地适配macOS的系统外观(深色/浅色模式)、响应手势,并提供流畅的动画。 - 原生而非跨平台:虽然Electron等框架可以跨平台,但它们带来的内存开销和非原生体验与Relay追求的“轻量、快捷”背道而驰。Relay的目标是成为一个即开即用、毫无存在感的效率工具,原生开发能确保其启动速度、响应速度和系统集成度(如文件系统监听、菜单栏扩展潜力)达到最佳。
- 开源与社区:选择Swift生态也便于吸引macOS开发者社区的关注和贡献。依赖管理使用Swift Package Manager (SPM),项目结构清晰,任何人都可以轻松地
git clone后打开Relay.xcodeproj进行编译或修改。
2.3 应用架构概览
Relay的架构遵循了典型的macOS应用模型,并清晰分层:
Relay App ├── 表示层 (Presentation Layer) │ ├── Views (SwiftUI): MainView, SidebarView, DetailView, EditorView │ └── ViewModels: 连接模型层与视图,处理UI逻辑 ├── 业务逻辑层 (Business Logic Layer) │ ├── ConfigScanner: 核心扫描器,负责发现和解析配置 │ ├── RuleFileManager: 管理规则文件的CRUD操作 │ └── TemplateManager: 处理预设模板的保存与应用 ├── 模型层 (Model Layer) │ ├── ConfigItem: 配置项的数据模型(类型、路径、内容、所属工具) │ ├── AITool: 枚举定义支持的AI工具(Cursor, Claude等) │ └── Project: 已注册的项目文件夹信息 └── 持久层 (Persistence Layer) ├── UserDefaults: 存储用户偏好(如注册的项目路径、窗口状态) └── File System: 所有配置文件的真实存储,Relay本身不另存副本关键的设计在于ConfigScanner。它不是一个简单的文件遍历器。它在启动时,以及受用户操作(如刷新)或系统事件(如监听的文件变更)触发时,执行以下任务:
- 定位配置源:根据预定义的知识(如
~/.cursor是Cursor的全局配置目录),结合用户手动注册的项目路径,构建待扫描的目录列表。 - 模式匹配:在每个目录中,使用文件扩展名和文件名模式(如
*.json,.cursorrules,CLAUDE.md)来识别潜在的配置文件。 - 解析与分类:读取文件内容(对于文本文件),或解析其结构(对于JSON/YAML),将其封装成统一的
ConfigItem模型。同时,根据文件路径和命名规则,判断该配置项属于哪个AITool,以及它是“全局配置”还是“项目级规则”。 - 通知更新:将扫描结果传递给
ViewModel,驱动UI更新。
这个架构保证了核心的扫描逻辑与UI展示解耦,便于未来扩展支持新的AI工具(只需在AITool枚举和ConfigScanner的识别逻辑中添加新条目)。
3. 关键功能实现细节与实操
3.1 三栏主界面:信息架构的设计
主界面采用NavigationSplitView实现三栏布局,这是macOS文件管理器(Finder)和很多专业应用(如Xcode)的经典布局,符合用户直觉。
第一栏(侧边栏):作为导航的“锚点”,提供了两个主要视角。
- 工具视角:按AI工具(如Cursor, Claude)分组。选择某个工具后,第二栏会显示该工具下的所有配置类型(全局设置、规则文件、钩子等)。
- 项目视角:显示所有用户手动“注册”的项目文件夹。选择某个项目后,第二栏会显示在该项目根目录下发现的所有AI工具配置文件(例如,同时显示该项目下的
.cursorrules和CLAUDE.md)。 - 这种双视角设计至关重要,它允许用户既可以从“工具维度”去管理某一类AI的所有配置,也可以从“项目维度”去统一查看和编辑某个项目下的所有AI约束。
第二栏(内容列表):根据第一栏的选择,动态列出相关的配置项列表。每个列表项显示文件名、所在路径的简短表示以及一个图标(根据文件类型)。这里的一个细节是,对于项目下的文件,我们会显示相对于项目根目录的路径;对于全局文件,则显示从用户主目录(
~)开始的路径,以增强可读性。第三栏(详情/编辑区):这是与配置内容交互的核心区域。当在第二栏选中一个文件后,第三栏会呈现以下两种视图之一:
- 预览模式:对于文本文件(Markdown, YAML, JSON等),会提供一个语法高亮的只读预览。这对于快速浏览、确认内容非常方便,避免了误操作。
- 编辑模式:用户点击“编辑”按钮后,界面会切换为一个功能完整的文本编辑器(基于
TextEditor并集成了一些如Monaco Editor的简单语法高亮逻辑)。编辑完成后,保存操作会直接写入原文件。Relay本身不存储文件副本,它只是一个“通道”。
实操心得:文件系统监听
为了让应用状态与真实文件系统同步,我们实现了文件系统事件监听(使用DispatchSourceFileSystemObject或第三方库如FileWatcher)。当用户通过Relay或其他方式(如终端Vim)修改了某个正在被管理的配置文件时,Relay能实时感知到变化,并更新UI中的预览内容。这个功能看似细小,但对维持用户“信任感”至关重要——用户必须确信他在Relay里看到的就是磁盘上最新的内容。实现时要注意性能,避免对同一文件的频繁修改导致事件风暴,通常需要做一个短时间的防抖(Debounce)处理。
3.2 配置的自动发现与解析逻辑
ConfigScanner的自动发现能力是Relay的“魔法”所在。其逻辑如下:
构建扫描路径库:
- 固定全局路径:硬编码已知的AI工具全局配置目录,如:
let globalPaths: [AITool: [String]] = [ .cursor: ["~/.cursor", "~/Library/Application Support/Cursor"], .claude: ["~/.config/claude", "~/Library/Application Support/Claude"], ] - 用户注册的项目路径:从
UserDefaults中读取用户之前通过“添加项目文件夹”操作注册的路径列表。
- 固定全局路径:硬编码已知的AI工具全局配置目录,如:
执行扫描:对上述每一个路径,异步执行文件遍历。使用
FileManager的enumerator方法,并设置skipsHiddenFiles为false(因为很多配置是隐藏文件),同时skipsPackageDescendants为true(避免进入.app等包内部)。智能识别:对于遍历到的每一个文件,通过其路径和名称进行匹配:
func classifyFile(at path: String) -> ConfigItem? { let url = URL(fileURLWithPath: path) let fileName = url.lastPathComponent // 识别工具 var tool: AITool? = nil if path.contains(".cursor") { tool = .cursor } else if path.contains("claude") || fileName == "CLAUDE.md" { tool = .claude } // ... 其他工具识别逻辑 // 识别类型 var type: ConfigType = .unknown if fileName.hasPrefix(".") && fileName.contains("rules") { type = .projectRule } else if fileName.hasSuffix(".md") && (fileName == "AGENTS.md" || fileName == "CLAUDE.md") { type = .projectRule } else if url.deletingLastPathComponent().lastPathComponent == "hooks" { type = .hook } else if fileName == "settings.json" { type = .globalSetting } // ... 其他类型识别逻辑 guard let tool = tool, type != .unknown else { return nil } return ConfigItem(tool: tool, type: type, fileURL: url) }这个过程允许一定程度的误报,但核心是“宁可多扫,不可漏扫”。在UI列表中可以允许用户手动隐藏某些误识别的文件。
内容提取与索引:对于识别出的文件,尝试读取其内容。对于文本文件,读取全文;对于大型或二进制文件,可能只读取元数据。这些内容会被缓存在内存中,用于快速预览和搜索。
3.3 预设模板功能:实现“配置即代码”的复用
这是Relay提升效率的关键功能。用户可以将一个编辑好的规则文件(例如,一个精心调校的代码审查规则集)保存为“预设模板”。
- 实现机制:
TemplateManager负责处理模板的存储。模板本身以纯文本文件的形式保存在一个固定的应用支持目录下(如~/Library/Application Support/com.yourname.Relay/Templates/),每个模板一个文件。同时,用一个templates.json的索引文件来记录模板的元数据(名称、描述、适用的原始工具、创建时间等)。 - 应用模板:当用户在一个项目(或全局配置目录)中想要创建新规则文件,或覆盖现有文件时,可以从模板列表中选择。操作实质上是文件内容的复制。但这里有一个进阶处理:Relay会解析模板内容中可能存在的“占位符变量”,例如
{{project_name}},并在应用时将其替换为当前项目的实际名称。这借鉴了基础设施即代码(IaC)工具的思想,让配置更具动态性。 - 版本管理的联想:虽然Relay本身不提供复杂的版本管理,但因为它管理的文件本身就是纯文本,且位于你的项目目录或标准配置目录中,它们天然就可以被Git等版本控制系统管理。Relay的模板功能可以看作是你的AI配置的“代码片段库”,而Git则是这个库的版本历史记录者。我强烈建议用户将重要的规则模板文件也纳入Git管理。
4. 开发历程中的挑战与解决方案
4.1 挑战一:沙盒限制与文件系统访问
macOS应用商店(Mac App Store)的应用默认运行在沙盒中,这极大地限制了其对用户文件系统的自由访问。Relay需要扫描用户主目录和任意项目文件夹,这与沙盒的严格权限模型冲突。
- 解决方案:我们选择了非沙盒化分发。这意味着Relay主要通过GitHub等渠道分发,用户下载的是经过公证(Notarized)的
.dmg或直接是.app包。这样应用可以获得必要的文件系统访问权限。为了获得这些权限,应用需要在首次访问用户指定目录时,通过系统的NSOpenPanel请求明确的用户授权。一旦用户授予了对某个目录的访问权限,系统会通过“安全范围书签”(Security-Scoped Bookmark)技术,允许应用在后续启动时再次访问该目录,而无需重复请求。我们在UserDefaults中安全地存储这些书签数据。注意事项:书签的持久化与恢复
处理安全范围书签需要非常小心。书签数据是二进制Data,必须使用UserDefaults的set(_:forKey:)方法存储。在应用启动时,需要尝试解析这些书签数据来恢复访问权限。这个过程可能会因为文件系统变动(如文件夹被移动)而失败,代码中必须有健全的错误处理逻辑,在失败时优雅地降级(例如,清除无效书签并等待用户重新选择目录)。
4.2 挑战二:支持多样化的文件格式与语法高亮
AI工具的配置文件格式五花八门:有简单的Markdown(.cursorrules),有JSON(settings.json),有YAML(某些工具的配置),甚至可能是自定义格式。
- 解决方案:我们采用了分层的处理策略。
- 文本编辑:对于所有文本文件,核心编辑器使用SwiftUI的
TextEditor。这是一个基础但可靠的组件。为了提升编辑体验,我们集成了一个轻量级的语法高亮库(例如,通过封装Highlightr,它基于JavaScript的highlight.js)。在编辑模式下,根据文件扩展名选择对应的语法高亮规则。 - 内容预览:在预览模式,我们不仅要高亮,还要对结构化数据(JSON, YAML)进行格式化(pretty-print)。我们使用了
Foundation框架的JSONSerialization来验证和格式化JSON。对于YAML,则引入了一个轻量的Swift YAML解析库(如Yams)。 - 未知格式:对于无法识别的二进制或特殊格式文件,Relay会显示一个警告图标,并提供一个“在Finder中显示”和“用默认应用打开”的按钮,将控制权交还给操作系统和用户熟悉的工具。
- 文本编辑:对于所有文本文件,核心编辑器使用SwiftUI的
4.3 挑战三:保持响应式与性能
当用户注册了包含成千上万个文件的大型项目目录(如整个Monorepo)时,启动时的全量扫描可能会阻塞UI线程,导致应用卡顿。
- 解决方案:彻底的异步化与增量更新。
ConfigScanner的所有文件遍历和读取操作都放在后台队列(DispatchQueue.global(qos: .userInitiated))中执行。- 扫描结果通过
@MainActor属性包装器或DispatchQueue.main.async安全地传回主线程更新UI。 - 实现增量扫描逻辑。应用启动后,
ConfigScanner会为每个被监控的目录创建一个文件系统监听器。当目录内发生文件创建、修改、删除事件时,只会重新扫描该目录下受影响的部分,而不是全量扫描,从而极大地提升了响应速度。 - 对于文件内容的预览,采用懒加载策略。列表视图只加载文件的元数据(名称、路径、大小)。只有当用户点击某个文件,进入详情预览时,才触发该文件内容的读取操作。
5. 构建、分发与开源协作
5.1 使用Swift Package Manager管理依赖
Relay的依赖非常精简,这符合其轻量化的定位。Package.swift文件清晰定义了依赖关系:
dependencies: [ .package(url: "https://github.com/johnsundell/files", from: "4.0.0"), // 更优雅的文件操作 .package(url: "https://github.com/sharplet/FileWatcher", from: "0.3.0"), // 文件系统监听 .package(url: "https://github.com/jpsim/Yams", from: "5.0.0"), // YAML解析 // 可选:语法高亮库 ],使用SPM使得项目设置非常简单,任何开发者克隆仓库后,Xcode会自动解析并获取依赖。
5.2 打包与公证
为了能在macOS上顺利分发(特别是避免Gatekeeper警告),必须对应用进行公证。
- 归档(Archive):在Xcode中,选择“Any Mac (Apple Silicon, Intel)”作为目标,进行归档。
- 导出:从归档管理器中,选择“Distribute App” -> “Developer ID”方式导出,这会创建一个
.app包。 - 公证(Notarize):使用
xcrun notarytool命令行工具或Xcode的“Distribute App”流程自动完成。你需要一个有效的Apple开发者账号。公证过程会将你的应用提交给Apple服务器进行安全扫描。 - 打票(Staple):公证成功后,将票据(ticket)钉到应用包上:
xcrun stapler staple "YourApp.app"。这一步至关重要,它确保了应用在离线环境下也能通过Gatekeeper验证。 - 创建DMG:使用
create-dmg等工具创建一个美观的磁盘映像文件,将.app拖进去,并创建一个指向“应用程序”文件夹的快捷方式,方便用户安装。
5.3 开源与社区反馈
将项目开源在GitHub上,不仅仅是发布代码,更是开启了一个协作循环。
- 清晰的README:README是项目的门面。我花了大量时间撰写一份详细的README,包括:功能特性截图、快速开始指南、开发环境搭建说明、技术栈介绍以及明确的贡献指南(如何提交Issue,如何发起Pull Request)。
- Issue模板:在GitHub仓库设置中,配置了Bug报告和功能请求的Issue模板,引导用户提供必要的信息(如macOS版本、Relay版本、复现步骤),这能极大提高沟通效率。
- 处理反馈:早期用户反馈非常宝贵。有人提出希望支持Windsurf的配置,有人发现对某些符号链接(Symlink)目录的扫描有问题,还有人希望编辑器的字体可以自定义。我根据反馈的优先级和普遍性,逐步将它们纳入开发路线图。例如,支持新工具往往只需要在
AITool枚举和ConfigScanner的识别规则中添加新条目,架构的可扩展性在这里得到了验证。
6. 总结与未来可能的演进
开发Relay的过程,是一个典型的“为自己造轮子,最终惠及他人”的故事。它解决了我个人工作流中的一个具体痛点,而Swift和SwiftUI的成熟生态让这个想法的实现变得异常顺畅。
目前,Relay处于一个“好用”的1.0状态。但它还有很长的进化路径。一些正在考虑或社区呼吁的方向包括:
- 更多AI工具集成:除了Cursor和Claude,像GitHub Copilot(其配置可能在VSCode或全局的
~/.config/github-copilot中)、Windsurf、甚至是本地运行的CodeLLM(如配置Ollama的模型参数)都可以成为聚合目标。 - 配置同步与共享:探索通过简单的Git仓库或加密云存储(如iCloud),在用户的多台Mac间同步“预设模板”甚至是被标记为“收藏”的配置项。
- 更智能的编辑辅助:基于现有规则文件,利用AI( ironic, isn‘t it?)来提供规则编写的建议、补全,或者检查规则之间的冲突。
- 规则有效性测试:提供一个沙盒环境,可以针对一段示例代码,快速测试当前项目的规则文件是否会被AI正确解读和执行。
这个项目的核心价值在于,它承认了“管理AI行为”正在成为软件开发中一个日益重要的子任务。Relay试图将这个任务工具化、可视化,让它变得更可控、更可积累。如果你也受困于多个AI编码助手的配置混乱,不妨试试Relay,或者基于它的开源代码构建属于你自己的“控制中心”。