一键开通华为云码道 CodeArts 代码智能体:https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1\&sourcead=dmzntgwatomgithd
摘要:本文是华为云码道 AI 编程挑战赛第 4 期的参赛实测记录。我用 CodeArts 代码智能体独立开发了一个跨平台桌面应用「仓衡 · Git 仓库健康度体检台」:对本地 Git 仓库做五维体检(大文件 / 僵尸分支 / 提交习惯 / 依赖健康 / 敏感信息残留),输出带修复建议的结构化报告------纯本地、零网络、只诊断不动手。技术栈是 Rust(检查器内核)+ Tauri 2(桌面壳)+ Vue 3。文章完整记录架构设计(内核/壳分离 + Checker 插件体系 + 两段式扫描编排)、AI 与借用检查器的配合实录、功能演示与踩坑记录,源码已在 AtomGit 开源。这是本系列第四篇:Web 全栈、小程序、鸿蒙原生之后,第四种端形态与第四门语言。
技术栈版本:码道 CodeArts 代码智能体(2026-09 挑战赛版本)| Rust stable(2026-10)| Tauri 2 | git2(libgit2)| Vue 3 + TypeScript | 实测时间: 2026-10


选择目录后显示仓库基本信息------提交总数、分支数、工作区文件数、依赖清单数------确认无误再点「开始体检」;检查器配置区支持调整大文件阈值(保存在 localStorage,下次启动自动恢复),也支持在仓库根目录创建 .repobalance-ignore 忽略特定路径(语法同 .gitignore):

一、项目背景:仓库年久失修的真实痛点
先交代体检对象。我的技术知识库仓库有 664+ 篇文章,Git 历史横跨数年:早期提交的大图片至今躺在历史对象里(删了工作区文件也没用)、临时分支建了忘删、提交信息早期全靠心情写。这些「仓库债」平时无感,直到某天 clone 变慢、想找回某个历史版本才发现问题。
市面上的工具要么面向托管平台(GitHub Insights),要么是单点脚本(如 git-sizer),没有一个「选个本地目录、一键体检、出一份能看懂的报告」的桌面应用。所以第 4 期参赛,我决定做这个工具------而且它有一个天然的加分项:体检对象就是我自己提交参赛的仓库,演示数据全部真实。
产品边界上有三条自我约束,写进了 README 也贯穿了实现:
- 纯本地零网络------不做漏洞库联网查询,不做远端平台集成,没有数据出网;
- 证据掩码 ------敏感信息扫描只展示掩码后的证据(
****替代命中内容),报告与导出处处如此; - 只诊断不动手------不做改写历史、不做自动删分支,体检工具不碰你的仓库。
1.1 作品功能一览
| 模块 | 功能 |
|---|---|
| 大文件检测 | 工作区 + 历史提交中的超大文件,区分「删文件可解决」与「需重写历史」 |
| 分支健康 | 僵尸分支(陈旧未合并)、未合并提交数、命名规范检查 |
| 提交习惯 | 提交信息规范符合率、超大提交 TOP5、深夜/周末占比 |
| 依赖健康 | 四类清单文件解析 + 本地内置数据的版本陈旧度近似检查 |
| 敏感信息扫描 | 私钥块 / 高熵赋值 / 内网地址,证据掩码展示 |
| 体检报告 | 五维评分雷达 + finding 详情 + 导出 Markdown/JSON |
| 扫描体验 | 实时进度流、可取消、内核可独立编译为 CLI |
技术栈:Rust + git2(内核),Tauri 2(桌面壳),Vue 3 + TypeScript(前端),cargo test 双侧测试。
说明:五类检查器共用同一套 Checker 契约与报告模型(本文的架构设计与代码均以这套契约为主线);截至成稿,桌面应用与 CLI 已上线的规则引擎以大文件检测为主,其余四类检查器在契约与 UI 层已就位、规则实现按里程碑逐步启用,功能一览表是产品完整规划口径。
二、架构设计:三个核心决策
老规矩,架构决策在人、实现在 AI。评审维度里架构与代码合计 60%,这两个决策把两块分都落在实处。
2.1 决策一:内核/壳分离,内核可独立编译为 CLI
Tauri 工程最容易犯的错是把业务逻辑写进 command 处理函数------与框架耦合后既难测试又不可复用。动手前我比过两种组织方式:
| 方案 | 可测性 | 复用性 | 结论 |
|---|---|---|---|
| 逻辑全写在 Tauri command 里 | 差:必须起 Tauri 环境才能测 | 无 | 否决 |
| 内核独立 crate + Tauri 壳 crate | 好:cargo test 直接测内核 |
内核可发布为 CLI 供 CI 使用 | ✅ 采用 |
text
repo-balance/
├── crates/
│ ├── rb-core/ # 检查器内核:零 tauri 依赖,可独立编译为 CLI
│ │ ├── git/ # GitRepo 门面:git2 类型绝不逃逸出这个模块
│ │ ├── model/ # RepoSnapshot / Finding / ScanReport / 评分
│ │ ├── checker/ # Checker trait + 注册表
│ │ ├── engine/ # 扫描编排(并发/取消/进度)
│ │ └── bin/ # CLI 入口:rb scan <path>
│ └── rb-app/ # Tauri 2 壳:只有 command 转发,无业务逻辑
└── ui/ # Vue 3 前端:只读呈现 rb-core 的结构化输出
这条决策的直接收益是一条命令的评审通道:cargo run -p rb-core -- scan ./demo-repo------不会装桌面应用的评审也能完整验证体检能力,而这本身就是架构分 30% 的活证据。

