AI 助力 Git 提交:智能生成 Commit 信息的 Node.js 工具

69 阅读1分钟

zshrc 配置 alias ccc='fnm exec --using=22 node ~/aicm/index.js'

package.json

{
  "name": "ccc",
  "version": "1.0.0",
  "main": "index.js",
  "author": "",
  "license": "ISC",
  "dependencies": {
    "chalk": "^5.4.1",
    "node-fetch": "^3.3.2",
    "openai": "^5.8.3"
  }
}

index.js

const { execSync } = require('child_process');
const { OpenAI } = require('openai')
const readline = require('readline');
const path = require('path');
const os = require('os');

async function main() {
    const chalk = (await import('chalk')).default;
    const branch = await execSync('git branch --show-current').toString();

    const client = new OpenAI({
        apiKey: apiKey,
        baseURL: baseURL,
    });
    console.log('node version:' + await execSync('node -v').toString().trim());

    const diffContent = await execSync(`git diff HEAD -- . ':!yarn.lock' ':!package-lock.json'`).toString()
    const messageIndex = process.argv.indexOf('-m')
    const userTip = messageIndex === -1 ? '' : process.argv[messageIndex + 1]
    const autoPush = process.argv.includes('-p')

    if (diffContent.trim() === '') {
        console.log(chalk.red('没有变更'));
        return;
    }

    const content =
        `生成提交信息

规则:
- 首先关注用户输入的提示信息(大概率用户不会输入提示信息)
- 根据下列提供的线索,生成提交信息
- 格式注意尽量与最近一次提交保持一致
- 生成的信息尽量保证简洁
- 仅需返回生成的 commit 内容

<section  title="用户输入的提示信息">
${userTip} 
</section>

<section  title="当前分支">
${branch}
</section>


<section  title="最近提交内容 HEAD">
${execSync('git show HEAD~0 --pretty=%B --no-patch').toString().trim()}
</section>

<section  title="最近提交内容 HEAD~1">
${execSync('git show HEAD~1 --pretty=%B --no-patch').toString().trim()}
</section>

<section  title="最近提交内容 HEAD~2">
${execSync('git show HEAD~2 --pretty=%B --no-patch').toString().trim()}
</section>

<section  title="变更内容">
${diffContent}
</section>
`
    // 当前提示信息接入日志文件
    console.log("查看日志 ~/aicm/log.txt\n");
    const logPath = path.join(os.homedir(), 'aicm', 'log.txt');
    const fs = require('fs');
    // 先清空文件(使用'w'标志会覆盖已有内容)
    fs.writeFileSync(logPath, '');
    const logFile = fs.createWriteStream(logPath, { flags: 'a' });
    logFile.write(content);
    logFile.write('\n\n');
    logFile.end();

    const completion = await client.chat.completions.create({
        messages: [{ role: "system", content }],
        model: "deepseek-v3-250324",
        stream: true,
    });

    let rs = ''
    // 逐字输出到终端
    for await (const part of completion) {
        const curr = part.choices[0].delta.content
        rs += curr
        process.stdout.write(chalk.green(curr));
    }
    process.stdout.write('\n\n');
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });
    rl.question(`按 Enter 提交${autoPush ? '并推送' : ''},按其他键退出: `, async (answer) => {
        if (answer === '') {
            try {
                await execSync(`git add . && git commit -m "${rs}"${autoPush ? '&& git push' : ''}`);
            } catch { }
        } else {
            console.log('退出...');
        }
        rl.close();
    });

}

main()