VSCode Commit AI插件开发:自动生成Git提交信息
发布时间:2026/10/7 12:37:54 锦皓数字建站

1. 为什么我要自己做一个提交信息生成插件每次写完代码git commit那一步总是让人纠结。改了三五个文件逻辑横跨好几个模块脑子里明明知道干了什么但要在-m后面用一句话说清楚还得符合团队规范真挺费劲。我以前的做法是打开终端敲git diff --stat扫一眼改了哪些文件再凭记忆拼一句feat: 增加用户登录校验之类的。问题是改得多了容易漏改得细了又写得太啰嗦时间一长提交历史就变得参差不齐回头查问题特别痛苦。后来我试过几个现成的提交信息生成工具要么是独立命令行程序要么是网页版粘贴 diff用起来都不够顺手。我每天大部分时间都在 VSCode 里如果能在源代码管理面板里点一下按钮直接根据暂存区的改动生成一条规范的提交信息那才是真正贴合工作流的方案。于是就有了这个VSCode Commit AI插件——一个跑在编辑器内部、读取 Git 暂存区差异、调用大模型接口、把生成结果直接填进提交输入框的小工具。它解决的核心问题很具体把看 diff、想措辞、敲格式这三步压缩成一次点击。适合所有用 Git 做版本管理、又希望提交历史保持整洁的开发者尤其是团队里对 commit message 有约定式提交Conventional Commits规范要求的场景。哪怕你之前没写过 VSCode 插件只要会一点 TypeScript 和 Node.js跟着下面的思路也能自己撸一个出来。我踩过的坑、调过的参数、绕过的弯路都会在这篇里讲清楚。2. 插件整体设计与核心技术选型2.1 为什么选择 VSCode 扩展而不是独立 CLI一开始我考虑过写个 Node 脚本配合 Git 的prepare-commit-msg钩子自动生成信息。这个方案理论上最无感提交时钩子触发脚本读 diff、调接口、写入文件一气呵成。但实际用下来有几个硬伤钩子是同步阻塞的网络请求一慢整个git commit就卡在那里体验很差而且钩子对错误处理不友好接口挂了或者超时提交直接失败还得手动清理.git目录里的临时文件。VSCode 扩展就不一样了。它跑在编辑器的扩展宿主进程里有独立的生命周期网络请求是异步的失败了顶多弹个提示不影响你正常提交。更重要的是它能直接访问 VSCode 的SourceControlAPI拿到当前仓库、暂存区状态还能把生成结果写回提交输入框整个交互闭环都在编辑器内完成。用户不需要切换窗口不需要记命令符合工具应该消失在流程里的原则。从技术栈上看VSCode 扩展本质是一个 Node.js 模块用 TypeScript 写通过vsce打包成.vsix离线包分发。这个.vsix后缀就是 VSCode 扩展的标准打包格式本质上是个 zip里面装着编译后的 JS、package.json清单和资源文件。用户拿到.vsix后在扩展面板里选择从 VSIX 安装就能离线装非常适合内网环境或者不方便走应用市场的团队。2.2 核心模块拆解与数据流整个插件我拆成了四个核心模块数据流是单向的从 Git 到 UI 再回到 GitGit 差异采集模块负责调用git diff --cached拿到暂存区的改动。这里有个关键决策——只读暂存区不读工作区。原因是提交信息应该描述这次要提交什么而不是我本地还改了什么没暂存。如果读工作区生成的信息会包含未暂存的改动导致提交信息和实际提交内容对不上这是很多人容易忽略的坑。提示词组装模块把 diff 内容、文件列表、当前分支名、可选的用户自定义模板拼成一段发给大模型的 prompt。prompt 的质量直接决定生成结果的质量后面会详细讲怎么调。模型调用模块封装 HTTP 请求对接 OpenAI 兼容的接口。这里我特意做成可配置的 base URL 和 model 名称因为不同团队可能用不同的服务端点硬编码就失去灵活性了。结果回填模块把模型返回的文本清洗后通过SourceControl.inputBox.value写进提交输入框。注意是写进输入框而不是直接提交把最终决定权留给用户这个设计很重要避免 AI 生成错误信息后直接污染历史。数据流用一句话概括用户点击按钮 → 采集暂存区 diff → 组装 prompt → 调用模型 → 清洗结果 → 回填输入框。每一步都有错误处理任何一环失败都不会让插件崩溃只会给出提示。2.3 接口协议与配置项设计模型调用这块我选择兼容 OpenAI 的 Chat Completions 协议。原因很实际这个协议已经成为事实标准市面上大量服务都支持用户只要填一个 API Key 和一个 base URL 就能用不需要为每个服务商单独适配。请求体大致长这样{ model: gpt-4o-mini, messages: [ { role: system, content: 你是一个 Git 提交信息生成助手... }, { role: user, content: 以下是暂存区改动\n... } ], temperature: 0.3, max_tokens: 200 }temperature设成 0.3 是经过多次试验的结果。提交信息需要稳定、可预测不能太有创造性0.3 能在保证措辞自然的同时避免胡编乱造。max_tokens限制在 200 左右因为一条规范的提交信息通常不会超过这个长度限制一下能防止模型啰嗦。配置项我放在 VSCode 的 settings 里通过contributes.configuration声明用户可以在设置界面直接改也可以写进settings.json配置项类型默认值说明commitAi.apiKeystring空模型服务的 API KeycommitAi.baseUrlstring空接口地址需兼容 OpenAI 协议commitAi.modelstringgpt-4o-mini使用的模型名称commitAi.languagestringzh-CN生成信息的语言commitAi.conventionstringconventional提交规范可选 conventional 或 plaincommitAi.maxDiffLengthnumber8000diff 截断长度防止超 tokenmaxDiffLength这个配置很关键。有些提交改动巨大diff 动辄几万字符直接发给模型既慢又贵还可能超出上下文限制。我的做法是超过阈值就截断并在 prompt 里注明改动过大仅展示部分让模型基于可见部分生成概括性信息。实测下来 8000 字符能覆盖绝大多数日常提交。3. 核心细节解析与实操要点3.1 如何正确采集 Git 暂存区差异采集 diff 看起来简单其实有不少细节。最直接的做法是执行git diff --cached但这里要区分几种情况。如果暂存区是空的说明用户还没git add这时候应该提示请先暂存改动而不是生成一条空信息。如果仓库是全新的还没有任何提交git diff --cached依然能工作因为它对比的是暂存区和 HEAD没有 HEAD 时会对比空树。我用 Node 的child_process.execFile来执行 git 命令而不是exec。原因是execFile不经过 shell能避免命令注入风险而且参数传递更清晰。调用方式大概是这样import { execFile } from child_process; import { promisify } from util; const execFileAsync promisify(execFile); async function getStagedDiff(cwd: string): Promisestring { const { stdout } await execFileAsync( git, [diff, --cached, --unified3, --no-color], { cwd, maxBuffer: 10 * 1024 * 1024 } ); return stdout; }这里有几个参数值得说明。--unified3表示每个改动块显示上下 3 行上下文这是默认值但显式写出来更清楚。上下文太少模型看不懂改动意图太多又浪费 token3 行是平衡点。--no-color是必须的否则 diff 里会混入 ANSI 颜色转义码模型读到一堆\x1b[32m会懵。maxBuffer调大到 10MB防止大改动时缓冲区溢出报错。注意如果你的项目里有二进制文件或者大文件被暂存git diff可能会输出乱码或者超长内容。建议在采集后做一次过滤把Binary files ... differ这类行剔除只保留文本改动。另外我还额外采集了文件列表用git diff --cached --name-status拿到每个文件的状态新增 A、修改 M、删除 D。这个信息会作为 prompt 的一部分帮助模型理解改动的整体范围。比如看到一堆新增文件模型就知道这是个新功能看到大量删除可能是重构或清理。3.2 提示词工程让模型输出规范提交信息提示词是决定生成质量的核心。我前后改了十几版最终稳定下来的结构是这样的系统提示词负责定角色和规则你是一个专业的 Git 提交信息生成助手。你的任务是根据用户提供的暂存区改动生成一条简洁、准确、符合规范的提交信息。 规则 1. 使用约定式提交格式type(scope): subject 2. type 从以下选择feat, fix, docs, style, refactor, perf, test, chore, build, ci 3. subject 使用祈使句不超过 50 个字符结尾不加句号 4. 如果需要可以在空行后添加 body说明改动原因和影响 5. 只输出提交信息本身不要添加任何解释、前缀或 markdown 代码块标记用户提示词负责提供上下文当前分支feature/user-auth 改动文件 M src/auth/login.ts A src/auth/token.ts D src/auth/old-login.ts 暂存区 diff diff 内容这个结构的关键在于只输出提交信息本身这条约束。早期版本我没写这句模型经常返回好的根据您的改动我建议的提交信息是...这种废话还得用正则去清洗。明确禁止后输出干净多了。scope的提取也有讲究。我让模型从文件路径里推断模块名比如src/auth/login.ts就提取auth作为 scope。但不是所有项目都有清晰的目录结构所以我在 prompt 里加了一句如果无法确定 scope可以省略。这样既保证了有结构时能利用又不会在结构混乱时硬编一个。实操心得如果你团队有特殊的提交规范比如必须关联 Jira 单号可以在系统提示词里加一条在 subject 末尾添加 [PROJ-xxx] 格式的单号占位符。我一般建议加占位符而不是让模型编单号因为模型不知道真实单号编出来的是错的。3.3 结果清洗与回填的边界处理模型返回的文本不能直接往输入框里塞得先清洗。常见的脏数据有几类一是包裹在 markdown 代码块里的比如feat: xxx二是带前缀的比如提交信息feat: xxx三是多行文本里混入了模型自己的解释。我的清洗逻辑是分步的。先去掉首尾空白然后用正则匹配并剥离代码块标记再检查第一行是否符合^[a-z](\(.\))?: .的约定式格式如果不符合就尝试从文本里提取最像提交信息的那一行。最后把清洗后的结果写进输入框。function cleanCommitMessage(raw: string): string { let text raw.trim(); // 剥离 markdown 代码块 text text.replace(/^[\w]*\n?/, ).replace(/\n?$/, ); // 去掉常见前缀 text text.replace(/^(提交信息|commit message)[:]\s*/i, ); return text.trim(); }回填的时候用sourceControl.inputBox.value message。这里有个细节如果输入框里已经有用户手动写的内容直接覆盖会让人不爽。我的处理是如果输入框非空弹一个确认提示问用户是覆盖还是追加。这个小小的交互改进实际用起来体验差别很大。注意回填后不要自动执行提交。我见过有的工具生成完直接git commit结果模型偶尔抽风生成个错误信息历史就被污染了。把最后一步交给用户是负责任的设计。4. 完整实操流程与关键环节实现4.1 从零搭建扩展项目骨架先说环境准备。你需要 Node.js建议 18 以上和 npm然后全局装两个工具yo和generator-code这是 VSCode 官方提供的脚手架。命令是npm install -g yo generator-code。装完后运行yo code选择 New Extension (TypeScript)按提示填插件名、标识符、描述脚手架会自动生成目录结构。生成的核心文件有几个package.json是扩展清单声明激活事件、命令、配置项src/extension.ts是入口包含activate和deactivate两个导出函数tsconfig.json管编译。我建议先把package.json里的contributes部分改好把命令和配置项都声明清楚这样后面写代码时心里有数。命令声明大概这样contributes: { commands: [ { command: commitAi.generate, title: Commit AI: 生成提交信息, icon: $(sparkle) } ], menus: { scm/title: [ { command: commitAi.generate, when: scmProvider git, group: navigation } ] } }scm/title这个菜单位置很关键它让按钮出现在源代码管理面板的标题栏用户点一下就能触发。when条件限定只在 Git 仓库里显示避免在其他 SCM 提供商下出现无意义的按钮。4.2 激活逻辑与命令注册activate函数是插件的入口VSCode 在扩展被激活时调用它。激活时机我在package.json里声明为onCommand:commitAi.generate也就是用户第一次点击命令时才激活这样不会拖慢编辑器启动。在activate里我注册命令并绑定处理函数export function activate(context: vscode.ExtensionContext) { const disposable vscode.commands.registerCommand( commitAi.generate, async () { const gitExtension vscode.extensions.getExtension(vscode.git); if (!gitExtension) { vscode.window.showErrorMessage(未找到 Git 扩展); return; } const git gitExtension.exports.getAPI(1); const repo git.repositories[0]; if (!repo) { vscode.window.showErrorMessage(当前没有打开的 Git 仓库); return; } // 后续处理... } ); context.subscriptions.push(disposable); }这里通过vscode.git扩展的 API 拿到仓库对象比自己去执行 git 命令找仓库路径更可靠。repo.rootUri.fsPath就是仓库根目录传给前面的getStagedDiff用。整个生成流程用vscode.window.withProgress包起来显示一个正在生成...的进度提示。因为网络请求有延迟没有进度提示用户会以为按钮没反应反复点击。进度提示的 location 选Notification这样即使用户切走了也能看到状态。4.3 模型调用的完整实现与超时控制模型调用我用 Node 内置的fetchNode 18 支持不引入额外依赖。核心代码如下async function callModel( diff: string, config: vscode.WorkspaceConfiguration ): Promisestring { const apiKey config.getstring(apiKey); const baseUrl config.getstring(baseUrl); const model config.getstring(model, gpt-4o-mini); const maxDiff config.getnumber(maxDiffLength, 8000); if (!apiKey || !baseUrl) { throw new Error(请先在设置中配置 API Key 和接口地址); } const truncatedDiff diff.length maxDiff ? diff.slice(0, maxDiff) \n...(改动过大已截断) : diff; const controller new AbortController(); const timeout setTimeout(() controller.abort(), 30000); try { const response await fetch(${baseUrl}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model, messages: [ { role: system, content: SYSTEM_PROMPT }, { role: user, content: buildUserPrompt(truncatedDiff) } ], temperature: 0.3, max_tokens: 200 }), signal: controller.signal }); if (!response.ok) { throw new Error(接口返回 ${response.status}); } const data await response.json(); return data.choices[0].message.content; } finally { clearTimeout(timeout); } }超时控制用AbortController设 30 秒。这个时间足够大多数模型响应又不会让用户等太久。超时后抛出错误外层捕获并提示生成超时请检查网络或接口配置。实操心得API Key 的存储我建议用 VSCode 的SecretStorage而不是明文写在 settings 里。context.secrets.store(commitAi.apiKey, key)会把密钥加密存在系统钥匙串里比明文安全得多。settings 里只放非敏感的配置项。4.4 打包成 VSIX 离线包开发调试用 F5 启动扩展宿主窗口就行但要分发给团队用得打包成.vsix。先装打包工具npm install -g vscode/vsce然后在项目根目录运行vsce package。它会读package.json里的信息编译 TypeScript生成一个插件名-版本号.vsix文件。打包前有几个检查点。package.json里的publisher字段必须填否则打包会报错。engines.vscode要写清楚兼容的最低 VSCode 版本比如^1.80.0。如果项目里有.vscodeignore文件确认它排除了src、node_modules里不必要的文件能显著减小包体积。生成的.vsix文件团队成员在 VSCode 扩展面板右上角点...选从 VSIX 安装选中文件即可。装完重启一下编辑器命令就能用了。这个方式特别适合内网环境不需要每个人都能访问应用市场。打包命令作用vsce package生成 .vsix 文件vsce package --no-dependencies不打包依赖体积更小vsce ls列出将要打包的文件用于检查5. 常见问题与排查技巧实录5.1 生成结果不符合预期怎么办这是反馈最多的问题。表现有好几种生成的 type 用错了比如把重构标成 featscope 提取错误subject 太长或者太笼统。排查思路是先看 diff 质量再看 prompt。如果 diff 里全是格式调整缩进、换行模型可能判断成 style但用户期望的是 refactor。这种情况可以在 prompt 里加一句如果改动主要是逻辑重组而非功能增减使用 refactor。如果 diff 被截断了模型看不到全貌生成的概括就会偏这时候要么调大maxDiffLength要么分批提交。还有一种情况是模型过度解读。比如你只改了一个变量名它生成feat: 重构用户认证模块这就夸张了。解决办法是在系统提示词里强调描述要具体、克制不要夸大改动范围。我试过把 temperature 降到 0.1能减少这种发挥但措辞会变得生硬0.3 还是平衡点。5.2 接口调用失败的排查路径接口问题分几类我整理成一张速查表现象可能原因排查方法提示未配置 Keysettings 里 apiKey 为空检查设置项是否填对返回 401Key 无效或过期用 curl 直接测接口返回 404baseUrl 路径不对确认是否要带/v1后缀返回 429请求频率超限降低调用频率或换额度请求超时网络不通或接口慢检查网络调大超时时间返回内容为空模型名写错确认 model 字段拼写baseUrl的路径问题特别常见。有的服务要求https://api.example.com/v1有的直接https://api.example.com代码里我拼的是${baseUrl}/chat/completions所以用户填的 baseUrl 要包含到版本号那一层。这个在文档里写清楚能省很多沟通成本。注意调试接口问题时别把 API Key 打印到日志里。我见过有人为了排查把完整请求体 log 出来结果 Key 泄露在日志文件里。要打印就打印脱敏后的比如只显示前 4 位和后 4 位。5.3 暂存区为空与多仓库场景用户没暂存任何改动就点按钮这是高频误操作。我的处理是检测 diff 是否为空为空就提示暂存区没有改动请先 git add。提示用showWarningMessage比错误提示温和一些因为这不是错误只是操作顺序问题。多仓库场景也值得说。VSCode 的 Git API 里git.repositories是个数组如果用户开了多个仓库的工作区repositories[0]可能不是用户想要的那个。更稳妥的做法是判断数组长度只有一个就直接用多个就弹 QuickPick 让用户选。这个细节很多插件都忽略了导致多仓库用户用起来很困惑。let repo git.repositories[0]; if (git.repositories.length 1) { const picked await vscode.window.showQuickPick( git.repositories.map(r ({ label: path.basename(r.rootUri.fsPath), repo: r })), { placeHolder: 选择要生成提交信息的仓库 } ); if (!picked) return; repo picked.repo; }5.4 几个我踩过的坑第一个坑是 diff 里的中文乱码。Git 默认可能用core.quotepath把非 ASCII 路径转义成\344\275\240这种八进制模型读到完全看不懂。解决办法是在执行 git 命令时加-c core.quotepathfalse让路径原样输出。第二个坑是 CRLF 和 LF 混用。Windows 上 checkout 的文件可能是 CRLFdiff 里会显示成^M模型有时会把这个当成改动内容。我在采集后做了一次replace(/\r\n/g, \n)统一换行符问题就没了。第三个坑是扩展激活失败。有次打包后装上点命令没反应。排查发现是package.json里main字段指向的入口文件路径不对编译输出在out/extension.js但字段写的是./extension.js。这种低级错误在本地 F5 调试时不会暴露因为调试走的是源码只有打包后才显现。所以每次打包后我都会在干净的 VSCode 里装一遍实测。第四个坑是模型偶尔返回空内容。原因是max_tokens设太小模型刚开始输出就被截断了。我把max_tokens从 100 调到 200 后就没再出现。如果你发现返回内容不完整优先检查这个参数。6. 关于扩展性与团队协作的几点经验插件跑通之后我在团队里推了一波收集到不少有价值的反馈。有人希望能自定义提交模板比如他们团队要求type(scope): subject后面必须跟一个空行再写 body说明影响范围。这个需求很合理我加了一个commitAi.template配置项允许用户用占位符写模板比如${type}(${scope}): ${subject}\n\n${body}模型按模板填充。还有人问能不能支持多个模型切换。这个其实已经支持了因为 baseUrl 和 model 都是配置项用户想换服务商改这两个值就行。我建议团队统一配置把 settings 写进.vscode/settings.json提交到仓库这样新人拉下代码就自动配好了只需要各自填自己的 API Key。Key 用 SecretStorage 存不会进版本库。从维护角度看这个插件最大的价值不是省了那几秒钟打字时间而是让提交历史变得一致。以前团队里有人写中文、有人写英文、有人用 emoji、有人啥都不写现在至少格式统一了git log看起来清爽很多做 changelog 或者排查问题时效率明显提升。如果你也想动手做一个我的建议是先把最小闭环跑通——能读 diff、能调接口、能回填然后再逐步加配置项和错误处理。别一上来就追求大而全那样很容易卡在细节里出不来。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。