news 2026/11/6 3:42:13

Vibe Coding 与工程架构:AI 写的代码如何被约束

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vibe Coding 与工程架构:AI 写的代码如何被约束

Vibe Coding 与工程架构:AI 写的代码如何被约束

第 3 篇。系列导航见 README。

背景:Vibe Coding 最容易失控的地方

AI 编程工具擅长"把功能写出来",但它不知道你的产品底线。对清理工具来说,底线是:不能删系统文件、不能删用户文档、不能在用户没确认时删除中高风险数据。这些约束必须在架构层面显式表达,否则生成代码越多,风险面越大。

Pure 在文件清理主链路上的做法是把约束写进类型和调用链:扫描器返回JunkItem,Cleaner 负责校验与删除,安全引擎独立于 UI,规则在载入期被过滤。需要先说明:仓库没有保留生成对话,无法逐文件判定 AI 与人工的贡献;下面讨论的是代码中实际存在的约束、调用链和审查痕迹。

实际架构:比规范更轻,但边界清楚

架构规范设想了Coordinator、UseCase、AppDependencies等层次,实际代码没有采用。真实结构是:

层代表类型职责不应该做什么
AppPureApp、AppDelegate、MenuBarController生命周期、菜单栏、开机启动不处理清理逻辑
UIScanViewModel、CleanView、DiskHealthViewModel状态编排与展示不直接删除文件
EngineScanEngine、CleanEngine、UninstallEngine扫描与清理流程编排、结果聚合;新清理入口负责应用风险策略不实现具体扫描规则
ScannerCacheScanner、BrowserScanner、GroupContainerScanner发现候选条目不执行删除
CleanerTrashCleaner、UninstallCleaner、GroupContainerCleaner执行校验与删除不负责选择条目;安全门槛由具体 Cleaner 实现
CoreSafetyEngine、RuleRepository、DatabaseManager、FileSystemKit安全、规则、持久化、文件系统工具多数不依赖 UI;L10n依赖 SwiftUI,PermissionManager依赖 AppKit

两个协议把边界固定下来:

protocolScanner:Sendable{varcategory:JunkCategory{get}funcscan(progress:@escaping@Sendable(Int,Int64)->Void)asyncthrows->[JunkItem]}protocolCleaner{funcvalidate(_item:JunkItem)throwsfuncclean(_item:JunkItem)throws->URL?}

Scanner协议要求category属性与scan返回[JunkItem];实现会读取文件系统、汇报进度、写入扫描缓存,但不会删除文件。Cleaner协议只有validate与clean两个要求,没有其他默认实现;三种 Cleaner 的校验内容并不相同:

Cleanervalidate的实际检查clean的行为
TrashCleaner存在性;新建 URL 读取最新大小/修改时间;拒绝符号链接;大小与时间复核;重新执行SafetyEngine;解析完整路径后复核危险区域默认移入废纸篓;永久删除需通过guardPermanentDelete的安全等级限制(仅safe/lowRisk)并再次检查符号链接,但不会完整重跑元信息校验
UninstallCleaner路径必须精确命中本次allowedPaths;存在性;SafetyLevel != dangerous;最新元信息;符号链接;大小/时间;解析后仍在allowedBases内移入废纸篓
GroupContainerCleaner精确命中allowedPaths;存在性;runningApplicationBundleIDs()命中bundleID则拒绝;最新元信息;符号链接;大小/时间;解析后仍在allowedBases内移入废纸篓

CleanEngine对真正进入执行队列的条目先调用validate,通过后再调用clean。这一步不是装饰,但它也不是"等价于沙盒"的安全保证:校验与删除是两次独立调用,细节见后文。

模块依赖与调用顺序

一次典型的扫描与清理流程:

DatabaseManagerCleanerCleanEngineCleanupAnalyzerScannersScanEngineScanViewModelCleanViewDatabaseManagerCleanerCleanEngineCleanupAnalyzerScannersScanEngineScanViewModelCleanViewalt[校验通过][校验失败]startScan()quickScan(applicationContext:)并发 scan()[JunkItem]ScanResultanalyze(items, context)[CleanupItem]clean(selected, confirmedReview:)cleanDetailed(items, confirmedReview:)validate(item)clean(item)成功 / 抛错CleanErrorrecord(clean_operation)

这张图只覆盖CleanView的主链路。CleanEngine 会按条目来源选择具体 Cleaner:新的CleanupItem入口中,Group Container / Application Container 走GroupContainerCleaner,其余走默认/注入的通用 Cleaner(生产路径是TrashCleaner);旧接口则由调用方传入 Cleaner。大文件、开发者缓存、遗留数据、卸载后剩余数据等入口走旧接口,不经过CleanupAnalyzer与confirmedReview,详见后文。