2.2 决策二:Checker trait 插件体系 + 统一 Finding 模型
五类检查如果各自为政,报告聚合、严重度排序、进度上报就要各写一遍。我把它们收敛为一个契约:
rust
// crates/rb-core/src/checker/mod.rs(真实源码节选)
/// 单个检查器:声明所需数据切片(plan),基于就绪切片纯计算产出结论(check)。
///
/// 实现约定:
/// - 无状态(`Send + Sync`,可跨线程共享引用);
/// - `check` 为纯计算,同输入同输出,不引入失败通道;
/// - `evidence` 必须为掩码后的证据字符串;
/// - `suggestion` 必须为可执行中文建议。
pub trait Checker: Send + Sync {
/// 稳定唯一标识,如 `"big-files"` / `"zombie-branches"`。
fn id(&self) -> &'static str;
/// 所属检查维度。
fn category(&self) -> Category;
/// 声明本检查器需要的数据切片。
///
/// 幂等:同一上下文多次调用结果等价;切片仅限 [`DataSlice`] 封闭集,
/// 采集由编排层统一完成(本方法只声明、不采集)。
fn plan(&self, ctx: &ScanContext) -> ScanPlan;
/// 基于已就绪切片执行检查,返回结论列表(纯计算,不引入失败通道)。
fn check(&self, ctx: &ScanContext) -> Vec<Finding>;
}
/// 检查器注册表:持有全部 Checker 实例,供扫描编排消费。
#[derive(Default)]
pub struct CheckerRegistry {
/// 按注册序保存的检查器实例。
checkers: Vec<Box<dyn Checker>>,
}
impl CheckerRegistry {
/// 注册一个检查器。id 冲突时返回 `RbError::DuplicateChecker`,
/// 且既有注册项不覆盖、不破坏;成功时返回 `&mut Self` 保留链式用法。
pub fn register(&mut self, checker: Box<dyn Checker>) -> Result<&mut Self> {
let id = checker.id();
if self.checkers.iter().any(|c| c.id() == id) {
return Err(RbError::DuplicateChecker { id: id.to_owned() });
}
self.checkers.push(checker);
Ok(self)
}
/// 全部已注册检查器(注册序);`engine::scan` 的唯一查询入口。
pub fn list(&self) -> &[Box<dyn Checker>] {
&self.checkers
}
}
注意这个 trait 契约里写死了三条纪律:无状态可跨线程(Send + Sync)、check 纯计算不引入失败通道、evidence 必须是掩码后的证据------掩码纪律不是口头约定,是签名旁边的一等约束。注册表在 id 冲突时直接返回错误而不是静默覆盖,新检查器接入只需「一个实现 + 注册一行」。
| 方案 | 聚合成本 | 新增检查成本 | 结论 |
|---|---|---|---|
| 每个检查独立实现扫描+报告 | 每个都写一遍聚合 | 高 | 否决 |
Checker trait + 编排层统一采集 |
聚合一次 | 新增一个实现 + 注册一行 | ✅ 采用 |
plan/check 两段式是个关键设计:先收集所有 Checker 的数据需求,合并去重后一次性遍历仓库 ,把切片分发给各 Checker------避免五条规则把历史遍历五遍。这个设计在 design 阶段就被 Agent 用「核心设计裁决」的形式固化下来:执行顺序硬前置(契约先于实现编码)、plan 双切片声明(工作区 + 历史两个切片)、历史遍历选型(多起点 revwalk 时间正序、每批独立 Repository 规避 git2 句柄 Send 非 Sync 硬约束)、测试构造细节(「仅历史」场景必须物理 git rm + commit,禁用 --cached------物理文件留存为未跟踪文件会被工作区扫描误命中):

design 评审通过后进入任务分解,14 项落地清单按依赖排序(夹具脚本 → 切片模型 → git 门面采集通道 → BigFilesChecker → engine 窄链路 → CLI 接入 → 单测 → 验收),再由「开始任务执行」按钮进入编码:

