1. 为什么我要自己做一个提交信息生成插件
每次写完代码,打开源代码管理面板,看到那一排待提交的文件,然后要在输入框里憋出一句像样的提交信息——这件事我忍了很久了。fix bug、update、修改这种提交记录,我自己看着都脸红,但忙起来的时候真的没精力去想什么feat: 新增用户登录态校验逻辑。团队里 code review 的时候,翻 git log 找某个改动,满屏的update和fix,那感觉就像在一堆没贴标签的罐头里找一颗特定的豆子。
市面上不是没有类似工具,但要么是独立客户端,要么需要把代码推到某个平台才能用,要么配置起来一堆依赖。我就想:能不能在 VSCode 里装一个插件,点一下按钮,它读一下我暂存区的 diff,然后直接给我生成一条规范的提交信息?最好是离线的规则引擎加可选的 AI 增强,不强制联网,不强制注册账号,装完就能用。
这个项目就是干这个的。它是一个 VSCode 扩展,打包成.vsix离线包,双击安装,重启编辑器就能在源代码管理面板看到一个小图标。点它,它做三件事:读取当前暂存区的变更、分析变更类型和范围、生成一条符合 Conventional Commits 规范的提交信息并填入输入框。如果你配置了 OpenAI 的 API Key,它还能调用模型生成更自然、更贴合业务语义的描述;不配置也没关系,内置的规则引擎覆盖了常见的增删改场景,准确率在日常使用中够用。
适合谁看?如果你每天都在用 Git 做版本控制,又不想在提交信息上花太多心思,或者你是一个团队的技术负责人,想统一团队的提交规范但推不动人,这个插件的思路和实现细节都值得参考。下面我会从整体设计、核心实现、实操步骤到踩坑记录,完整拆一遍。
2. 整体设计与技术选型拆解
2.1 为什么是 VSCode 扩展而不是独立 CLI
一开始我考虑过写一个 Node.js 脚本,通过 git hook 在 commit 之前自动生成信息。但很快放弃了,原因有三个。
第一,git hook 的触发时机是prepare-commit-msg,这时候暂存区已经确定了,但用户往往还没想好要提交什么。如果生成的信息不符合预期,用户需要中断提交、修改、重新提交,体验很割裂。而在 VSCode 里,用户可以在提交之前就看到生成的信息,不满意直接改,改完再点提交,流程顺畅得多。
第二,VSCode 扩展能直接访问编辑器的 API,比如获取当前工作区的根路径、读取配置、显示通知、操作源代码管理面板的输入框。这些能力用 CLI 实现起来要绕很多弯,比如通过git config读取配置、通过标准输出传递信息,维护成本高。
第三,分发方便。.vsix文件发给团队成员,双击安装,不需要每个人去配环境变量、装 Node 依赖。对于不熟悉命令行的同事来说,这个门槛低得多。
2.2 规则引擎加 AI 增强的双层架构
核心设计上,我把生成逻辑分成了两层:规则引擎层和AI 增强层。
规则引擎层负责处理绝大多数常见场景。它的输入是git diff --cached的输出,输出是一条结构化的提交信息。具体来说,它会解析 diff 中的文件路径、变更类型(新增、修改、删除、重命名)、变更行数,然后根据一套预设的映射规则生成信息。比如检测到新增了.ts文件且内容包含export function,就归类为feat;检测到修改了测试文件,就归类为test;检测到只改了.md文件,就归类为docs。
AI 增强层是可选的。当用户在配置里填了 API Key 和模型名称后,插件会把 diff 内容截断到一定长度,加上一段精心设计的 prompt,发给模型,让模型生成一条更自然的提交信息。如果 API 调用失败或者超时,自动回退到规则引擎的结果,保证功能始终可用。
这个双层架构的好处是:离线可用、在线增强、失败降级。用户不会因为网络问题或者 API 配额用完就卡住。
2.3 为什么选择 Conventional Commits 作为输出规范
提交信息的格式有很多种,我最终选了 Conventional Commits。原因很简单:它有明确的类型前缀(feat、fix、docs、style、refactor、perf、test、chore),有可选的范围(scope),有描述主体,还支持 breaking change 标记。这套规范被大量工具链支持,比如自动生成 changelog、自动决定版本号、自动触发 CI 流程。
对于团队协作来说,统一的格式意味着 git log 可读性大幅提升,也意味着可以用工具自动分析提交历史。我在插件里内置了一个类型映射表,根据文件路径和变更内容自动推断类型,用户也可以在设置里覆盖这个映射。
2.4 技术栈与依赖选择
插件本身用 TypeScript 写,编译目标是 ES2020,运行在 VSCode 的扩展宿主进程里。依赖方面,我刻意保持精简:
simple-git:封装 git 命令调用,比直接child_process.exec更安全,能处理路径转义和错误捕获。openai:官方 Node SDK,用于调用兼容 OpenAI 接口的模型服务。@types/vscode:VSCode 扩展 API 的类型定义。
没有引入任何 UI 框架,所有交互都通过 VSCode 原生的window.showInputBox、window.showQuickPick、window.showInformationMessage实现。这样打包出来的.vsix体积很小,安装快,启动也不拖慢编辑器。
3. 核心细节解析与实操要点
3.1 暂存区 diff 的读取与解析
读取暂存区 diff 是整个流程的第一步。我用simple-git的diff方法,传入['--cached']参数,拿到的是标准 unified diff 格式的文本。这个文本包含了每个文件的变更块,每个块以@@开头,后面跟着行号范围和变更内容。
解析的时候,我主要提取三类信息:
- 文件路径:从
diff --git a/xxx b/xxx这一行提取,同时处理重命名的情况(rename from和rename to)。 - 变更类型:新增文件会有
new file mode,删除文件会有deleted file mode,重命名会有rename from/to,其余归为修改。 - 变更内容摘要:统计每个文件新增和删除了多少行,以及是否包含特定关键词(比如
test、docs、config)。
这里有个细节要注意:diff 文本可能非常大,尤其是首次提交或者大规模重构的时候。如果直接把整个 diff 发给 AI 模型,token 消耗会很高,而且可能超出上下文限制。我的做法是:只取每个文件的前 50 行变更内容,并且总长度超过 8000 字符时截断,优先保留新增行。实测下来,这个策略在保证生成质量的同时,把 token 消耗控制在了合理范围内。
注意:如果你的项目里有大文件(比如图片、二进制文件),
git diff --cached可能会输出乱码或者极长的文本。建议在插件设置里加一个文件类型过滤,把.png、.jpg、.zip这类扩展名排除掉。
3.2 规则引擎的类型推断逻辑
规则引擎的核心是一张映射表,我把它设计成了可配置的 JSON 结构。默认配置大致如下:
{ "typeRules": [ { "pattern": "\\.(test|spec)\\.(ts|js|tsx|jsx)$", "type": "test" }, { "pattern": "\\.(md|mdx|txt)$", "type": "docs" }, { "pattern": "\\.(css|scss|less)$", "type": "style" }, { "pattern": "package\\.json$", "type": "chore" }, { "pattern": "\\.(yml|yaml|json|toml)$", "type": "chore" } ], "keywordRules": [ { "keyword": "fix", "type": "fix" }, { "keyword": "bug", "type": "fix" }, { "keyword": "refactor", "type": "refactor" }, { "keyword": "optimize", "type": "perf" } ] }推断流程是:先按文件路径匹配typeRules,如果所有文件都匹配到同一个类型,就用这个类型;如果匹配到多个类型,取优先级最高的(feat>fix>refactor>perf>test>docs>style>chore)。如果路径没匹配上,再看变更内容里有没有keywordRules里的关键词。最后如果什么都没匹配到,默认用chore。
范围(scope)的推断稍微简单一些:取变更文件所在的最深层公共目录名。比如改了src/components/Button.tsx和src/components/Modal.tsx,scope 就是components。如果改了多个不同目录的文件,scope 留空。
3.3 AI 增强层的 prompt 设计
调用 AI 模型的时候,prompt 的质量直接决定了生成结果的好坏。我试过很多版本,最终稳定下来的 prompt 结构是这样的:
你是一个 Git 提交信息生成助手。请根据以下暂存区的变更内容,生成一条符合 Conventional Commits 规范的提交信息。 要求: 1. 格式为 type(scope): description 2. type 从 feat/fix/docs/style/refactor/perf/test/chore 中选择 3. description 用中文,不超过 50 个字,动词开头,说明做了什么 4. 如果变更涉及多个不相关的改动,用分号分隔 5. 只输出提交信息本身,不要任何解释 变更内容: { diff 摘要 }这个 prompt 的关键点在于:明确输出格式、限制长度、要求中文、禁止解释。早期版本我没有加“只输出提交信息本身”这句话,结果模型经常返回“好的,根据您的变更,我建议的提交信息是:...”这种废话,还得额外写代码去提取。
另外,我把temperature设成了 0.3,让输出更稳定。max_tokens设成 100,因为提交信息本身很短,不需要太多 token。
3.4 配置项的设计与默认值
插件暴露了以下配置项,用户可以在 VSCode 设置里搜索commitAi找到:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
commitAi.enableAI | boolean | false | 是否启用 AI 增强 |
commitAi.apiKey | string | "" | API Key,存储在 VSCode 的 SecretStorage 里 |
commitAi.baseUrl | string | "https://api.openai.com/v1" | API 基础地址,支持兼容接口 |
commitAi.model | string | "gpt-4o-mini" | 模型名称 |
commitAi.maxDiffLength | number | 8000 | diff 截断长度 |
commitAi.language | string | "zh-CN" | 生成信息的语言 |
commitAi.customTypeRules | array | [] | 自定义类型映射规则 |
API Key 我特意存在了SecretStorage里,而不是普通的workspaceConfiguration。因为普通配置会明文写在settings.json里,如果不小心把配置文件提交到仓库,Key 就泄露了。SecretStorage是 VSCode 提供的加密存储,只有扩展本身能读取。
提示:如果你用的是兼容 OpenAI 接口的第三方服务,只需要改
baseUrl和model两个配置就行。但要注意,不同服务对 prompt 的响应格式可能有差异,建议先用curl测试一下接口是否正常返回。
4. 实操过程与核心环节实现
4.1 从零搭建扩展项目骨架
先确保你本地有 Node.js 18 以上版本和 npm。然后安装 VSCode 扩展开发脚手架:
npm install -g yo generator-code yo code在交互式界面里选择New Extension (TypeScript),输入扩展名称commit-ai,其余选项保持默认。生成的项目结构里,核心文件是src/extension.ts,这是扩展的入口。
接下来安装依赖:
npm install simple-git openai npm install --save-dev @types/vscode然后在package.json里注册命令和配置项。命令的command字段填commitAi.generate,title填生成提交信息。配置项按照上一节的表格逐个填入contributes.configuration.properties。
4.2 注册命令与激活事件
在package.json的activationEvents里加上onCommand:commitAi.generate,这样用户第一次点击按钮时扩展才会激活,不会拖慢编辑器启动。
然后在extension.ts的activate函数里注册命令:
import * as vscode from 'vscode'; import { generateCommitMessage } from './generator'; export function activate(context: vscode.ExtensionContext) { const disposable = vscode.commands.registerCommand('commitAi.generate', async () => { const message = await generateCommitMessage(context); if (message) { const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports; const git = gitExtension?.getAPI(1); const repo = git?.repositories[0]; if (repo) { repo.inputBox.value = message; vscode.window.showInformationMessage('提交信息已生成'); } } }); context.subscriptions.push(disposable); }这里用到了 VSCode 内置的 Git 扩展 API。repo.inputBox.value就是源代码管理面板那个输入框的值,直接赋值就能填入生成的信息。
4.3 实现 diff 读取与规则引擎
新建src/generator.ts,核心逻辑如下:
import * as vscode from 'vscode'; import simpleGit from 'simple-git'; import { inferType, inferScope } from './rules'; import { callAI } from './ai'; export async function generateCommitMessage(context: vscode.ExtensionContext): Promise<string | undefined> { const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { vscode.window.showWarningMessage('请先打开一个 Git 仓库'); return; } const rootPath = workspaceFolders[0].uri.fsPath; const git = simpleGit(rootPath); const isRepo = await git.checkIsRepo(); if (!isRepo) { vscode.window.showWarningMessage('当前目录不是 Git 仓库'); return; } const diff = await git.diff(['--cached']); if (!diff.trim()) { vscode.window.showWarningMessage('暂存区没有变更,请先 git add'); return; } const config = vscode.workspace.getConfiguration('commitAi'); const enableAI = config.get<boolean>('enableAI', false); if (enableAI) { const apiKey = await context.secrets.get('commitAi.apiKey'); if (apiKey) { try { const aiMessage = await callAI(diff, apiKey, config); if (aiMessage) return aiMessage; } catch (err) { console.error('AI 生成失败,回退到规则引擎', err); } } } const type = inferType(diff); const scope = inferScope(diff); const description = buildDescription(diff); return scope ? `${type}(${scope}): ${description}` : `${type}: ${description}`; }buildDescription函数负责从 diff 里提取一个简短的中文描述。我的做法是:统计新增和删除的文件数,如果只有一个文件,就用文件名加动作;如果有多个文件,就用“更新多个文件”加主要变更类型。这个描述不算完美,但作为规则引擎的兜底够用了。
4.4 打包成 .vsix 离线包
开发调试完成后,安装vsce打包工具:
npm install -g @vscode/vsce在项目根目录执行:
vsce package如果提示缺少repository字段,在package.json里补上一个即可。打包成功后会生成commit-ai-0.0.1.vsix文件。把这个文件发给团队成员,他们在 VSCode 里按Ctrl+Shift+P打开命令面板,输入Install from VSIX,选择文件就能安装。
注意:打包之前记得把
package.json里的publisher字段填上,否则vsce会报错。这个字段可以随便填一个你的名字或团队名,不影响本地安装使用。
4.5 配置 API Key 与测试 AI 生成
安装完插件后,按Ctrl+,打开设置,搜索commitAi,把enableAI勾上。然后按Ctrl+Shift+P输入Commit AI: 设置 API Key,在弹出的输入框里粘贴你的 Key。这个命令是我额外注册的,专门用来往SecretStorage里写 Key。
配置完成后,随便改一个文件,git add之后点击源代码管理面板的生成按钮。如果一切正常,输入框里会出现一条类似feat(components): 新增按钮组件的加载状态的信息。
如果 AI 调用失败,控制台会输出错误日志。你可以按Ctrl+Shift+P输入Developer: Toggle Developer Tools打开开发者工具,在 Console 面板里看到具体的报错信息。
5. 常见问题与排查技巧实录
5.1 生成的信息不符合预期怎么办
这是最常见的问题。首先要区分是规则引擎的问题还是 AI 的问题。如果没开 AI,那问题出在类型映射规则上。你可以打开设置,找到commitAi.customTypeRules,添加自己的规则。比如你的项目里api目录下的文件都应该归为feat,就加一条{ "pattern": "api/", "type": "feat" }。
如果开了 AI 但生成的信息还是不对,大概率是 diff 截断导致的。模型只看到了部分变更,自然推断不准确。你可以把commitAi.maxDiffLength调大,比如改成 16000,但要注意 token 消耗会相应增加。
还有一种情况是模型本身的能力问题。gpt-4o-mini在简单场景下够用,但如果你的变更涉及复杂的业务逻辑,可能需要换成更强的模型。在设置里改commitAi.model即可。
5.2 API 调用超时或报错
API 调用失败的原因很多,我整理了一个排查表:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 提示 401 Unauthorized | API Key 错误或过期 | 重新设置 Key |
| 提示 429 Too Many Requests | 请求频率超限 | 降低使用频率或升级配额 |
| 提示 timeout | 网络不通或服务不可达 | 检查baseUrl是否正确 |
| 提示 model not found | 模型名称错误 | 确认服务商支持的模型列表 |
| 返回内容为空 | prompt 被过滤或模型拒绝 | 检查 diff 是否包含敏感内容 |
我遇到最多的是baseUrl配置错误。很多人复制地址的时候会多带一个/或者少写/v1,导致请求路径不对。建议直接用curl测试一下:
curl -X POST https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"test"}]}'如果这条命令能正常返回,说明配置没问题,问题出在插件代码里。
5.3 暂存区没有变更时的处理
用户可能忘记git add就直接点生成按钮。这时候git diff --cached返回空字符串,插件会提示“暂存区没有变更”。但有些用户不理解什么是暂存区,所以我后来把提示改成了“请先在源代码管理面板中暂存要提交的文件”。
另外,如果用户改了文件但没保存,VSCode 的 Git 面板可能显示有变更,但git diff --cached读不到内容。这种情况需要先保存文件再暂存。我在插件的 README 里专门写了这一点,但实际使用中还是有人踩坑。
5.4 多人协作时的配置同步
团队使用的时候,每个人的 API Key 不一样,不能共享。但类型映射规则、语言偏好这些是可以统一的。我的做法是在项目根目录放一个.vscode/settings.json,把commitAi.customTypeRules和commitAi.language写进去,提交到仓库。这样新成员拉取代码后,规则自动生效,只需要自己配 Key 就行。
提示:不要把 API Key 写进
.vscode/settings.json,那个文件是明文存储的。Key 只能通过SecretStorage或者环境变量传递。
5.5 扩展与其他 Git 工具的冲突
有些团队用husky加commitlint做提交信息校验。如果插件生成的信息不符合commitlint的规则,提交会被拦截。解决办法是确保插件的输出格式和commitlint的配置一致。比如commitlint要求subject不能为空且不超过 72 个字符,那就在 prompt 里明确加上这个限制。
还有一种情况是用户同时装了其他 Git 增强插件,比如 GitLens。这些插件可能会修改源代码管理面板的 UI,导致生成按钮的位置变化。但功能本身不冲突,因为我是通过命令注册的,不依赖 UI 位置。
6. 一些实操心得与后续扩展方向
这个插件我从有这个想法到跑通第一个可用版本,大概花了两个周末。最大的感受是:规则引擎的覆盖度比想象中重要。一开始我太依赖 AI 了,结果发现很多同事根本不配 Key,或者配了之后因为网络问题经常失败。后来我把规则引擎打磨了一遍,现在即使完全离线,生成的提交信息也能达到“能用”的水平。
另一个心得是关于 prompt 的。我试过让模型直接输出 JSON 格式,然后解析 JSON 拿字段,但模型经常在 JSON 外面包一层 markdown 代码块,解析起来很麻烦。后来改成让模型直接输出纯文本,反而更稳定。有时候简单的方案比复杂的方案更可靠。
后续我打算加两个功能:一是支持自定义模板,让用户决定输出格式,比如有些人喜欢[类型] 描述而不是类型: 描述;二是加一个提交历史分析,统计最近一段时间各类型的提交占比,帮团队发现是不是fix太多了,需要还技术债了。
如果你也想自己改这个插件,代码结构很清晰,generator.ts负责主流程,rules.ts负责规则引擎,ai.ts负责 API 调用。改起来不复杂,关键是理解 VSCode 扩展的激活机制和 Git 扩展 API 的用法。踩过几次坑之后,你会发现这套东西比想象中好上手。