CleanupAnalyzer确实在扫描结束后运行:ScanViewModel.startScan()拿到ScanResult后调用CleanupAnalyzer.analyze(items:context:),把JunkItem转成带source、role、ownership、applicationState、recoverability、risk、score、confidence、reasons、recommendedAction、safetyOverride的CleanupItem。扫描器不计算CleanupRisk;Group Container 扫描器会按容器内部角色产出条目,但统一的风险评分仍由后续分析完成。

清理历史也不是在 UI 层写入。CleanEngine.process对每个真正尝试过的条目,在validate/clean之后写一条clean_operation,成功、跳过、失败都会写;被风险策略直接拦下、没有进入执行队列的条目不会写历史。卸载流程还会额外写uninstall_operation。

当前代码中的两套安全模型

当前代码里同时存在两套模型。仓库没有 Git 历史,无法证明它们是严格的替代关系;这里只描述当前实现中的分工,不编造演进时间线。

模型取值使用位置
SafetyLevelsafe/lowRisk/caution/dangerousJunkItem、SafetyEngine、TrashCleaner、UninstallCleaner
CleanupRisksafe/low/medium/high/prohibited,另有 0–100 评分CleanupItem、CleanupAnalyzer、CleanupRiskScorer、CleanView、CleanEngine的新入口

SafetyLevel主要回答"路径是否命中规则或黑名单";CleanupRisk在此基础上综合应用生命周期、所有权、数据角色、来源、可恢复性、置信度和其他应用引用。评分从 50 起算,按维度加减,并映射为:90–100safe、75–89low、50–74medium、20–49high、0–19prohibited。

硬规则由CleanupRiskScorer.safetyOverride计算,输入是路径、来源、角色、所有权与应用状态。关键规则如下:

switchoverride{case.prohibited:risk=.prohibited score=min(score,10)case.high:risk=CleanupRisk.max(riskLevel(forScore:score),.high)case.medium:risk=CleanupRisk.max(riskLevel(forScore:score),.medium)case.none:risk=riskLevel(forScore:score)}

Keychain、iCloud、用户文档、核心系统数据、系统库直接prohibited;Apple Group Container、运行中应用数据至少high;Group Container 的非可重建角色至少medium。即使评分为 95,也不能推翻这些硬规则——但"不能被评分推翻"不等于"删除一定安全",它仍然只是策略判断。

confirmedReview的实际含义也需要限定:新入口中,prohibited一律跳过;medium/high只有confirmedReview == true才执行;safe/low直接执行。CleanView在选中项包含中高风险或设置要求确认时弹出确认面板,用户确认后传入true。它不是一个全局"允许删除"开关,也不会让prohibited放行。被风险策略拦下的条目不进入validate,直接计为 skipped;只有真正进入执行队列的条目才走validate → clean。此外,confirmedReview只是一个布尔参数,API 本身不验证用户是否真的看过确认面板,语义仍依赖调用方。

两套模型负责不同阶段的判断:CleanupRisk决定条目是否允许进入清理队列,以及是否需要用户确认;SafetyLevel和 Cleaner 的路径、白名单、元信息检查,则约束实际执行的清理操作。前者不是后者的替代品,后者也不会重新计算CleanupRisk分数。CleanupRisk.max按sortRank取较高等级,所以.high/.medium覆盖是最低风险等级的约束,不是重新评分。

需要注意,validate与clean之间仍存在 TOCTOU 竞争窗口,因此这些检查只能降低误删风险,不能证明文件在整个操作期间始终满足安全条件。

旧接口的兼容性:不是等价安全保证

CleanEngine目前保留了新旧两套入口,且都在生产代码中使用:

入口主要生产调用方是否经过风险模型与确认Cleaner 选择
clean([CleanupItem], confirmedReview:)ScanViewModel.clean(CleanView调用)是:风险过滤 + 中高风险确认Group Container 专用 Cleaner,其余默认/注入的通用 Cleaner(生产路径为TrashCleaner)
clean([JunkItem])/cleanDetailed([JunkItem])ScanViewModel.trashLeftover、trashLargeFile、trashDeveloperItem、ApplicationDetailView.trashRemaining、UninstallEngine.uninstall(先 preflight)否:不做CleanupAnalyzer、风险过滤或confirmedReview由调用方传入的白名单 Cleaner;默认是TrashCleaner