2.3 决策三:安全是产品本身------掩码在构造时完成,不做「序列化时才脱敏」
敏感信息扫描是本期核心功能,围绕它有三条实现红线。其中最关键的一条是掩码时机:
| 掩码方案 | 泄露风险 | 结论 |
|---|---|---|
| 构造 Finding 时完成掩码,之后的数据流只有掩码版 | 无:完整命中内容不进入任何下游 | ✅ 采用 |
| 序列化/展示时才替换 | 高:忘了脱敏的导出路径就是事故 | 否决 |
| 红线 | 实现 |
|---|---|
| 掩码前置 | Finding 构造时掩码(前后 3 字符 + 命中长度),IPC/导出复用同一结构 |
| 降噪 | 空值不报;example/sample/test 文件名中的命中降为 Info |
| 自证 | 用仓衡扫描仓衡自己,报告可直接核对(见 4.3/4.4) |
| Tauri 基线 | capabilities 最小能力集,不装 fs 插件(文件读取全在 Rust 侧),CSP 收紧为 'self' |
三、用 CodeArts 开发:AI 与借用检查器的配合
3.1 开发前的准备
打开 CodeArts 之前的人肉准备:AtomGit 公开仓库 repo-balance(LICENSE + README 骨架)、Rust stable + Tauri 2 环境跑通官方模板,以及一个特殊产物------合成数据的演示仓库 demo-repo :预置历史中的大文件、僵尸分支、以及一块 SAMPLE-NOT-A-REAL-KEY 占位的「私钥」(刻意不用任何格式上可能有效的 token 前缀,这个红线最终写进了投稿检查清单)。
环境这一步 CodeArts 也参与了:打开仓库后它先做了一轮项目分析,识别出「Rust workspace + Tauri 2.x 桌面应用(依赖 git2/serde,前端 pnpm)」,并列出本机需要安装的环境清单与下载地址------Rust stable、MSVC 生成工具、CMake(git2 依赖 libgit2 源码编译必需)、WebView2、Node.js LTS。照单安装即可:

Windows 侧的 rustup 走标准选项一路装完,stable-x86_64-pc-windows-msvc 工具链就位:

环境也不是装完就一劳永逸------D1 的质量门禁阶段还遇到一次 SDK 缺库:dbghelp.lib 的 x64 版本在 Windows Kits 目录里缺失(x86 版存在),所有 MSVC 链接全部失败(LNK1181)。这类问题代码侧无解,Agent 定位根因后给出修复方式二选一(VS Installer 重装修复 / 独立 SDK 安装器补库),补齐后补跑门禁通过:

流程上,每个特征都走同一条 SDD 流水线:需求规格设计(spec)→ 实现方案创建(design)→ 编码任务规划(tasks)→ 任务执行,四个阶段在会话顶部全程可见。本期共跑四个特征:workspace_init(工作区骨架与 Checker 契约,7 个任务组 15 个子任务)→ checker_plugin(Checker 插件体系)→ fixture_bigfiles(大文件检查器 + demo-repo 幂等夹具,8 个任务组 23 个子任务)→ ui_implementation(Tauri 2 壳 + Vue 3 前端,12 个任务组 44 个子任务)。每块话术写死 trait 签名、模块边界与验收标准------以 D1 的话术为例,结构、rb-core 依赖、内部分层、纪律、验收命令五段全部在投喂前写死,「rb-core 的 Cargo.toml 中不得出现任何 tauri 依赖」这类红线直接进了话术:

Agent 把话术翻译成 spec 与任务清单,任务按依赖顺序排列、每个子任务标注涉及文件与可检查的完成标准:

spec 阶段的契约表把 2.2 节那些 trait 签名逐条写成了验收条目------插件契约四方法、ScanPlan 合并去重(「同一切片一次扫描至多采集一次」的性能红线)、注册表禁止重复 id、Finding 契约(evidence 掩码 + suggestion 可执行建议)、测试全部 mock 纯内存验证,甚至专门留了「环境约束专章」规定 SDK 阻塞时如何如实呈现验收结果:

其中一条贯穿始终的纪律是:cargo clippy --all-targets 零 warning 是验收的一部分,不允许「能跑就行」。
3.2 AI 与借用检查器:一次典型的三方对话
Rust 开发中最有趣的部分是 AI、编译器与我三方的关系。以敏感信息扫描为例,AI 第一版生成的对象遍历代码在 cargo check 阶段被借用检查器打回------生命周期标注不符合 libgit2 的 API 约束。我的处理流程是「报错原文完整喂回」:
text
你上一步的修改存在问题,实测结果如下:
【操作】:cargo check -p rb-core
【期望】:编译通过
【实际】:error[E0597]: `repo` does not live long enough
(附完整借用检查器报错,包括它给出的修复建议)
请:1. 先定位根因并解释(不要直接改);
2. 给出最小范围修复;
3. 修复后重新执行 cargo test 与 clippy 并报告。
借用检查器的报错自带修复建议,完整喂回后 AI 一次收敛。这件事改变了我的投喂纪律:Rust 的编译器报错必须贴全------截断的报错等于浪费一次往返。
借用检查器打回之后收敛出的最终形态,就是 GitRepo 门面------git2 类型被彻底关在这个模块里,对外只暴露自有模型:
rust
// crates/rb-core/src/git/repo.rs(真实源码节选)
/// `git2::Repository` 的只读门面。外界不接触任何 `git2` 类型。
pub struct GitRepo {
repo: git2::Repository,
}
impl GitRepo {
/// 打开磁盘上的仓库。
pub fn open(path: &Path) -> Result<Self> {
let repo = git2::Repository::discover(path)
.map_err(|e| RbError::InvalidRepo(format!("{} ({e})", path.display())))?;
Ok(Self { repo })
}
/// 构建只读快照:分支、提交摘要、文件清单、大小与依赖清单文件。
pub fn snapshot(&self) -> Result<RepoSnapshot> {
let workdir = self
.repo
.workdir()
.ok_or_else(|| RbError::InvalidRepo("bare 仓库暂不支持".into()))?
.to_path_buf();
let branches = self.collect_branches()?;
let commit_count = self.count_commits()?;
let recent_commits = self.collect_recent_commits()?;
let (files, total_bytes) = collect_files(&workdir);
let dep_manifests = collect_dep_manifests(&files);
Ok(RepoSnapshot {
path: workdir.display().to_string(),
branch_count: branches.len(),
branches,
commit_count,
recent_commits,
file_count: files.len(),
total_bytes,
dep_manifests,
})
}
}
门面内部还有一处典型的 libgit2 生命周期妥协值得展开------历史 blob 采集的「分批独立开仓」:
rust
// crates/rb-core/src/git/repo.rs · history_blob_metas(真实源码节选)
// 分批处理:每批独立开仓(Repository Send 非 Sync 硬约束)。
// 改为顺序处理以在批次边界检查取消标志(大仓库场景下批次少,
// 取消响应延迟可接受;后续可恢复并行 + 批间检查)。
let width = batch_size.max(1);
let total_commits = commit_oids.len();
if let Some(cb) = on_progress {
cb(0, total_commits);
}
let mut processed_commits = 0usize;
let mut batch_results: Vec<Vec<(String, String, u64, CommitSummary)>> = Vec::new();
for batch in commit_oids.chunks(width) {
if let Some(flag) = cancel_flag {
if flag.load(Ordering::Relaxed) {
break;
}
}
batch_results.push(process_batch(&workdir, batch));
processed_commits += batch.len();
if let Some(cb) = on_progress {
cb(processed_commits, total_commits);
}
}
git2::Repository 是 Send 但非 Sync,不能跨线程共享引用,所以每批扫描在 process_batch 里独立开仓;而「批次边界检查取消标志」顺手解决了另一个问题------用户点取消后,最多等一个批次(默认 64 个提交)就停下,已采集的部分 blob 保留为部分报告。这两个约束(生命周期 + 可取消)在一处代码里同时落地,是 libgit2 绑定给上的真实一课。

Rust 的类型系统还带来一个 TS/Java 项目给不了的叙事:把非法状态挡在编译期。Severity 与 Category 是真实源码里的普通枚举而非字符串------「不存在的严重度」在仓衡里不是运行时 bug,是编译错误:
rust
// crates/rb-core/src/model/finding.rs(真实源码节选)
/// 问题严重程度。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Critical,
Warning,
Info,
}
/// 检查维度。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Category {
Structure,
History,
Branches,
Deps,
Security,
}
/// 一条检查结论。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finding {
/// 稳定唯一标识,如 `history.merge-commit-ratio`。
pub id: String,
/// 严重程度。
pub severity: Severity,
/// 所属维度。
pub category: Category,
/// 简短标题。
pub title: String,
/// 证据(触发该结论的数据摘要)。
///
/// # 约定
/// 必须为**掩码后的证据字符串**,禁止包含未掩码的敏感原文;
/// 安全类检查的强制掩码规则归属 D3 话术落地(一处掩码,处处掩码:
/// 序列化、导出与 IPC 复用同一 Finding)。
pub evidence: String,
/// 改进建议。
///
/// # 约定
/// 必须是面向开发者的**可执行中文建议**,包含明确的命令或操作步骤。
/// 正例:`使用 git filter-repo 从历史中移除该文件`;
/// 反例:仅"请优化"类无操作步骤的文本。
pub suggestion: String,
}
注意 Finding 的文档注释:掩码纪律和「可执行中文建议」都写在了类型定义上------AI 每次生成新的 Checker 都会看到这份契约,约束跟着类型走,而不是跟着某条会话走。
3.3 会话全程:从需求到验收的完整链路
一个特征从话术到收口,全程在一个会话里完成。D4(Tauri 壳竖切)的 design 摘要把 IPC 命令、数据模型、三视图、样式系统、安全设计一次定清,关键决策四条(rb-core 零改动、前置依赖 stub 策略保证 UI 可独立编译、src-tauri 遗留不改造、前端单页 v-show 切换不引入 vue-router):

