从“AI功能失灵”到“架构升级”:一次深度协作的Swift & Supabase调试与重构之旅

状态
Tags
Tech_Tag
Created
Sep 11, 2025 02:13 PM
最近,我的 Swift 应用在生产环境遇到了一个头疼的问题:明明 Supabase Edge Function 收到了 API Key 请求,但 AI 功能就是不工作!原以为只是个小修小补,没想到这成了我与 AI 助手一次深度协作,从代码细节到设计理念全面升级的起点。今天,我想和大家分享这段“痛并快乐着”的调试与重构之旅。

第一章:表面症状与初步排查——小坑与大坑预警

问题最初表现为:App 无法调用 AI API。我的第一反应是检查 API Key 的获取流程。通过与 AI 助手的来回沟通,我们很快发现了几处可疑之处:
  1. 拼写错误(KAMI_API_KEY vs KIMI_API_KEY:这是最常见的“低级错误”,Edge Function 配置的 Secret 和实际调用的环境变量名不一致。
  1. JSON 字段名不匹配(snake_case vs camelCase:Supabase Edge Function 返回的是 snake_case 格式的 JSON 字段(如 kimi_api_key),而 Swift 的 Codable 模型是 camelCasekimiApiKey)。这会导致 JSON 解码失败。
  1. 用户认证时机问题:App 在启动时就尝试获取 API Key,但 Edge Function 要求用户必须登录。如果用户会话尚未恢复,就会收到 401 未授权错误。
我和助手迅速修复了拼写和 JSON 字段名。但故事并没有结束,AI 功能依旧罢工。
反思: 很多时候,Bug 的根源比我们想的更深。看似简单的拼写和大小写问题,可能是冰山一角。更重要的是,在生产环境下,一个静默的错误(例如 API Key 为空却不提示)是最大的敌人。

第二章:大面积编译错误与策略反思——“别急着动手,先深呼吸!”

为了进一步诊断,我请助手提供了一个 curl 命令来直接测试 Edge Function,并增加了 UI 错误提示状态。助手也试图调整代码,将 Key 的获取逻辑推迟到用户登录之后。
然而,这次改动却带来了一次“小型灾难”:ContentView.swift 出现了大面积的编译错误! 视图找不到、EnvironmentObject 报错、onChange 订阅非 Equatable 类型……我当时的心情是崩溃的,直接对助手喊话:“你还是引入了 bug 并且没有解决,请理解之前的逻辑,以及我们的原始出发点是……先不要开始上手,take a deep breath!”
这次激烈的对话,成了整个过程的转折点。它迫使我们跳出代码细节,重新审视问题。
用户的核心洞察: “边缘函数要求用户登录才能拿到 apikey,但这个并不是一个产品需求而只是当时的一个实现方案,实际上的需求只是用户端需要拿到 key,然后保障安全。”
这简直是醍醐灌顶!API Key 是应用的核心能力,它应该在 App 一启动就可用,而不是依赖用户登录。将“设备/应用需求”与“用户行为”绑定,是一个糟糕的架构决策,带来了复杂性和糟糕的用户体验。
AI 助手的反思与战略调整: 助手也意识到之前的思路错了。它立刻提出了一个全新的、更高级别的解决方案:
  1. 新建一个公共 Edge Function (get-public-api-keys):这个函数不要求用户认证,只负责安全地从环境变量中读取 Kimi 和 TTS API Key。
  1. 利用 anon_key 作为基础安全防护:虽然 anon_key 是公开的,但它提供了一层基本的防护,防止随机滥用。
  1. App 启动时即获取 Key:将 Key 获取逻辑移回 manitodoApp.swift,确保 AI 功能在 App 启动后立即对所有用户可用。
核心权衡: 这意味着 API Key 理论上可能被技术高手提取并滥用,导致财务风险。但对于 MVP 或早期产品,这通常被认为是可接受的风险,换取了巨大的用户体验和开发简洁性提升。
反思: 这段经历教会了我一个最重要的道理:永远要区分“产品需求”与“实现细节”。 一个好的架构,能让你的实现细节服务于产品需求,而不是反过来被实现细节所困扰。有时,一个 Bug 会迫使你重新思考整个设计,带来意想不到的收获。

第三章:新策略落地与调试工具的完善

在达成新共识后,助手迅速行动:
  1. 公共 Edge Function 部署:我按照助手提供的简化代码,创建并部署了 get-public-api-keys
  1. KeyService 调整:修改 Swift 端的 KeyService,使其调用新的公共函数。
  1. manitodoApp.swift 恢复:将 Key 获取逻辑移回 App 启动时执行。
  1. ContentView.swift 精简:移除了冗余的登录后获取 Key 逻辑。
为了让新方案更易测试和维护,我们还增加了两项强大的调试功能:
  1. DEBUG 模式下的远程 Key 获取开关:一个 forceRemoteKeyFetchingInDebug 标志,可以在 DebugView 中强制 App 模拟生产环境,跳过本地环境变量回退,直接测试远程 Edge Function 的响应。
  1. AI API 连接验证工具:在 DebugView 中添加了一个按钮,发送一个 max_tokens: 1 的简单测试请求到 AI 服务,快速验证 API Key、Base URL 和网络连接是否正常。这极大提升了诊断效率。
反思: 完善的调试工具是提高开发效率和产品质量的关键。它们能让我们在问题发生时,快速定位和验证。

第四章:依赖注入的深度重构——架构的最终升华

在验证 AI API 连接时,我们又遇到了新的编译错误:AppDependencies 无法直接访问 aiAPIClient。这揭示了应用中一个更深层次的架构问题:依赖注入(Dependency Injection, DI) 的不一致性。
问题所在: 很多服务(KimiGenerationEngineTarotAudioGeneratorTarotAPIClient 等),甚至一些 ViewModel,都各自创建AIAPIClient 的实例。这导致:
  • 多重“真相来源”:AIAPIClient 的配置修改需要在多处进行,容易遗漏和出错。
  • 难以测试:无法轻松地将真实的网络请求替换为 Mock 对象进行单元测试。
  • 隐藏依赖:类内部自行创建依赖,使得其职责不清晰。
DI 解决方案(Inversion of Control): AI 助手详细解释了依赖注入的优点,并提出了一致的重构方案:
  1. AppDependencies 作为中央工厂AppDependencies 应该成为唯一创建和持有 AIAPIClient 单一共享实例的地方。
  1. 构造函数注入:所有需要 AIAPIClient 的服务、ViewModel 和视图,都通过其构造函数接收这个共享实例,而不是自行创建。
这个重构过程经历了一次“多米诺骨牌效应”:
  • 首先,修改 AIAPIClient 下游的服务 (KimiGenerationEngine, TarotAudioGenerator, TarotAPIClient),让它们接收 AIAPIClient
  • 接着,这些服务的下游(如 KimiTaskGenerator, TarotAudioService, TarotReadingService)也因为其依赖发生了变化,需要跟着修改。
  • 最终,最上层的 ViewModels (TaskViewModel, GoalViewModel, TarotViewModel) 和视图 (AddTaskView, TaskCustomizationView, DebugView, GoalCreationFlow, ExecutionView) 也必须更新,以正确地接收和传递这些依赖。
虽然这个过程漫长且充满了来回的修正,但最终,整个依赖图谱被理顺,应用的架构变得清晰、健壮。
反思: 依赖注入是构建可测试、可维护、可扩展应用的关键。尽管重构过程可能复杂,但它带来的长期收益是巨大的。理解其“级联效应”并有耐心去解决每一个环节,是成为一名优秀开发者的必经之路。

总结:一次有意义的成长

从最初的 AI 功能失灵,到最终完成整个应用的依赖注入重构,这不仅仅是修复 Bug,更是一次深入学习和架构升级的旅程。我学到了:
  • 透过现象看本质:不要只盯着表面 Bug,要挖掘其背后的产品需求和架构问题。
  • 战略与执行并重:有时,暂停编码,重新思考高层设计,比盲目修补更重要。
  • 健壮性与可维护性:优秀的调试工具和一致的架构模式(如依赖注入)是它们的核心。
  • 与 AI 助手的协作:尽管过程中有来回和失误,但这种迭代式的协作,最终能推动我们达到更高的代码质量和更深的理解。
现在,我的应用不仅 AI 功能恢复正常,而且拥有了一个更稳定、更高效、更易于测试和维护的底层架构。这让我对未来的开发充满了信心!
希望我的这段经历能给你带来一些启发。如果你也有类似的调试和重构故事,欢迎在评论区分享!