两条路径最终都进入CleanEngine.process。对于进入执行队列的条目,流程都会先调用validate,只有校验通过后才调用clean;两条路径使用相同的错误分类和历史记录机制:missing/changed/unsafe计为 skipped,permissionDenied/inUse/failed计为 failed,其他错误计为 failed。风险策略拦下的条目不会进入逐项执行,也不写clean_operation;旧接口没有这层风险拦截,进入队列的条目都会尝试校验。

其中UninstallEngine并不是裸调用旧接口:它在删除前先执行UninstallPreflight,检查废纸篓、系统应用、存在性、符号链接、Bundle ID、版本变化、运行状态与权限,再构造UninstallCleaner的白名单。但 preflight 也不是CleanupRisk模型,二者不能互相替代。

差异在"进入队列之前":旧接口不会把CleanupItem的多维风险与confirmedReview应用到条目上。TrashCleaner仍会拒绝SafetyLevel.dangerous,但允许caution;UninstallCleaner与GroupContainerCleaner依靠本次白名单与基目录限制范围。旧接口调用分别服务于大文件、开发者缓存、遗留数据和卸载后剩余数据等操作;操作范围由allowedPaths与allowedBases限定,但不同入口的确认方式并不一致:卸载流程有选择/预览 Sheet,开发者条目有确认弹窗,而大文件、遗留数据、卸载后剩余数据等入口使用直接操作按钮。操作范围明确,并不意味着这些入口具有与主清理页相同的风险评估和确认保证。

测试中也同时存在两种调用:CleanEngineTests、CleanHistoryTests、TrashIntegrationTests使用旧接口覆盖校验、历史与废纸篓流程,CleanEngineRiskPolicyTests则覆盖新接口的风险策略与确认参数。

因此,旧接口是兼容 API,不是与主清理页等价的安全保证。仅凭静态阅读,没有发现旧入口能绕过Cleaner.validate或删除白名单之外的任意路径;但"显式单条操作即视为确认"是否足够,属于产品策略问题。也就是说,目前不能判定它是已证实的安全缺陷,更准确的分类是确认语义不一致。若未来要求统一,应把这些入口迁移到CleanupItemAPI,或在 ViewModel 层补充确认,而不是假设旧接口继承了新模型的保证。

需要人工兜底的几类问题

从代码与注释可以观察到几类需要人工兜底的地方。先看@unchecked Sendable的实际共享状态:

类型共享可变状态线程安全依靠什么
ScanEngine注入的扫描器/存储引用(不可变);ProgressCounter的计数ProgressCounter使用NSLock;引擎本身无可变共享状态
DirectorySizeCacheentries字典NSLock覆盖读写与身份校验
ScanCacheSession每次扫描的缓存、统计、待写入集合NSLock;commit()先快照再落库
DatabaseManager/FileEntryStoreSQLite 连接句柄NSLock+SQLITE_OPEN_FULLMUTEX;FileEntryStore使用独立连接
SettingsStoresettings与业务快照缓存UI 绑定驱动settings写入;snapshot()通过NSLock读取

@unchecked Sendable的共性是:编译器不再证明线程安全,正确性依赖锁的范围和调用时序。锁覆盖得好,不代表所有并发路径都被测试覆盖;这部分仍需要人工审查。其余标注(如RuleRepository、CleanEngine、UninstallEngine、SnapshotService、系统 Provider)大多共享状态较少,本文不逐一展开。

资源生命周期也需要人工兜底。SQLite 连接、测试用临时数据库、FileEntryStore的独立连接都有显式close();DatabaseManager.close()的注释明确提到"避免 vnode 占用警告",说明这是实际踩过的坑。ScanCacheSession.commit()失败时会记录日志而不是中断清理,历史写入失败也不会回滚已执行的删除。

安全校验的顺序同样值得单独看:TrashCleaner.validate依次检查存在性、最新元信息、符号链接、大小与修改时间、SafetyEngine安全等级,最后解析完整路径复核危险区域。但validate与clean是两次独立调用,之间仍有竞争窗口;trashItem/removeItem按路径操作,guardPermanentDelete只复核安全等级与符号链接,不会重新执行完整元信息校验。这些检查降低误删风险,但不能消除 TOCTOU。

Coordinator/UseCase没有落地,实际是单例 + ViewModel;是否需要补回,应结合多入口的重复流程与编排测试需求判断,而不是类型名称本身。

代码中的审查 P1-1、审查 P2-7、审查 P3-9等标记,表明项目至少经历过按优先级编号的审查或修订,涉及规则护栏、错误分类、隐藏文件统计等问题。但仓库没有对应的审查记录,无法还原完整讨论,也不能据此推断每处修改由谁完成。

可维护性的真实问题

架构边界清楚,不等于代码没有债务。按行数统计,几个明显偏大的文件是:

文件行数问题
UI/Clean/CleanView.swift901列表、分组、应用视图、确认页、结果页集中在一个文件
Core/Common/Localization/L10n.swift744大量计算属性,键与 UI 强绑定
UI/Applications/ApplicationDetailView.swift657详情、卸载 Sheet、存储分解混在一起
Core/Database/DatabaseManager.swift562多张表与多组业务查询共用一把锁

单例(ScanEngine.shared、CleanEngine.shared、DatabaseManager.shared等)降低了装配成本,但也让测试必须通过构造函数注入替身;项目确实这样做了(ScanEngine(scanners:)、CleanEngine(cleaner:history:)),算是可测试性上的补救。

为什么没有落地规划中的分层

架构规范提出了 Coordinator 与 UseCase,本意是把扫描与清理流程从 UI 中抽离。实际实现选择了 Engine + ViewModel:ScanViewModel.startScan()直接调用ScanEngine.quickScan,清理也直接调用CleanEngine。这个选择的代价是 ViewModel 承担了部分流程编排职责,好处是调用链短、调试路径清晰。是否引入 Coordinator,不应只看调用方数量,而应看多个入口是否存在重复的流程编排、是否需要共享业务流程,以及是否需要独立测试编排逻辑。当前项目已有多个清理入口,但它们承担的业务职责和安全策略并不完全相同;是否抽取统一的编排层,需要结合重复逻辑与实际维护成本判断,而不是为了让架构图更整齐而增加一层类型。

小结

从仓库产物看,真正起约束作用的是主清理链路:Scanner/Cleaner 分离、SafetyEngine独立、风险模型在扫描后统一分析。但这些约束不是完整的安全保证:旧JunkItem接口不经过CleanupAnalyzer与confirmedReview,删除阶段与validate之间仍有 TOCTOU 窗口,@unchecked Sendable的并发正确性依赖锁与人工审查。计划中的分层没有落地、部分文件过大,也是明确的维护成本。

下一篇进入具体功能,讨论磁盘扫描、文件分类、清理安全和应用卸载这四条链路。

  • 上一篇:项目规划与技术选型
  • 下一篇:核心功能实现与文件系统安全
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 3:40:01

项目上线后的那一杯五粮春

项目上线后的那一杯五粮春凌晨一点半,最后一条监控告警终于安静下来。灰度发布完成,核心接口的 P99 从 480ms 回落到 120ms,日志里不再有红色的 ERROR。我合上笔记本,长长地舒了一口气——这个熬了三个多月的项目,总算…

作者头像 李华
网站建设 2026/11/6 3:39:59

工程师的生活解药

凌晨两点,我 push 完最后一次提交,git log 显示过去三周有十九个 commit 是在零点之后产生的。关掉 IDE,屏幕暗下去的那一刻,脑子里还在跑单元测试——那种明明已经 exit(0) 了、但线程池迟迟不肯 shutdown 的感觉,干过…

作者头像 李华
网站建设 2026/11/6 3:37:49

MySQL什么情况下索引会失效

核心原则:索引列要保持“原生值”,不能被函数、运算、隐式转换破坏;不走索引范围、前缀规则、优化器判断全表更快时也会失效1. 索引列上做运算、函数、类型转换-- 失效:对索引列用函数 select * from t where date(create_time) …

作者头像 李华
网站建设 2026/11/6 3:37:25

Java 系列教程(第一篇):Java 简介与开发环境搭建

本系列教程将按照目录逐一深入 Java 核心知识,从基础语法到高级特性,每篇均附详细讲解与可运行代码示例。本篇作为开篇,带你全面认识 Java 语言,并完成第一个程序的编写与运行。 1. 什么是 Java? Java 是由 Sun Micro…

作者头像 李华
网站建设 2026/11/6 3:37:17

一句话完成内网穿透:ZeroNews MCP 连接器接入 Cursor / Claude 实战

前言现在大家用 Cursor、WorkBuddy、Codex、Claude 这些 AI,写代码、查日志、跑命令、连内网 MCP 都很顺手了。但一到内网穿透,往往还得自己装客户端、打开控制台加隧道——比如本机 3000 端口的 Web 服务,或者 SSH,需要在浏览器和…

作者头像 李华
网站建设 2026/11/6 3:37:06

【研发类-API开发Skills】azure-data-tables-java 技能

使用Azure Tables SDK for Java构建表存储应用程序。同时支持Azure Table Storage和Cosmos DB Table API。 📥 下载地址: agentic-awesome-skills/skills/azure-data-tables-java at main sickn33/agentic-awesome-skills GitHub 技能概述 azure-da…

作者头像 李华