随后 tasks.md 生成 12 个任务组 44 个子任务,从「Tauri 2 壳工程搭建与安全基线」到「报告页·总评与雷达图」,按依赖顺序排列,逐组执行:

质量门禁阶段的「AI 与编译器对抗」是最有看点的一幕:cargo clippy --all-targets 一次报出 10 个编译错误,Agent 把它们整理成「错误 / 根因 / 修复方案」三列表格逐项消化------E0423(模块导入写成函数)、E0369(Finding 未 derive PartialEq)、E0277(CheckerRegistry 未实现 Debug)、E0106+E0308(ScanContext 生命周期不匹配):

修复后的完成报告同样以表格收口------每个错误对应文件行号与修法(E0423×3 → use crate::engine::scan::scan、E0369×2 → Finding derive PartialEq, Eq、E0277×2 → 手动 impl Debug for CheckerRegistry 只输出 len),最后一行 clippy 警告也顺手清零:

验收阶段跑完异常场景(不存在路径 / 非 git 目录 / 无参数,退出码与错误信息逐项核对)、只读幂等验证(扫描前后 demo-repo HEAD、status、log 零变更)、源码规模盘点(18 个 .rs 文件 2106 行,三特征全部任务组完成、质量门禁全绿):

IDE 之外,AtomGit 工作台上的华为云码道 Agent 会话也能看到同一条工作线------代码拉到本地、逐文件修改(rb-app/commands.rs 的 scan_with_progress、rb-app/state.rs 的 begin_scan CAS 修复)、任务状态跟随更新,多仓库会话列表并存:

四、功能演示:从选目录到五维雷达
项目完成后,cargo tauri build 产出 dmg(产物体积与启动数据以实测为准),安装后完整走一遍体检链路。
4.1 接入与流式扫描
选择目录后显示仓库基本信息,点击「开始体检」:顶部进度条按阶段推进,当前 Checker 名滚动展示,中途可取消(取消后保留已完成部分并标注「已取消」)。扫描跑在 async_runtime,UI 全程不卡。

进度事件不是前端轮询出来的,是 Rust 侧在引擎阶段边界主动 emit 的。estimate_percent 把 (stage, done, total) 映射为 0-100 的估算百分比------各阶段权重手工分配(初始化 5%、工作区采集到 10%、历史采集占 10-90 按提交比例线性推进、逐检查器检查占 90-99):
rust
// crates/rb-app/src/commands.rs(真实源码节选)
/// 启动扫描:异步包装同步 scan(spawn_blocking 避免占死 tokio worker),
/// 完成后 emit scan-done;失败 emit scan-error(与用户取消事件分离)。
#[tauri::command]
pub async fn start_scan(
app: tauri::AppHandle,
state: State<'_, ScanState>,
path: String,
config: Option<crate::dto::ScanConfigDto>,
) -> Result<(), String> {
let path_buf = PathBuf::from(&path);
rb_core::git::GitRepo::open(&path_buf).map_err(map_error)?;
// CAS 领取扫描权:复位取消标志并原子置位 scanning,拒绝并发扫描。
if !state.begin_scan() {
return Err("已有扫描正在进行,请先等待完成或取消".to_owned());
}
// ... spawn + spawn_blocking 执行 scan_with_progress,
// 进度回调里 app.emit("scan-progress", payload),
// 完成后 emit("scan-done", dto),取消时 dto.cancelled = true
}
同步的 engine::scan 被 spawn_blocking 包起来丢进阻塞线程池,避免占死 tokio worker 冻结 UI------Tauri 异步命令的标准姿势。begin_scan() 用 CAS 方式领取扫描权,并发扫描在门口就被拒绝。
4.2 五维雷达与 finding 详情
扫描完成:顶部总评大数字(≥80 绿 / 60-79 黄 / <60 红),SVG 自绘五维雷达(Structure / History / Branches / Deps / Security),finding 列表按严重度分组排序。点开任意一条 finding:证据、修复建议是可执行的命令(如 git filter-repo 示例)。


