拒绝傻瓜式截断 Diff:聊聊我在 AI Commit 插件里做的 7 个技术微创新

昨晚熬夜写了个 VS Code 插件,彻底解放写 Commit Message 的双手

昨晚写代码写到凌晨,准备提交的时候,看着空荡荡的 commit message 输入框,突然不想动脑子了。

"这是 feat 还是 fix?scope 填 auth 还是 user?message 怎么概括才得体?" ------ 每次都要在大脑里过一遍这些 Conventional Commits 的规则,真的很累。

于是,从昨晚 11 点开始,到今天上午发布上线,花了不到 4 个小时,我vibe coding了一个极其顺手的 Commit Message 生成器。

写完复盘了一下,发现虽然功能简单,但为了"极致的体验",里面用了不少有意思的技术细节。

为什么还要造轮子

其实 VS Code 原生就支持 Copilot 生成 commit message,但我之前的 Copilot 服务因为账号问题用不了了(懂的都懂)。

没了 Copilot,我去市场搜了一圈替代品,发现最大的痛点是:不够智能,也不够灵活。

这就导致了两个问题:

  1. 没法适应团队规范:很多插件生成的格式是死的,但我们团队要求特定的 type 或 scope 格式,甚至要求用中文/日文写 commit。
  2. 配置极其繁琐:想用自己的 API Key,得填一堆 Provider ID、Endpoint,非常折腾。

我想要的是:

  1. 完全的 Prompt 自定义权限:不仅仅是改个前缀,而是能完全控制 AI 的系统提示词(System Prompt)。我想让它用中文写、用emoji、或者严格遵循 Angular 规范,都能通过改 Prompt 实现。
  2. 不想填一堆配置:给我一个 API Key,一个 Base URL,一个 Model 名字,这就够了。管你是 OpenAI、Claude 还是 Ollama 本地模型,只要兼容 OpenAI 格式,通通能用。
  3. 懂我的代码:不要傻傻地把几千行 diff 扔给 AI,它会晕;也不要粗暴地截断,它会瞎。
  4. 要有爽感 :点一下,文字必须像打字机一样一个个蹦出来(流式输出),而不是转圈转半天然后 一下甩我脸上。

基于这些需求,我 vibe coding 出了这个插件。

核心设计

1. 统一的 API 抽象

最初我设计了四个 Provider:OpenAI、Claude、Gemini、Custom。后来发现这是过度设计------现在大部分 LLM 服务都兼容 OpenAI 格式,没必要分开。

最终只留了三个配置项:

typescript 复制代码
interface ProviderConfig {
    apiKey: string;
    model: string;
    baseUrl: string;  // https://api.openai.com/v1 或 http://localhost:11434/v1
}

用 OpenAI?填 api.openai.com/v1。用 Claude 代理?换个 baseUrl。用本地 Ollama?localhost:11434/v1。一套代码全搞定。

2. 流式输出

等 AI 生成完再显示结果,体验很差。用户会以为插件卡死了。

解决方案是 SSE(Server-Sent Events)流式输出:

typescript 复制代码
// 请求时开启流式
body: JSON.stringify({
    model: this.config.model,
    messages: [{ role: 'user', content: prompt }],
    stream: true  // 关键
})

然后解析 SSE 格式的响应:

typescript 复制代码
async function readOpenAICompatibleStream(
    body: ReadableStream<Uint8Array>,
    onToken: (text: string) => void
): Promise<string> {
    const reader = body.getReader();
    const decoder = new TextDecoder('utf-8');
    let buffer = '';
    let result = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        
        // SSE 事件以双换行分隔
        let sepIndex;
        while ((sepIndex = buffer.indexOf('\n\n')) !== -1) {
            const event = buffer.slice(0, sepIndex);
            buffer = buffer.slice(sepIndex + 2);

            for (const line of event.split('\n')) {
                if (!line.startsWith('data:')) continue;
                const payload = line.slice(5).trim();
                if (payload === '[DONE]') return result;

                const json = JSON.parse(payload);
                const delta = json?.choices?.[0]?.delta?.content ?? '';
                if (delta) {
                    result += delta;
                    onToken(delta);  // 每个 token 实时回调
                }
            }
        }
    }
    return result;
}

每收到一个 token,就往 VS Code 的 commit 输入框里追加,用户能看到文字一个一个蹦出来。

3. 智能 type/scope 推断

Conventional Commits 格式是 type(scope): description。让 AI 自己猜 type 有时候不太准,比如改了测试文件,AI 可能还是写 feat

我加了一层启发式推断:

typescript 复制代码
function inferType(files: string[]): string {
    const isDocsFile = (f) => f.endsWith('.md') || f.startsWith('docs/');
    const isTestFile = (f) => f.includes('test') || /\.(test|spec)\.[jt]sx?$/.test(f);
    const isCIFile = (f) => f.startsWith('.github/workflows/');
    
    if (files.every(isDocsFile)) return 'docs';
    if (files.every(isTestFile)) return 'test';
    if (files.some(isCIFile)) return 'ci';
    // ...
    return 'feat';
}

把推断结果作为"建议"传给 AI,而不是强制限制。这样 AI 既有参考,又保留了灵活性:

markdown 复制代码
## Suggestions (optional)
- Suggested type: docs
- Suggested scope: readme

4. 智能 Diff 裁剪

大型重构可能有几千行 diff,直接喂给 AI 会超 token 限制。简单粗暴地截断前 N 个字符,可能会把函数签名截断一半,AI 看不懂。

我的做法是语义裁剪

