本文是 IntelliGit 智能提交链路优化复盘系列的下篇,聚焦能力落地:AST 语义全链路接入、Hunk 级精准暂存、智能分组校正、提交信息生成质量提升。建议先阅读上篇了解底层引擎设计。
一、前言:底层能力落地,实现体验闭环
上篇介绍了astChangeAnalyzer.ts中 AST 语义分析、Hunk 细粒度识别、三级置信度计算等底层基建。技术基建的核心价值最终要在业务场景中得到体现。
本次迭代在搭建完底层分析引擎后,完成了智能分组、提交信息生成、精准暂存、视图展示全链路接入,解决了传统智能提交体验差、准确率低、实用性弱的痛点。
整体改造围绕三大模块展开:
底层 AST 分析引擎(astChangeAnalyzer.ts) ↓ 核心服务层(smartCommitService.ts) ├── analyzeSmartCommitChanges() ← AST 上下文全量接入分组分析 ├── generateSmartCommitMessage() ← AST 上下文驱动提交信息生成 └── stageGroupAndGenerateMessage() ← Hunk 级精准暂存 + 按组生成 ↓ LLM Provider 层(smartCommitProvider.ts) ├── analyzeChanges() ← AI 分组 + 本地 AST 兜底 └── generateMessage() ← AI 生成 + 格式化校验 ↓ 视图层(CommitPanel / DiffView) └── 可视化展示 + 精准操作二、核心服务升级:AST 语义全链路接入
2.1 构建完整 AST 上下文
旧版analyzeSmartCommitChanges()仅传递 Diff 文本和文件列表给 AI,AI 只能凭文本猜测变更意图。
新版在调用 Provider 之前,先构建完整的 AST 语义上下文:
export async function analyzeSmartCommitChanges(): Promise<AgentResult<SmartCommitAnalysisResult>> { const [workdirDiff, stagedDiff] = await Promise.all([ invokeGit('diff.workdirRaw', {}), invokeGit('diff.stagedRaw', {}) ]) const diff = workdirDiff.diff || stagedDiff.diff const files = getChangedFiles() // 1. 构建文件内容映射(HEAD 版本 vs 当前版本) const astContentMap = await buildAstContentMap(diff) // 2. 运行 AST 分析引擎,得到结构化语义洞察 const astInsights = analyzeAstChanges(files, diff, astContentMap) // 3. 渲染为 AI 可直接消费的文本上下文 const astContext = renderAstContext(files, diff, astContentMap) if (!diff.trim()) { return { success: false, error: '当前没有可分析的代码变更' } } const result = await smartCommitProvider.analyzeChanges({ diff, files, astContext }) // 4. 用 AST 结果反向校正 AI 分组数据 if (result.success && result.data) { return { ...result, data: enrichGroupsWithAst(result.data, astInsights) } } return result }renderAstContext()输出的内容示例:
- [typescript/modified] smartCommitService.ts:函数体/结构变更、参数变更,涉及 buildGroupContext、enrichGroupsWithAst,2 个 Hunk(置信度 high) 变更类型:函数体/结构变更, 参数变更 关注符号:buildGroupContext, enrichGroupsWithAst AST 符号:function:buildGroupContext@28-35, function:enrichGroupsWithAst@82-97 AST Diff:新增参数 hunks: string[],修改函数体逻辑 Hunk 摘要:@@ -28,6 +28,8 @@(+2, -0,置信度 high,所属 function:buildGroupContext)AI 拿到这份上下文后,不再需要"猜"变更意图,而是基于精准的符号级变更描述来做分组判断。
2.2buildAstContentMap():获取文件双版本内容
AST 分析需要文件的旧版本(HEAD)和新版本内容做对比。buildAstContentMap()通过解析 Diff 文件路径,并发拉取各文件在 HEAD 的内容:
async function buildAstContentMap(diff: string): Promise<AstFileContentMap> { const entries = parseDiffFilePaths(diff) const pairs = await Promise.all( entries.map(async (entry) => { const oldPath = entry.oldPath || entry.filePath const [oldContent, newContent] = await Promise.all([ entry.status === 'added' ? Promise.resolve('') : readGitText(oldPath, 'HEAD'), Promise.resolve('') // 新版本内容由 AST 引擎从 Diff 中重建 ]) return [entry.filePath, { oldContent, newContent }] as const }) ) return Object.fromEntries(pairs) }对于新增文件(status === 'added'),旧版本内容为空,AST diff 会将所有符号归类为added-symbol。
三、智能分组双向增强:AI + 本地 AST 校正
3.1 传统纯 AI 分组的问题
纯 AI 分组的核心缺陷是随机性强、结果不可控:同样的 Diff 两次调用可能产生不同分组,且分组置信度全靠 AI "自述",没有客观依据。
3.2enrichGroupsWithAst():AST 回填校正
新增的enrichGroupsWithAst()将底层 AST 分析结果回填至 AI 分组数据:
function enrichGroupsWithAst( result: SmartCommitAnalysisResult, insights: AstChangeInsight[] ): SmartCommitAnalysisResult { const insightMap = new Map(insights.map(insight => [insight.filePath, insight])) const fallback = buildAnalysisSummary(insights) const groups = result.groups.map(group => { // 找出该分组关联文件的 AST 洞察数据 const groupInsights = group.files .map(file => insightMap.get(file)) .filter((insight): insight is AstChangeInsight => Boolean(insight)) return { ...group, // AI 给了置信度就用 AI 的,否则用 AST 聚合计算结果 confidence: group.confidence || mergeConfidence( groupInsights.map(insight => insight.confidence) ) } }) return { ...result, groups, // 分析摘要、置信度、变更类型:AI 有则用 AI,否则 AST 兜底 analysisSummary: result.analysisSummary || fallback.analysisSummary, confidence: result.confidence || fallback.confidence, changeKinds: result.changeKinds?.length ? result.changeKinds : fallback.changeKinds } }分析摘要生成逻辑(buildAnalysisSummary()):
function buildAnalysisSummary(insights: AstChangeInsight[]): Pick<SmartCommitAnalysisResult, 'analysisSummary' | 'confidence' | 'changeKinds'> { const changeKinds = [...new Set(insights.flatMap(i => i.changeKinds))].slice(0, 6) const confidence = mergeConfidence(insights.map(i => i.confidence)) const files = insights.length return { analysisSummary: files > 0 ? `识别到 ${files} 个文件的 ${changeKinds.slice(0, 3).join('、') || '代码'} 变更` : '已完成智能分组分析', confidence, changeKinds } }改造后,智能分组从「纯 AI 黑盒输出」升级为「AI 智能判断 + 本地 AST 精准校正兜底」的双向增强模式。
四、完善分组上下文:高质量提交信息的基础
4.1buildGroupContext():封装完整意图数据包
生成提交信息时,旧版只向 AI 传递 Diff 文本。新版通过buildGroupContext()为每个分组封装完整的语义数据包:
function buildGroupContext(group: CommitIntentGroup): string { const scope = group.scope ? `\n作用域:${group.scope}` : '' const confidence = group.confidence ? `\n置信度:${group.confidence}` : '' const hunkText = group.hunks?.length ? `\n关联 Hunk:\n${group.hunks.map(h => `- ${h}`).join('\n')}` : '' return `提交意图:${group.type}${scope}${confidence}\n分组摘要:${group.summary}\n分组文件:\n${ group.files.map(f => `- ${f}`).join('\n') }${hunkText}` }输出示例:
提交意图:feat 作用域:smart-commit 置信度:high 分组摘要:新增 AST 上下文接入与 Hunk 精准暂存能力 分组文件: - src/services/smartCommitService.ts - src/utils/astChangeAnalyzer.ts 关联 Hunk: - src/services/smartCommitService.ts@@@ -82,12 +82,28 @@AI 基于这份完整意图包生成提交信息,产出内容更贴合 Conventional Commits 规范,精准匹配实际改动场景。
4.2 Provider 层的提交信息格式化与校验
smartCommitProvider.ts中的格式化函数对 AI 输出做严格清洗,避免不合规格式进入提交记录:
function sanitizeType(type: string | undefined): string { const normalized = type?.trim().toLowerCase() return normalized && COMMIT_TYPES.has(normalized) ? normalized : 'chore' } function sanitizeScope(scope: string | undefined): string | undefined { const normalized = scope?.trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-') if (!normalized || !SCOPE_PATTERN.test(normalized)) return undefined return normalized } function sanitizeSubject(subject: string | undefined): string { const normalized = limitText(subject || '更新代码变更', MAX_COMMIT_SUBJECT_LENGTH) return normalized.replace(/[。.!!]+$/g, '') || '更新代码变更' }校验规则一览:
| 字段 | 规则 |
|---|---|
type | 必须在 COMMIT_TYPES 白名单内,否则降级为chore |
scope | 只允许小写字母、数字、连字符,不合规则丢弃 |
subject | 截断至 72 字符,移除末尾标点 |
body | 去除首尾空白,为空则省略 |
五、Hunk 级精准暂存:最具工程价值的核心突破
5.1 传统文件级暂存的痛点
日常开发中,一个文件往往包含多次独立改动——新增功能、修复 bug、代码重构混在一起。传统git add <file>只能整体暂存,必须手动用git add -p拆分,操作繁琐且极易出错。
5.2stageGroupAndGenerateMessage():按意图分组暂存
新版核心函数stageGroupAndGenerateMessage()实现了按意图分组精准暂存:
export async function stageGroupAndGenerateMessage( group: CommitIntentGroup ): Promise<AgentResult<SmartCommitGroupWorkflowResult>> { return withOperation('commit.generateMessage', async () => { const files = normalizeFiles(group.files) // Step 1:获取该分组文件的 workdir diff const workdirGroupDiff = (await Promise.all( files.map(async f => (await invokeGit('diff.workdirRaw', { path: f })).diff) )).filter(Boolean).join('\n') // Step 2:优先尝试 Hunk 级 patch 暂存 const appliedPatch = workdirGroupDiff.trim() ? await applyGroupHunks(group, workdirGroupDiff) : null // Step 3:Hunk 匹配失败时,回退到文件级暂存(兜底) if (!appliedPatch) { for (const f of files) { await invokeGit('staging.add', { path: f }) } } await useGitStatusStore.getState().refreshStatus() // Step 4:基于实际暂存的 diff 生成提交信息 const stagedGroupDiff = (await Promise.all( files.map(async f => (await invokeGit('diff.stagedRaw', { path: f })).diff) )).filter(Boolean).join('\n') const groupDiff = appliedPatch || stagedGroupDiff const astContentMap = await buildAstContentMap(groupDiff) const result = await smartCommitProvider.generateMessage({ diff: groupDiff, stagedFileCount: files.length, groupContext: buildGroupContext(group), astContext: renderAstContext(files, groupDiff, astContentMap) }) // ... }) }5.3 Hunk 匹配与 Patch 应用
applyGroupHunks()从完整 Diff 中精准提取分组对应的 Hunk patch,只暂存目标变更片段:
function matchGroupHunks(group: CommitIntentGroup, diff: string): RawHunk[] { const files = new Set(normalizeFiles(group.files)) const wantedHunks = new Set(group.hunks || []) return parseRawDiffHunks(diff).filter(hunk => { if (!files.has(hunk.filePath)) return false if (wantedHunks.size === 0) return true // 优先精确匹配 `filePath@@hunkHeader` 格式的 Hunk ID return wantedHunks.has(`${hunk.filePath}@@${hunk.header}`) || wantedHunks.has(hunk.header) }) } async function applyGroupHunks(group: CommitIntentGroup, diff: string): Promise<string | null> { const hunks = matchGroupHunks(group, diff) if (hunks.length === 0) return null const patch = hunks.map(h => h.patch).join('\n') await invokeGit('staging.applyPatch', { patch }) return patch }Hunk ID 格式定义在CommitIntentGroup.hunks字段中,格式为${filePath}@@${hunkHeader},例如:
src/services/smartCommitService.ts@@@ -82,12 +82,28 @@AI 在分组时可以为每组分配关联的 Hunk ID 列表,实现"AI 决策 + 精准执行"的完整闭环。
5.4 容错兜底策略
| 场景 | 处理方式 |
|---|---|
| Hunk 匹配成功 | 应用精准 patch,只暂存目标变更 |
| Hunk 匹配失败(patch 为空) | 回退到git add <file>文件级暂存 |
| 暂存后 diff 仍为空 | 返回错误,不继续生成信息 |
三层容错保证功能 100% 可用,不会因 Hunk 匹配失败阻塞正常工作流。
六、Provider 层的降级闭环设计
smartCommitProvider.ts中的LlmSmartCommitProvider实现了AI 优先、本地兜底的完整闭环:
async analyzeChanges(input: SmartCommitAnalyzeInput): Promise<AgentResult<SmartCommitAnalysisResult>> { const config = getCurrentLlmConfig() // 未配置 API Key → 直接走本地 fallback if (!hasUsableLlmConfig(config)) { const fallback = fallbackCommitGroups(input.files) as AgentResult<SmartCommitAnalysisResult> return { ...fallback, error: toFallbackReason(config) } } // 调用 AI const result = await runAgent(config, { ... }, parseStructured) if (result.success && result.data) { const sanitized = sanitizeGroups(result.data, input.files) if (sanitized) return { ...result, data: sanitized } } // AI 失败 / 数据校验不通过 → 本地 fallback const fallback = fallbackCommitGroups(input.files) as AgentResult<SmartCommitAnalysisResult> return { ...fallback, error: result.error || toFallbackReason(config), rawOutput: result.rawOutput } }降级层次:
AI 服务已配置 + 调用成功 + 数据校验通过 → 返回 AI 分组结果(经 AST 校正) AI 服务已配置 + 调用失败 / 数据异常 → 本地 fallback 分组 + 记录错误原因 AI 服务未配置 → 本地 fallback 分组 + 提示用户配置无论哪种情况,用户都能得到一个可用的分组结果,功能不会因 AI 服务问题而中断。
七、分组数据安全校验:sanitizeGroups()
AI 输出的分组数据在进入业务逻辑前,经过严格的安全过滤:
function sanitizeGroups( result: SmartCommitAnalysisResult, inputFiles: string[] ): SmartCommitAnalysisResult | null { const allowedFiles = new Set(inputFiles) const usedFiles = new Set<string>() const groups = result.groups .map(group => ({ type: sanitizeType(group.type), scope: sanitizeScope(group.scope), summary: limitText(group.summary, MAX_GROUP_SUMMARY_LENGTH), confidence: group.confidence, // 过滤掉不在变更文件列表中的幻觉文件,并去重 files: group.files .map(f => f.trim()) .filter(f => allowedFiles.has(f) && !usedFiles.has(f)) })) .filter(group => { group.files.forEach(f => usedFiles.add(f)) return group.summary && group.files.length > 0 }) .slice(0, 5) // 最多 5 个分组,避免过碎 return groups.length > 0 ? { groups, ... } : null }这里有一个重要细节:过滤 AI 幻觉文件。AI 有时会在分组中编造不存在于实际变更列表的文件路径,allowedFiles过滤器可完全杜绝这类问题。
八、完整链路数据流
本次全链路优化的完整数据流:
用户触发智能提交分析 ↓ invokeGit('diff.workdirRaw') / invokeGit('diff.stagedRaw') ↓ ↓ buildAstContentMap(diff) getChangedFiles() ↓ analyzeAstChanges() → AstChangeInsight[] renderAstContext() → AI 可用文本上下文 ↓ smartCommitProvider.analyzeChanges({ diff, files, astContext }) ↓ runAgent()(AI 分组) ↓ sanitizeGroups()(安全校验) ↓ enrichGroupsWithAst()(AST 置信度回填) ↓ SmartCommitAnalysisResult(分组 + 摘要 + 置信度 + 变更类型) ↓ 用户选择分组 → stageGroupAndGenerateMessage() ↓ applyGroupHunks()(Hunk 精准暂存) ← 失败 → git add <file>(回退) ↓ smartCommitProvider.generateMessage({ diff, groupContext, astContext }) ↓ formatCommitMessage()(格式化校验) ↓ 最终提交信息填入输入框九、优化价值复盘
核心能力对比
| 能力 | 优化前 | 优化后 |
|---|---|---|
| AI 分析上下文 | 仅 Diff 文本 | Diff + AST 符号 + 变更类型 + Hunk 摘要 |
| 智能分组依据 | 纯 AI 黑盒 | AI 分组 + AST 置信度校正 |
| 分组兜底机制 | 无 | AI 失败时本地 fallback 兜底 |
| 暂存粒度 | 文件级 | Hunk 级(失败回退文件级) |
| 提交信息上下文 | 仅 Diff | 完整意图包(意图 + 作用域 + Hunk + AST) |
| 输出格式校验 | 无 | type / scope / subject 严格校验 |
整体价值
本次全链路优化的本质,是将智能提交从"依赖文本匹配的粗粒度工具"升级为"基于 AST + Hunk 语义分析的智能化开发辅助能力"。
系统不再只知道"文件改了",更能读懂"代码结构改了什么、为何改动、可信度如何"。从底层分析、智能分组、精准暂存到提交信息生成,形成了完整的语义化智能链路,让 AI 辅助开发真正落地到高频 Git 操作场景。
系列导读
- 上篇:[从文本到语义:AST + Hunk 重构 IntelliGit 智能提交底层分析能力]
- 下篇(本文):全链路落地——AST 赋能智能分组、Hunk 精准暂存、提交信息生成