这是对真实 Flutter 项目 flutter-ledger 的体检结果:总评 80(绿/健康),结构维度 46 条警告(Flutter 构建产物 .dart_tool/ 下的 app.dill、app.so 等大文件),历史/分支/依赖/安全四个维度零发现满分。点开任意一条 finding:证据、修复建议是可执行的命令(如 git filter-repo 示例),与源码里 Finding.suggestion 的约定一致。
扫描编排的核心循环对应引擎里的真实实现------plan 合并后按切片定点采集,逐检查器纯计算,每个检查器完成即上报进度:
rust
// crates/rb-core/src/engine/scan.rs(真实源码节选)
// plan 阶段:base 上下文收集全部检查器的切片请求。
let mut ctx = ScanContext::base(&snapshot, config);
let plans: Vec<_> = registry.list().iter().map(|c| c.plan(&ctx)).collect();
let merged = merge_plans(&plans);
// 定点采集:仅落地合并后需要的切片。
for slice in merged.slices() {
match slice {
DataSlice::FileContents { pattern } if pattern == "all" => {
report_progress("workdir", 0, 0);
let metas = repo.workdir_file_metas()?;
let metas: Vec<_> = metas
.into_iter()
.filter(|m| !ignore.is_ignored(&m.path))
.collect();
ctx.insert_slice(slice.clone(), SliceData::FileContents(metas));
report_progress("workdir", 1, 1);
}
DataSlice::FullHistory => { /* 历史采集,批次级进度回调 */ }
_ => {}
}
}
// check 阶段:逐检查器纯计算产出 findings,每个检查器完成即上报进度。
let total_checkers = registry.list().len();
report_progress("check", 0, total_checkers);
let mut findings = Vec::new();
for (i, checker) in registry.list().iter().enumerate() {
findings.extend(checker.check(&ctx));
report_progress("check", i + 1, total_checkers);
}
// 评分:按各维度 finding 严重度扣分。
let scores = compute_scores(&findings);
评分规则同样简单透明:每个 finding 按严重度扣分(Critical -15 / Warning -8 / Info -3),各维度独立计算并 clamp 到 0, 100,无 finding 的维度保持 100------所以 flutter-ledger 那份报告里「结构 0 分」的雷达凹角,就是 46 条 Warning × 8 分扣穿的结果。
4.3 历史对比:同一仓库不同时期的体检报告 diff
报告页之外还有一个「历史对比」视图:每次扫描自动落入历史(最多保留 20 条),任意两条可标记为基准/对照做 diff------总分变化、发现项变化、新增/消除/仍存在的 finding 一目了然。双语切换(中/EN)也是一期迭代进去的:



这份对比里基准是 flutter-ledger(46 项发现)、对照是 FlowTime(98 项发现):总分同为 80,但发现项构成完全不同------新增 59 条大文件警告全部来自 FlowTime 的构建产物。对「体检 → 清理 → 复扫」的修复闭环来说,这个 diff 视图就是验收凭证。
双语切换的效果(同一页面中/英文两套文案,localStorage 持久化):