typescript 复制代码
function summarizeDiffBlock(block: string, budget: number): string {
    const lines = block.split('\n');
    const header = [];    // 保留 diff 头部
    const important = []; // 保留关键行

    // 用正则识别关键行
    const signatureLike = /^[+-]?\s*(export\s+)?(function|class|interface)/;
    const commentLike = /^[+-]?\s*(\/\/|#|\/\*)/;
    
    for (const line of lines) {
        if (signatureLike.test(line) || commentLike.test(line)) {
            important.push(line);
        }
    }
    
    return [...header, ...important].join('\n');
}

优先保留:

  • 函数/类签名
  • 注释
  • import 语句
  • hunk 的前 6 行变更

这样即使原始 diff 有 5000 行,裁剪到 500 行后,AI 仍然能理解代码的意图。

5. 单行/多行模式

有人喜欢简洁的单行 commit,有人喜欢带 body 的详细版本。

我加了 outputStyle 配置:

typescript 复制代码
type OutputStyle = 'headerOnly' | 'headerAndBody';

不同模式下,prompt 模板会动态变化:

handlebars 复制代码
{{#if header_only}}
- Output ONLY ONE line (header only): no body, no footer
- Keep it concise (max 72 characters)
{{/if}}

{{#if allow_body}}
- Output a header line and optionally a short body
{{/if}}

headerOnly 模式还有个优化:检测到第一个换行符就立即中断流式输出,不浪费后面的 token:

typescript 复制代码
if (headerOnly) {
    const newlineIndex = text.search(/\r?\n/);
    if (newlineIndex !== -1) {
        abortController.abort();  // 立即停止
        return;
    }
}

6. 后处理清洗

AI 有时候会输出一些垃圾:

  • Commit message: feat: ...(带前缀)
  • feat: ...(带 markdown 代码块)
  • "feat: ..."(带引号)

我加了一层后处理:

typescript 复制代码
function normalizeCommitMessage(raw: string, options: { headerOnly: boolean }): string {
    let text = raw;
    
    // 去掉 markdown 代码块
    text = text.replace(/```[\s\S]*?```/g, (block) => {
        const lines = block.split('\n');
        return lines.slice(1, -1).join('\n');  // 只保留内容
    });
    
    // 去掉常见前缀
    text = text.replace(/^\s*(commit message|message)\s*:\s*/i, '');
    
    // 去掉首尾引号
    text = text.replace(/^["'`]+|["'`]+$/g, '');
    
    if (options.headerOnly) {
        return text.split('\n').find(l => l.trim()) ?? '';
    }
    return text;
}

7. 暂存区智能感知

很多时候写完代码忘了 git add 就直接点生成,大部分插件会报错 "No staged changes"。

我觉得工具应该更聪明一点:

  1. 只有暂存区代码:直接生成(标准流程)。
  2. 只有工作区代码(未暂存) :直接生成(省去 git add 步骤)。
  3. 都有:弹窗让用户选。
typescript 复制代码
const hasStaged = await git.hasStagedChanges();
const hasUnstaged = await git.hasUnstagedChanges();

if (!hasStaged && !hasUnstaged) {
    return vscode.window.showWarningMessage('No changes found');
}

if (hasStaged && hasUnstaged) {
    // 弹窗让用户选
    const picked = await vscode.window.showWarningMessage(
        'Detected both staged and unstaged changes',
        'Use Staged',
        'Use Unstaged'
    );
    // ...
}

这样在这个微小的交互上,又能少点一次鼠标。

打包发布

VS Code 插件用 vsce 打包:

bash 复制代码
npx vsce package

为了减小包体积,我用了 esbuild 打包,把所有 TypeScript 代码编译成一个 extension.js

javascript 复制代码
// esbuild.mjs
await esbuild.build({
    entryPoints: ['src/extension.ts'],
    bundle: true,
    outfile: 'dist/extension.js',
    platform: 'node',
    external: ['vscode'],
    minify: production
});

最终 .vsix 包只有 435 KB,8 个文件。

总结

这个插件的核心思路就是:在对的地方做对的事

  • API 层:统一抽象,不过度设计
  • 交互层:流式输出,即时反馈
  • 推理层:启发式辅助,而非强制替代
  • 数据层:语义裁剪,而非暴力截断
  • 输出层:后处理清洗,容错 AI 的奇怪输出

代码不多,但每个模块都解决了一个实际问题。


项目已开源:vscode-ai-commit

VS Code Marketplace 搜索 "AI Commit" 可以直接安装。

如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:

Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):

qwen/gemini/claude - cli 原理学习网站

  • coding-cli-guide学习网站)- 学习 qwen-cli 时整理的笔记,40+ 交互式动画演示 AI CLI 内部机制 全栈项目(适合学习现代技术栈):

  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑

  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB

相关推荐
李剑一3 小时前
uni-app实现leaflet地图图标旋转
前端·trae
嗡嗡嗡qwq3 小时前
Claude Code体验-ai高级工程师
ai编程·claude code
闲云一鹤4 小时前
2026 最新 ComfyUI 教程 - 本地部署 AI 生图模型 - Z-Image-Turbo
前端·人工智能·ai编程
Captaincc5 小时前
Vibe Coding 进阶:非技术人员的生存手册
程序员·vibecoding
狼爷6 小时前
一文看懂 AI 世界里的新黑话Skills、MCP、Projects、Prompts
人工智能·openai·ai编程
该用户已不存在7 小时前
Symfony AI v0.2.0 正式发布:功能解读与实战指南
php·ai编程·symfony
黄林晴8 小时前
Anthropic 发布 Cowork:让 AI 成为你的「虚拟同事」
openai·ai编程·vibecoding
AlienZHOU9 小时前
MCP 是最大骗局?Skills 才是救星?
agent·mcp·vibecoding
Glink10 小时前
从零开始编写自己的AI账单Agent
前端·agent·ai编程