4.4 CLI 通道
不会装桌面应用的评审走 CLI。以下是 cargo run -p rb-core -- scan ./demo-repo 的真实终端输出(demo-repo 是内置合成数据仓库,预置一个 5MB 的 big.bin 与一个未合并分支 old-branch):
bash
$ cargo run -p rb-core -- scan ./demo-repo
仓衡 (rb) 仓库摘要: /Users/dickeryang/Desktop/projects/repo-balance/demo-repo/
提交数: 3
分支数: 2 (main, old-branch)
文件数: 3
总大小: 5242969 bytes
体检结论: 共 1 条 finding
[Warning] big-files.worktree:big.bin --- 工作区存在大文件: big.bin
路径: big.bin
当前大小: 5.0 MB
历史最大: 5.0 MB
首次引入: 5c4a1a21 chore: big file
建议: 建议将 big.bin 加入 .gitignore,或使用 git lfs migrate import --include="big.bin" 迁移到大文件存储
一条命令、无需桌面环境,报告结构(finding → 证据 → 可执行建议)与桌面应用完全同源------这就是「内核可独立编译为 CLI」这条架构决策兑现的评审通道。
五、踩坑记录:AI 写对了九成,剩下一成靠人
如实记录开发过程中真实遇到的问题(坑位与数据以开发日志为准,成稿时填入实测细节)。
1:git2 类型逃逸出门面,测试写不下去
现象 :D1 的分支健康规则写单测时发现,AI 生成的 Checker 直接持有了 git2::Repository 引用------单测必须构造真实 libgit2 仓库对象,生命周期与清理纠缠不清,测试写了三版都不干净。
根因:话术里写了「GitRepo 门面」,但没写死「git2 类型不得逃逸出 git 模块」,AI 按最短路径把底层类型透传给了上层。
解决 :用 8.2 重构话术让 AI 列出全部逃逸点,收拢进 GitRepo 门面,对 Checker 只暴露自有 RepoSnapshot 只读模型(借出数据,不借出句柄)。重构后单测只需要构造快照数据,干净了一个数量级。这条纪律随后被固化为每期终检的搜索项。
重构后的边界是可以用一条搜索验证的:全仓库 grep git2:: 只会命中 crates/rb-core/src/git/ 下的文件(外加 Cargo.toml 的依赖声明)------这正是 3.2 节贴出的 GitRepo 门面代码。模块 mod.rs 的头注释也把这个约束写死了:
rust
// crates/rb-core/src/checker/mod.rs 模块头(真实源码)
//! checker 模块:检查器插件体系(契约、注册表、切片计划、扫描上下文、占位实现)。
//!
//! 模块依赖方向:mod → plan/context → model/error,noop → mod,无循环依赖;
//! 本模块不 import git2、不引入 tauri,全部公共类型经 `crate::checker::`
//! 单一命名空间导出。
教训:「封装某依赖」这类约束必须写成「类型不得出现在哪些文件」的可搜索规则,否则 AI 的「封装」只是包了一层皮。
2:Tauri capability 漏配,前端调用 dialog 无响应
现象:竖切联调时,前端点「选择目录」毫无反应------没有报错、没有弹窗,像按钮没绑事件。
根因 :Tauri 2 的权限模型是显式 capability:dialog:open 权限没有加进 capabilities 配置,IPC 调用被静默拒绝。AI 生成的配置文件少了这一项,且静默失败让排查方向先在事件绑定上绕了两圈。
解决:补上 capability 后恢复正常;排障话术中追加「capability 权限名是否为 Tauri 2 当前版本的准确拼写(对照官方文档核实)」检查项。
教训:Tauri 2 的显式权限模型下,「静默拒绝」是配置类问题最迷惑的表现------无报错不等于代码没执行到,先查 capability 再查业务逻辑。
修复后的配置长这样------三个权限就是全部能力面,文件读取始终在 Rust 侧,前端拿不到任意 fs 访问:
json
// crates/rb-app/capabilities/default.json(真实源码)
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "最小能力集:窗口管理 + 目录对话框 + 事件机制",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:allow-open",
"core:event:default"
]
}
注意权限名是 dialog:allow-open 而不是文档里常见的 dialog:open------Tauri 2 的权限粒度是「每个 dialog 方法一条 allow 规则」,当时的坑正是栽在这种拼写差异上。这个文件本身就是「capabilities 最小能力集」红线的活证据:没装 fs 插件,CSP 收紧为 'self'。
3:敏感信息规则误报,password = "" 也中招
现象 :敏感信息扫描首次跑真实仓库,一批测试代码里的 password = "" 空赋值、示例文件里的占位符全部报 Warning,误报多到报告没法看。
根因:初版正则只匹配「键名 + 赋值」结构,没有值有效性判断与路径降噪------AI 按「规则覆盖面」实现,而真正决定可用性的是「误报率」。
解决:三条降噪规则落地:空值不报、example/sample/test 文件名中的命中降为 Info、高熵判断过滤纯占位符。用真实误报样本调完规则后,报告信噪比恢复正常。调参过程全程走掩码 evidence,未在会话中展开过任何真实命中内容。
教训:检测类工具的验收指标是误报率不是规则数量------「每条规则一命中一不命中」的用例结构(话术中预先写死)是控制误报最便宜的时机。
5.4 小结
三个坑依旧符合系列规律:AI 的失误都发生在「话术没约束到的地方」------模块边界的可搜索定义、平台权限模型的静默语义、检测工具的验收指标定义。架构决策与验收标准由人写死,AI 在约束内执行。
质量收口的实测数据(2026-09-30 本机实测):
- cargo test 全绿(rb-core 56 + rb-app 6,共 62 个用例,0 失败);
cargo clippy --all-targets零 warning;非测试代码零 unwrap/expect(搜索确认);- rb-core 零 tauri 依赖;全仓库零网络请求代码(reqwest/ureq/fetch 均不存在);
- 自我扫描:对 repo-balance 仓库本体跑
scan .,引擎正常出报告(构建产物 target/ 下的中间文件会被规则命中,配合.repobalance-ignore排除后报告干净)。
六、提效数据:AI 辅助开发到底快了多少
如实给出本次开发的提效记录(基于开发日志的实际耗时统计,为个人单项目样本,仅供参考)。
| 开发阶段 | 纯手写预估 | AI 辅助实际 | 提效来源 |
|---|---|---|---|
| workspace 骨架 + GitRepo 门面 | 约 1.5 天 | 约 2.5 小时 | 模板与绑定类代码收益最大 |
| 大文件检查器 + 引擎编排 + 单测 | 约 3 天 | 约 6 小时 | 规则是纯函数,AI 强项 |
| Tauri 壳 + 前端竖切 | 约 2 天 | 约 5 小时 | 借用检查器往返是额外成本 |
| 报告页 + 历史/对比/i18n + 质量收口 + 文档 | 约 2.5 天 | 约 6 小时 | 迭代功能小而密,自查清单执行快 |
开发节奏与交付物在 git 历史里完全可溯------功能提交的粒度大致是一天 1~2 个特性(以下为真实 commit 序列节选):
text
$ git log --oneline(节选)
de81c14 fix: begin_scan 竞态 + 雷达图维度名 i18n + CI 扫描路径
6995a3b fix: i18n 阶段名中英混合 + 历史删除选择残留
a190db2 feat: 国际化 - 中英文切换(极简 i18n,localStorage 持久化)
9db4038 feat: CI 集成 - --ci/--json 标志 + GitHub Actions/AtomGit 流水线示例
a620d24 feat: 忽略规则 - .repobalance-ignore gitignore 风格路径排除
text
$ cargo test(workspace 全量)
running 56 tests ...
test result: ok. 56 passed; 0 failed (rb-core)
running 6 tests ...
test result: ok. 6 passed; 0 failed (rb-app)
两点如实说明:一是「纯手写预估」基于经验估算,存在主观误差;二是 Rust 路线的「编译器往返」是真实成本------借检报错喂回的每次往返约 1~2 分钟,本期发生约十余次,已计入实际耗时。综合感受:整体工期压缩到传统方式的四分之一左右;Rust 类型系统把一类运行时 bug(非法状态值)提前到了编译期,这在「AI 生成占比高」的项目里价值放大------类型即护栏。
七、开源地址与运行方式
项目已开源,包含可构建源代码、README(作品介绍)与开源协议:
项目仓库:https://atomgit.com/dickeryang/repo-balance.git(AtomGit 公开仓库)
本地运行两条通道:
bash
# 通道一:CLI(无需桌面环境,一条命令出报告)
git clone https://atomgit.com/dickeryang/repo-balance.git
cd repo-balance
cargo build --workspace
cargo run -p rb-core -- scan ./demo-repo # 内置合成数据示例仓库
cargo run -p rb-core -- scan ./你的仓库 --json # 输出完整 JSON 报告
# 通道二:桌面应用(macOS)
# README 下载 dmg → 首次启动右键打开(Gatekeeper 说明见 README)→ 选择目录体检
仓库内附 docs/ci-integration.md(GitHub Actions / AtomGit 流水线示例,CLI 支持 --ci/--json/--quiet 标志,CI 模式下存在 Critical 时退出码 2)与 docs/prototype/(产品原型 HTML)。示例仓库中所有「敏感信息」均为 SAMPLE-NOT-A-REAL-KEY 类合成数据。项目采用 Apache-2.0 协议开源。
八、总结与平台使用建议
8.1 结论
第四种端形态、第四门语言,方法依然成立:在「人定架构与验收、AI 做实现」的分工下,Rust + Tauri 桌面应用一周完成。
本期的新发现有两个:一是借用检查器意外地成为了 AI 协作的「第三方质检员」------报错原文喂回即收敛,且收敛后的代码比一次生成的更符合 Rust 惯例;二是「内核可独立编译为 CLI」这个架构决策同时服务了可测试性、CI 集成与评审体验,是本期性价比最高的一个决策。
8.2 给平台的建议
四周连续使用下来,对 CodeArts 代码智能体的几点优化建议(如实反馈,供官方参考):
- Rust 生态语料:libgit2 这类绑定库的生命周期模式建议有专项感知,减少借检往返;生成代码标注「依据的 crate 版本」会很有帮助;
- 配置类文件的文档对齐:Tauri capabilities 这类版本敏感配置,希望 Agent 主动提示与官方文档核对(与第 3 期权限名建议同源);
- 项目级约定持久化(连续三期提出,期待最高):模块边界、掩码纪律这类项目级约束,若能持久化就不必每条话术重复投喂。
8.3 写在最后
四种端形态(Web 全栈 / 微信小程序 / 鸿蒙原生 / 桌面应用)、四门语言主线、同一套方法。Rust 这一期额外验证了一件事:AI 协作的质量下限,取决于人把约束写得多可验收------「封装」要写成「类型不得逃逸出哪个目录」,「安全」要写成「掩码在构造时完成」。
收官期(第 5 期,10-12 ~ 10-18)将是零依赖 TypeScript + Canvas 的小游戏「熵减特勤局」,确定性回放引擎登场------一整局游戏就是一个测试用例。如果你也在参加本期华为云码道 AI 编程挑战赛,欢迎在评论区交流。
说明:因 AtomGit 的码道额度用尽,本文部分功能使用码道客户端进行开发,其余功能通过码道 Web 端完成。
觉得有用就点个赞,有疑问或不同看法欢迎评论区交流。