goodiffer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Goodiffer
2
+
3
+ AI-powered git diff analyzer for code review - 基于 AI 的 Git Diff 智能分析工具
4
+
5
+ [![npm version](https://badge.fury.io/js/goodiffer.svg)](https://www.npmjs.com/package/goodiffer)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - 🤖 支持 Claude (Anthropic) 和 GPT (OpenAI) 模型
11
+ - 🔍 自动分析 git diff,识别潜在问题
12
+ - 📊 生成结构化的代码审查报告
13
+ - 🔗 检测代码关联性风险
14
+ - 📋 生成可复制的修复提示词,方便在 Claude Code / Codex 中使用
15
+ - 🌐 支持第三方 API 代理
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install -g goodiffer
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ # 1. 初始化配置
27
+ goodiffer init
28
+
29
+ # 2. 分析最近一次 commit
30
+ goodiffer
31
+
32
+ # 3. 查看帮助
33
+ goodiffer --help
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### 初始化配置
39
+
40
+ ```bash
41
+ goodiffer init
42
+ ```
43
+
44
+ 交互式配置:
45
+ - 选择 API Host (Anthropic/OpenAI/PackyAPI/自定义)
46
+ - 输入 API Key
47
+ - 选择模型 (claude-sonnet-4-5/gpt-4o/自定义)
48
+
49
+ ### 分析命令
50
+
51
+ ```bash
52
+ # 分析最近一次 commit (默认)
53
+ goodiffer
54
+
55
+ # 分析暂存区
56
+ goodiffer -s
57
+ goodiffer --staged
58
+
59
+ # 分析指定 commit
60
+ goodiffer -c <commit-sha>
61
+ goodiffer --commit <commit-sha>
62
+
63
+ # 分析 commit 范围
64
+ goodiffer --from <start-sha> --to <end-sha>
65
+ ```
66
+
67
+ ### 配置管理
68
+
69
+ ```bash
70
+ # 查看当前配置
71
+ goodiffer config list
72
+
73
+ # 设置配置项
74
+ goodiffer config set apiHost https://api.anthropic.com
75
+ goodiffer config set model claude-sonnet-4-5-20250929
76
+
77
+ # 清除配置
78
+ goodiffer config clear
79
+ ```
80
+
81
+ ## Output Example
82
+
83
+ ```
84
+ ╭──────────────────────────────────────────────────────────╮
85
+ │ Goodiffer Analysis Report │
86
+ ╰──────────────────────────────────────────────────────────╯
87
+
88
+ 📝 Commit: feat: add user authentication
89
+
90
+ 📊 Summary: 添加用户认证功能,包含登录表单和 API 调用
91
+
92
+ 🎯 Commit 匹配: ✓ 代码修改符合 commit 描述
93
+
94
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95
+
96
+ 🔴 ERRORS (1)
97
+
98
+ [E001] src/auth/login.js:45-52
99
+ 问题: 未处理 API 调用失败的情况
100
+
101
+ 📋 修复提示词 (复制到 cc/codex):
102
+ ┌────────────────────────────────────────────────────────┐
103
+ │ 在 src/auth/login.js 第45行添加 try-catch 处理异常 │
104
+ └────────────────────────────────────────────────────────┘
105
+
106
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
107
+
108
+ 📈 统计: 1 errors 0 warnings 0 info 0 risks
109
+ ```
110
+
111
+ ## Supported API Providers
112
+
113
+ | Provider | API Host | Models |
114
+ |----------|----------|--------|
115
+ | Anthropic | https://api.anthropic.com | claude-sonnet-4-5, claude-3-opus |
116
+ | OpenAI | https://api.openai.com | gpt-4o, gpt-4-turbo |
117
+ | PackyAPI | https://www.packyapi.com | claude-*, gpt-* |
118
+ | Custom | 自定义 URL | 任意模型 |
119
+
120
+ ## Configuration
121
+
122
+ 配置文件存储在 `~/.config/goodiffer-nodejs/config.json`
123
+
124
+ 可配置项:
125
+ - `apiHost` - API 服务地址
126
+ - `apiKey` - API 密钥
127
+ - `model` - 模型名称
128
+ - `provider` - 提供商 (claude/openai/custom)
129
+
130
+ ## Requirements
131
+
132
+ - Node.js >= 18.0.0
133
+ - Git
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from '../src/index.js';
4
+
5
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "goodiffer",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered git diff analyzer for code review - 智能代码审查工具",
5
+ "type": "module",
6
+ "bin": {
7
+ "goodiffer": "./bin/goodiffer.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "start": "node bin/goodiffer.js"
16
+ },
17
+ "keywords": [
18
+ "git",
19
+ "diff",
20
+ "code-review",
21
+ "ai",
22
+ "cli",
23
+ "claude",
24
+ "openai",
25
+ "gpt",
26
+ "anthropic",
27
+ "code-analysis",
28
+ "developer-tools"
29
+ ],
30
+ "author": "kris",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/kris/goodiffer"
35
+ },
36
+ "homepage": "https://github.com/kris/goodiffer#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/kris/goodiffer/issues"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "dependencies": {
44
+ "chalk": "^5.3.0",
45
+ "commander": "^12.1.0",
46
+ "conf": "^12.0.0",
47
+ "inquirer": "^9.2.15",
48
+ "openai": "^4.70.0",
49
+ "ora": "^8.0.1",
50
+ "simple-git": "^3.22.0"
51
+ }
52
+ }
@@ -0,0 +1,137 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { getConfig, isConfigured } from '../utils/config-store.js';
4
+ import logger from '../utils/logger.js';
5
+ import { isGitRepo, getLastCommitInfo, getCommitInfo, getDiff, getChangedFiles } from '../services/git.js';
6
+ import { AIClient } from '../services/ai-client.js';
7
+ import { buildReviewPrompt } from '../prompts/review-prompt.js';
8
+ import { parseAIResponse, printReport } from '../services/reporter.js';
9
+
10
+ export async function analyzeCommand(options = {}) {
11
+ // 检查是否在 git 仓库中
12
+ if (!await isGitRepo()) {
13
+ logger.error('当前目录不是 git 仓库');
14
+ process.exit(1);
15
+ }
16
+
17
+ // 检查配置
18
+ if (!isConfigured()) {
19
+ logger.error('尚未配置 API。请先运行: goodiffer init');
20
+ process.exit(1);
21
+ }
22
+
23
+ const config = getConfig();
24
+
25
+ // 获取 commit 信息
26
+ let commitInfo;
27
+ if (options.commit) {
28
+ commitInfo = await getCommitInfo(options.commit);
29
+ } else if (options.staged) {
30
+ commitInfo = {
31
+ sha: 'staged',
32
+ message: '(暂存区变更)',
33
+ author: '',
34
+ date: new Date().toISOString()
35
+ };
36
+ } else {
37
+ commitInfo = await getLastCommitInfo();
38
+ }
39
+
40
+ if (!commitInfo && !options.staged) {
41
+ logger.error('无法获取 commit 信息');
42
+ process.exit(1);
43
+ }
44
+
45
+ // 获取 diff
46
+ const spinner = ora('获取代码变更...').start();
47
+
48
+ const diff = await getDiff({
49
+ staged: options.staged,
50
+ commit: options.commit,
51
+ from: options.from,
52
+ to: options.to
53
+ });
54
+
55
+ if (!diff || diff.trim().length === 0) {
56
+ spinner.fail('没有找到代码变更');
57
+ process.exit(0);
58
+ }
59
+
60
+ const changedFiles = await getChangedFiles({
61
+ staged: options.staged,
62
+ commit: options.commit,
63
+ from: options.from,
64
+ to: options.to
65
+ });
66
+
67
+ spinner.succeed(`找到 ${changedFiles.length} 个文件变更`);
68
+
69
+ // 检查 diff 大小
70
+ const diffLines = diff.split('\n').length;
71
+ if (diffLines > 2000) {
72
+ logger.warn(`diff 较大 (${diffLines} 行),分析可能需要较长时间`);
73
+ }
74
+
75
+ // 构建提示词
76
+ const prompt = buildReviewPrompt(commitInfo.message, diff, changedFiles);
77
+
78
+ // 创建 AI 客户端
79
+ const aiClient = new AIClient(config);
80
+
81
+ // 流式分析
82
+ console.log(chalk.cyan('\n🤖 AI 分析中...\n'));
83
+ console.log(chalk.gray('─'.repeat(60)));
84
+
85
+ let fullResponse = '';
86
+ let isShowingStream = true;
87
+
88
+ try {
89
+ fullResponse = await aiClient.analyzeStream(prompt, (chunk) => {
90
+ if (isShowingStream) {
91
+ process.stdout.write(chalk.gray(chunk));
92
+ }
93
+ });
94
+
95
+ console.log(chalk.gray('\n' + '─'.repeat(60)));
96
+
97
+ // 解析响应
98
+ const report = parseAIResponse(fullResponse);
99
+
100
+ if (report) {
101
+ // 打印格式化报告
102
+ printReport(commitInfo, report);
103
+ } else {
104
+ logger.error('无法解析 AI 响应,请查看上方原始输出');
105
+ }
106
+
107
+ } catch (error) {
108
+ console.log('');
109
+ const errMsg = error.message || String(error);
110
+
111
+ if (errMsg.includes('401') || errMsg.includes('API key') || errMsg.includes('Unauthorized')) {
112
+ logger.error('API Key 无效或已过期。请运行 goodiffer init 重新配置');
113
+ } else if (errMsg.includes('403') || errMsg.includes('blocked') || errMsg.includes('Forbidden')) {
114
+ logger.error('API 请求被拒绝 (403)');
115
+ logger.info('可能原因:');
116
+ logger.info(' 1. API Key 无效或无权限');
117
+ logger.info(' 2. 模型名称不正确 (第三方 API 可能使用不同名称)');
118
+ logger.info(' 3. 账户余额不足或已过期');
119
+ logger.info(`当前配置: apiHost=${config.apiHost}, model=${config.model}`);
120
+ logger.info('请运行 goodiffer init 检查配置');
121
+ } else if (errMsg.includes('404')) {
122
+ logger.error('API 端点不存在 (404)');
123
+ logger.info(`当前端点: ${config.apiHost}/v1/chat/completions`);
124
+ logger.info('请检查 API Host 是否正确');
125
+ } else if (errMsg.includes('rate limit') || errMsg.includes('429')) {
126
+ logger.error('API 请求频率超限,请稍后重试');
127
+ } else if (errMsg.includes('ENOTFOUND') || errMsg.includes('ECONNREFUSED')) {
128
+ logger.error('无法连接到 API 服务器');
129
+ logger.info(`当前 API Host: ${config.apiHost}`);
130
+ } else {
131
+ logger.error(`分析失败: ${errMsg}`);
132
+ }
133
+ process.exit(1);
134
+ }
135
+ }
136
+
137
+ export default analyzeCommand;
@@ -0,0 +1,85 @@
1
+ import { getConfig, setConfig, clearConfig } from '../utils/config-store.js';
2
+ import logger from '../utils/logger.js';
3
+
4
+ export function configCommand(action, key, value) {
5
+ switch (action) {
6
+ case 'list':
7
+ listConfig();
8
+ break;
9
+ case 'get':
10
+ getConfigValue(key);
11
+ break;
12
+ case 'set':
13
+ setConfigValue(key, value);
14
+ break;
15
+ case 'clear':
16
+ clearAllConfig();
17
+ break;
18
+ default:
19
+ logger.error(`未知操作: ${action}`);
20
+ logger.info('可用操作: list, get, set, clear');
21
+ }
22
+ }
23
+
24
+ function listConfig() {
25
+ const config = getConfig();
26
+ logger.title('当前配置');
27
+
28
+ console.log(` apiHost: ${config.apiHost || '(未设置)'}`);
29
+ console.log(` provider: ${config.provider || '(未设置)'}`);
30
+ console.log(` model: ${config.model || '(未设置)'}`);
31
+ console.log(` apiKey: ${config.apiKey ? '*'.repeat(8) + '...' + config.apiKey.slice(-4) : '(未设置)'}`);
32
+
33
+ // 显示实际 API 端点
34
+ if (config.apiHost) {
35
+ console.log('');
36
+ // 根据模型名称判断端点
37
+ if (config.model && config.model.toLowerCase().startsWith('claude')) {
38
+ console.log(` 端点: ${config.apiHost}/v1/messages`);
39
+ } else {
40
+ console.log(` 端点: ${config.apiHost}/v1/chat/completions`);
41
+ }
42
+ }
43
+ }
44
+
45
+ function getConfigValue(key) {
46
+ if (!key) {
47
+ logger.error('请指定配置项名称');
48
+ return;
49
+ }
50
+ const config = getConfig();
51
+ if (key === 'apiKey' && config[key]) {
52
+ console.log('*'.repeat(8) + '...' + config[key].slice(-4));
53
+ } else {
54
+ console.log(config[key] || '(未设置)');
55
+ }
56
+ }
57
+
58
+ function setConfigValue(key, value) {
59
+ if (!key || value === undefined) {
60
+ logger.error('用法: goodiffer config set <key> <value>');
61
+ return;
62
+ }
63
+
64
+ const validKeys = ['provider', 'apiHost', 'apiKey', 'model'];
65
+ if (!validKeys.includes(key)) {
66
+ logger.error(`无效的配置项: ${key}`);
67
+ logger.info(`可用配置项: ${validKeys.join(', ')}`);
68
+ return;
69
+ }
70
+
71
+ if (key === 'provider' && !['claude', 'openai', 'custom'].includes(value)) {
72
+ logger.error('provider 必须是 claude、openai 或 custom');
73
+ return;
74
+ }
75
+
76
+ setConfig(key, value);
77
+ logger.success(`已设置 ${key}`);
78
+ }
79
+
80
+ function clearAllConfig() {
81
+ clearConfig();
82
+ logger.success('配置已清除');
83
+ }
84
+
85
+ export default configCommand;
@@ -0,0 +1,137 @@
1
+ import inquirer from 'inquirer';
2
+ import { setAllConfig, getConfig } from '../utils/config-store.js';
3
+ import logger from '../utils/logger.js';
4
+
5
+ // 预设的模型列表
6
+ const MODEL_CHOICES = [
7
+ { name: 'claude-sonnet-4-5 (Claude)', value: 'claude-sonnet-4-5' },
8
+ { name: 'claude-sonnet-4-20250514 (Claude)', value: 'claude-sonnet-4-20250514' },
9
+ { name: 'gpt-5.1-high (OpenAI)', value: 'gpt-5.1-high' },
10
+ { name: 'gpt-4o (OpenAI)', value: 'gpt-4o' },
11
+ { name: 'gpt-4-turbo (OpenAI)', value: 'gpt-4-turbo' },
12
+ { name: 'deepseek-chat (DeepSeek)', value: 'deepseek-chat' },
13
+ { name: '自定义模型...', value: '__custom__' }
14
+ ];
15
+
16
+ // 预设的 API Host 列表
17
+ const API_HOST_CHOICES = [
18
+ { name: 'Anthropic 官方 (api.anthropic.com)', value: 'https://api.anthropic.com' },
19
+ { name: 'OpenAI 官方 (api.openai.com)', value: 'https://api.openai.com' },
20
+ { name: 'PackyAPI (packyapi.com)', value: 'https://www.packyapi.com' },
21
+ { name: '自定义 API Host...', value: '__custom__' }
22
+ ];
23
+
24
+ export async function initCommand() {
25
+ logger.title('Goodiffer 初始化配置');
26
+
27
+ const currentConfig = getConfig();
28
+
29
+ const answers = await inquirer.prompt([
30
+ {
31
+ type: 'list',
32
+ name: 'apiHostChoice',
33
+ message: '选择 API Host:',
34
+ choices: API_HOST_CHOICES,
35
+ default: () => {
36
+ if (currentConfig.apiHost) {
37
+ const found = API_HOST_CHOICES.find(c => c.value === currentConfig.apiHost);
38
+ return found ? found.value : '__custom__';
39
+ }
40
+ return 'https://api.anthropic.com';
41
+ }
42
+ },
43
+ {
44
+ type: 'input',
45
+ name: 'customApiHost',
46
+ message: '输入自定义 API Host (例如 https://api.example.com):',
47
+ when: (answers) => answers.apiHostChoice === '__custom__',
48
+ default: currentConfig.apiHost || '',
49
+ validate: (input) => {
50
+ if (!input || !input.startsWith('http')) {
51
+ return '请输入有效的 URL (以 http:// 或 https:// 开头)';
52
+ }
53
+ return true;
54
+ }
55
+ },
56
+ {
57
+ type: 'password',
58
+ name: 'apiKey',
59
+ message: 'API Key:',
60
+ mask: '*',
61
+ validate: (input) => {
62
+ if (!input || input.length < 10) {
63
+ return '请输入有效的 API Key';
64
+ }
65
+ return true;
66
+ }
67
+ },
68
+ {
69
+ type: 'list',
70
+ name: 'modelChoice',
71
+ message: '选择模型:',
72
+ choices: MODEL_CHOICES,
73
+ default: () => {
74
+ if (currentConfig.model) {
75
+ const found = MODEL_CHOICES.find(c => c.value === currentConfig.model);
76
+ return found ? found.value : '__custom__';
77
+ }
78
+ return 'claude-sonnet-4-5';
79
+ }
80
+ },
81
+ {
82
+ type: 'input',
83
+ name: 'customModel',
84
+ message: '输入自定义模型名称:',
85
+ when: (answers) => answers.modelChoice === '__custom__',
86
+ default: currentConfig.model || '',
87
+ validate: (input) => {
88
+ if (!input || input.length < 1) {
89
+ return '请输入模型名称';
90
+ }
91
+ return true;
92
+ }
93
+ }
94
+ ]);
95
+
96
+ // 处理选择结果
97
+ const apiHost = answers.apiHostChoice === '__custom__'
98
+ ? answers.customApiHost
99
+ : answers.apiHostChoice;
100
+
101
+ const model = answers.modelChoice === '__custom__'
102
+ ? answers.customModel
103
+ : answers.modelChoice;
104
+
105
+ // 根据 API Host 和 Model 自动判断 provider
106
+ let provider = 'custom';
107
+ if (apiHost.includes('anthropic.com') || model.startsWith('claude')) {
108
+ provider = 'claude';
109
+ } else if (apiHost.includes('openai.com') || model.startsWith('gpt')) {
110
+ provider = 'openai';
111
+ }
112
+
113
+ const configToSave = {
114
+ provider,
115
+ apiHost: apiHost.replace(/\/+$/, ''), // 移除末尾斜杠
116
+ apiKey: answers.apiKey,
117
+ model
118
+ };
119
+
120
+ setAllConfig(configToSave);
121
+
122
+ logger.success('配置已保存!');
123
+ logger.info(`API Host: ${configToSave.apiHost}`);
124
+ logger.info(`Provider: ${configToSave.provider}`);
125
+ logger.info(`模型: ${configToSave.model}`);
126
+ logger.info(`API Key: ${'*'.repeat(8)}...${answers.apiKey.slice(-4)}`);
127
+
128
+ // 显示实际 API 端点
129
+ console.log('');
130
+ if (provider === 'claude' && apiHost.includes('anthropic.com')) {
131
+ logger.info(`API 端点: ${configToSave.apiHost}/v1/messages`);
132
+ } else {
133
+ logger.info(`API 端点: ${configToSave.apiHost}/v1/chat/completions`);
134
+ }
135
+ }
136
+
137
+ export default initCommand;
package/src/index.js ADDED
@@ -0,0 +1,35 @@
1
+ import { Command } from 'commander';
2
+ import { initCommand } from './commands/init.js';
3
+ import { configCommand } from './commands/config.js';
4
+ import { analyzeCommand } from './commands/analyze.js';
5
+
6
+ export const program = new Command();
7
+
8
+ program
9
+ .name('goodiffer')
10
+ .description('AI-powered git diff analyzer for code review')
11
+ .version('1.0.0');
12
+
13
+ // 初始化命令
14
+ program
15
+ .command('init')
16
+ .description('初始化配置 (设置 AI 提供商和 API Key)')
17
+ .action(initCommand);
18
+
19
+ // 配置管理命令
20
+ program
21
+ .command('config <action> [key] [value]')
22
+ .description('配置管理 (list/get/set/clear)')
23
+ .action(configCommand);
24
+
25
+ // 分析命令
26
+ program
27
+ .command('analyze', { isDefault: true })
28
+ .description('分析 git diff (默认命令)')
29
+ .option('-s, --staged', '分析暂存区的变更')
30
+ .option('-c, --commit <sha>', '分析指定 commit')
31
+ .option('--from <sha>', '分析 commit 范围 (起始)')
32
+ .option('--to <sha>', '分析 commit 范围 (结束)')
33
+ .action(analyzeCommand);
34
+
35
+ export default program;
@@ -0,0 +1,78 @@
1
+ export function buildReviewPrompt(commitMessage, diff, changedFiles) {
2
+ return `你是一个专业的 Code Reviewer,专注于代码质量和潜在问题分析。
3
+
4
+ ## 任务
5
+
6
+ 1. **理解 Commit 意图**: 首先仔细阅读并理解以下 commit message,它描述了这次修改的目的和内容。
7
+ 2. **分析代码变更**: 检查 diff 中的代码修改是否准确实现了 commit 描述的意图。
8
+ 3. **识别潜在问题**: 检查代码中可能存在的问题,包括:
9
+ - 编译/运行时错误风险
10
+ - 逻辑错误或边界情况未处理
11
+ - 类型错误或空值处理问题
12
+ - 关联性 bug (修改可能影响其他模块或功能)
13
+
14
+ ## Commit Message
15
+ \`\`\`
16
+ ${commitMessage}
17
+ \`\`\`
18
+
19
+ ## 变更文件列表
20
+ ${changedFiles.map(f => `- ${f}`).join('\n')}
21
+
22
+ ## 代码 Diff
23
+ \`\`\`diff
24
+ ${diff}
25
+ \`\`\`
26
+
27
+ ## 输出要求
28
+
29
+ 请以 JSON 格式输出分析结果,格式如下:
30
+
31
+ \`\`\`json
32
+ {
33
+ "summary": "简要概述这次变更的内容和目的 (1-2句话)",
34
+ "commitAlignment": {
35
+ "aligned": true/false,
36
+ "comment": "代码修改是否符合 commit 描述的意图的说明"
37
+ },
38
+ "issues": [
39
+ {
40
+ "level": "error|warning|info",
41
+ "type": "compile|runtime|logic|security|association",
42
+ "file": "文件路径",
43
+ "line": "行号或行号范围 (如 45 或 45-52)",
44
+ "code": "问题代码片段",
45
+ "description": "问题的详细描述",
46
+ "suggestion": "修复建议",
47
+ "fixPrompt": "可以直接复制到 Claude Code 或 Codex 中的修复提示词"
48
+ }
49
+ ],
50
+ "associationRisks": [
51
+ {
52
+ "changedFile": "被修改的文件路径",
53
+ "relatedFiles": ["可能受影响的文件1", "可能受影响的文件2"],
54
+ "risk": "潜在的关联风险描述",
55
+ "checkPrompt": "用于检查关联问题的提示词"
56
+ }
57
+ ],
58
+ "suggestions": [
59
+ "其他改进建议 (可选)"
60
+ ]
61
+ }
62
+ \`\`\`
63
+
64
+ ## 问题级别说明
65
+ - **error**: 会导致编译失败、运行时崩溃或严重 bug 的问题
66
+ - **warning**: 可能导致问题或不符合最佳实践的代码
67
+ - **info**: 建议性的改进,不影响功能
68
+
69
+ ## 注意事项
70
+ 1. 只报告实际存在的问题,不要过度警告
71
+ 2. fixPrompt 应该简洁明确,方便用户直接复制使用
72
+ 3. 关联风险只报告真正可能受影响的情况
73
+ 4. 如果代码没有问题,issues 和 associationRisks 可以为空数组
74
+
75
+ 请仅输出 JSON,不要包含其他解释文字。`;
76
+ }
77
+
78
+ export default buildReviewPrompt;
@@ -0,0 +1,184 @@
1
+ import OpenAI from 'openai';
2
+
3
+ export class AIClient {
4
+ constructor(config) {
5
+ this.provider = config.provider;
6
+ this.apiKey = config.apiKey;
7
+ this.apiHost = config.apiHost;
8
+ this.model = config.model;
9
+
10
+ // 根据模型名称判断使用哪种 API 格式
11
+ this.useAnthropicFormat = this.model && this.model.toLowerCase().startsWith('claude');
12
+
13
+ if (!this.useAnthropicFormat) {
14
+ // OpenAI 兼容格式 API
15
+ const baseUrl = this.buildOpenAIBaseUrl();
16
+ this.client = new OpenAI({
17
+ apiKey: this.apiKey,
18
+ baseURL: baseUrl
19
+ });
20
+ }
21
+ }
22
+
23
+ buildOpenAIBaseUrl() {
24
+ if (!this.apiHost) {
25
+ return 'https://api.openai.com/v1';
26
+ }
27
+
28
+ let host = this.apiHost.replace(/\/+$/, '');
29
+
30
+ if (!host.endsWith('/v1')) {
31
+ host = `${host}/v1`;
32
+ }
33
+
34
+ return host;
35
+ }
36
+
37
+ async analyzeStream(prompt, onChunk) {
38
+ if (this.useAnthropicFormat) {
39
+ return this.analyzeWithClaudeFetch(prompt, onChunk);
40
+ } else {
41
+ return this.analyzeWithOpenAI(prompt, onChunk);
42
+ }
43
+ }
44
+
45
+ // 使用原生 fetch 调用 Claude API (绕过 SDK 的 Cloudflare 问题)
46
+ async analyzeWithClaudeFetch(prompt, onChunk) {
47
+ const baseUrl = (this.apiHost || 'https://api.anthropic.com').replace(/\/+$/, '');
48
+ const url = `${baseUrl}/v1/messages`;
49
+
50
+ const response = await fetch(url, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ 'x-api-key': this.apiKey,
55
+ 'anthropic-version': '2023-06-01'
56
+ },
57
+ body: JSON.stringify({
58
+ model: this.model || 'claude-sonnet-4-20250514',
59
+ max_tokens: 8192,
60
+ stream: true,
61
+ messages: [
62
+ {
63
+ role: 'user',
64
+ content: prompt
65
+ }
66
+ ]
67
+ })
68
+ });
69
+
70
+ if (!response.ok) {
71
+ const errorText = await response.text();
72
+ throw new Error(`${response.status} ${errorText}`);
73
+ }
74
+
75
+ let fullContent = '';
76
+ const reader = response.body.getReader();
77
+ const decoder = new TextDecoder();
78
+
79
+ while (true) {
80
+ const { done, value } = await reader.read();
81
+ if (done) break;
82
+
83
+ const chunk = decoder.decode(value, { stream: true });
84
+ const lines = chunk.split('\n');
85
+
86
+ for (const line of lines) {
87
+ if (line.startsWith('data: ')) {
88
+ const data = line.slice(6);
89
+ if (data === '[DONE]') continue;
90
+
91
+ try {
92
+ const parsed = JSON.parse(data);
93
+ if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
94
+ const text = parsed.delta.text;
95
+ fullContent += text;
96
+ if (onChunk) {
97
+ onChunk(text);
98
+ }
99
+ }
100
+ } catch {
101
+ // 忽略解析错误
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ return fullContent;
108
+ }
109
+
110
+ async analyzeWithOpenAI(prompt, onChunk) {
111
+ let fullContent = '';
112
+
113
+ const stream = await this.client.chat.completions.create({
114
+ model: this.model || 'gpt-4o',
115
+ messages: [
116
+ {
117
+ role: 'user',
118
+ content: prompt
119
+ }
120
+ ],
121
+ stream: true
122
+ });
123
+
124
+ for await (const chunk of stream) {
125
+ const text = chunk.choices[0]?.delta?.content || '';
126
+ if (text) {
127
+ fullContent += text;
128
+ if (onChunk) {
129
+ onChunk(text);
130
+ }
131
+ }
132
+ }
133
+
134
+ return fullContent;
135
+ }
136
+
137
+ // 非流式分析 (备用)
138
+ async analyze(prompt) {
139
+ if (this.useAnthropicFormat) {
140
+ const baseUrl = (this.apiHost || 'https://api.anthropic.com').replace(/\/+$/, '');
141
+ const url = `${baseUrl}/v1/messages`;
142
+
143
+ const response = await fetch(url, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ 'x-api-key': this.apiKey,
148
+ 'anthropic-version': '2023-06-01'
149
+ },
150
+ body: JSON.stringify({
151
+ model: this.model || 'claude-sonnet-4-20250514',
152
+ max_tokens: 8192,
153
+ messages: [
154
+ {
155
+ role: 'user',
156
+ content: prompt
157
+ }
158
+ ]
159
+ })
160
+ });
161
+
162
+ if (!response.ok) {
163
+ const errorText = await response.text();
164
+ throw new Error(`${response.status} ${errorText}`);
165
+ }
166
+
167
+ const data = await response.json();
168
+ return data.content[0].text;
169
+ } else {
170
+ const response = await this.client.chat.completions.create({
171
+ model: this.model || 'gpt-4o',
172
+ messages: [
173
+ {
174
+ role: 'user',
175
+ content: prompt
176
+ }
177
+ ]
178
+ });
179
+ return response.choices[0].message.content;
180
+ }
181
+ }
182
+ }
183
+
184
+ export default AIClient;
@@ -0,0 +1,136 @@
1
+ import { simpleGit } from 'simple-git';
2
+ import logger from '../utils/logger.js';
3
+
4
+ const git = simpleGit();
5
+
6
+ export async function isGitRepo() {
7
+ try {
8
+ await git.revparse(['--git-dir']);
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ export async function getLastCommitInfo() {
16
+ try {
17
+ const log = await git.log({ maxCount: 1 });
18
+ if (log.latest) {
19
+ return {
20
+ sha: log.latest.hash,
21
+ message: log.latest.message,
22
+ author: log.latest.author_name,
23
+ date: log.latest.date
24
+ };
25
+ }
26
+ return null;
27
+ } catch (error) {
28
+ logger.error(`获取 commit 信息失败: ${error.message}`);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ export async function getCommitInfo(sha) {
34
+ try {
35
+ const log = await git.log({ from: sha, to: sha, maxCount: 1 });
36
+ if (log.latest) {
37
+ return {
38
+ sha: log.latest.hash,
39
+ message: log.latest.message,
40
+ author: log.latest.author_name,
41
+ date: log.latest.date
42
+ };
43
+ }
44
+ return null;
45
+ } catch (error) {
46
+ logger.error(`获取 commit ${sha} 信息失败: ${error.message}`);
47
+ return null;
48
+ }
49
+ }
50
+
51
+ export async function getDiff(options = {}) {
52
+ try {
53
+ const { staged, commit, from, to } = options;
54
+
55
+ if (staged) {
56
+ // 暂存区 diff
57
+ return await git.diff(['--cached']);
58
+ }
59
+
60
+ if (commit) {
61
+ // 指定 commit 的 diff
62
+ return await git.diff([`${commit}^`, commit]);
63
+ }
64
+
65
+ if (from && to) {
66
+ // commit 范围
67
+ return await git.diff([from, to]);
68
+ }
69
+
70
+ // 默认: 最近一次 commit 的 diff
71
+ const log = await git.log({ maxCount: 1 });
72
+ if (log.latest) {
73
+ return await git.diff([`${log.latest.hash}^`, log.latest.hash]);
74
+ }
75
+
76
+ return '';
77
+ } catch (error) {
78
+ // 如果是首次 commit,没有父 commit
79
+ if (error.message.includes('unknown revision')) {
80
+ try {
81
+ // 尝试获取首次 commit 的全部内容
82
+ return await git.diff(['--cached', 'HEAD']);
83
+ } catch {
84
+ return await git.diff(['HEAD']);
85
+ }
86
+ }
87
+ logger.error(`获取 diff 失败: ${error.message}`);
88
+ return '';
89
+ }
90
+ }
91
+
92
+ export async function getChangedFiles(options = {}) {
93
+ try {
94
+ const { staged, commit, from, to } = options;
95
+
96
+ if (staged) {
97
+ const status = await git.status();
98
+ return status.staged;
99
+ }
100
+
101
+ if (commit) {
102
+ const result = await git.diff(['--name-only', `${commit}^`, commit]);
103
+ return result.trim().split('\n').filter(Boolean);
104
+ }
105
+
106
+ if (from && to) {
107
+ const result = await git.diff(['--name-only', from, to]);
108
+ return result.trim().split('\n').filter(Boolean);
109
+ }
110
+
111
+ // 默认: 最近一次 commit
112
+ const log = await git.log({ maxCount: 1 });
113
+ if (log.latest) {
114
+ const result = await git.diff(['--name-only', `${log.latest.hash}^`, log.latest.hash]);
115
+ return result.trim().split('\n').filter(Boolean);
116
+ }
117
+
118
+ return [];
119
+ } catch (error) {
120
+ logger.error(`获取变更文件列表失败: ${error.message}`);
121
+ return [];
122
+ }
123
+ }
124
+
125
+ export async function getStagedDiff() {
126
+ return getDiff({ staged: true });
127
+ }
128
+
129
+ export default {
130
+ isGitRepo,
131
+ getLastCommitInfo,
132
+ getCommitInfo,
133
+ getDiff,
134
+ getChangedFiles,
135
+ getStagedDiff
136
+ };
@@ -0,0 +1,155 @@
1
+ import chalk from 'chalk';
2
+ import logger from '../utils/logger.js';
3
+
4
+ export function parseAIResponse(content) {
5
+ try {
6
+ // 尝试从响应中提取 JSON
7
+ const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
8
+ if (jsonMatch) {
9
+ return JSON.parse(jsonMatch[1]);
10
+ }
11
+
12
+ // 尝试直接解析
13
+ const trimmed = content.trim();
14
+ if (trimmed.startsWith('{')) {
15
+ return JSON.parse(trimmed);
16
+ }
17
+
18
+ throw new Error('无法解析 AI 响应');
19
+ } catch (error) {
20
+ logger.error(`解析 AI 响应失败: ${error.message}`);
21
+ return null;
22
+ }
23
+ }
24
+
25
+ export function printReport(commitInfo, report) {
26
+ console.log('\n');
27
+
28
+ // 标题
29
+ console.log(chalk.cyan('╭' + '─'.repeat(58) + '╮'));
30
+ console.log(chalk.cyan('│') + chalk.bold.white(' Goodiffer Analysis Report').padEnd(66) + chalk.cyan('│'));
31
+ console.log(chalk.cyan('╰' + '─'.repeat(58) + '╯'));
32
+
33
+ // Commit 信息
34
+ console.log(chalk.blue('\n📝 Commit: ') + chalk.white(commitInfo.message.split('\n')[0]));
35
+ console.log(chalk.gray(` SHA: ${commitInfo.sha.substring(0, 8)} | ${commitInfo.author} | ${commitInfo.date}`));
36
+
37
+ // Summary
38
+ if (report.summary) {
39
+ console.log(chalk.green('\n📊 Summary: ') + chalk.white(report.summary));
40
+ }
41
+
42
+ // Commit Alignment
43
+ if (report.commitAlignment) {
44
+ const aligned = report.commitAlignment.aligned;
45
+ const icon = aligned ? chalk.green('✓') : chalk.red('✗');
46
+ console.log(chalk.yellow('\n🎯 Commit 匹配: ') + icon + ' ' + report.commitAlignment.comment);
47
+ }
48
+
49
+ logger.divider();
50
+
51
+ // Issues
52
+ const issues = report.issues || [];
53
+ const errors = issues.filter(i => i.level === 'error');
54
+ const warnings = issues.filter(i => i.level === 'warning');
55
+ const infos = issues.filter(i => i.level === 'info');
56
+
57
+ if (errors.length > 0) {
58
+ console.log(chalk.red.bold(`\n🔴 ERRORS (${errors.length})`));
59
+ errors.forEach((issue, idx) => printIssue(issue, `E${String(idx + 1).padStart(3, '0')}`));
60
+ }
61
+
62
+ if (warnings.length > 0) {
63
+ console.log(chalk.yellow.bold(`\n🟡 WARNINGS (${warnings.length})`));
64
+ warnings.forEach((issue, idx) => printIssue(issue, `W${String(idx + 1).padStart(3, '0')}`));
65
+ }
66
+
67
+ if (infos.length > 0) {
68
+ console.log(chalk.blue.bold(`\n🔵 INFO (${infos.length})`));
69
+ infos.forEach((issue, idx) => printIssue(issue, `I${String(idx + 1).padStart(3, '0')}`));
70
+ }
71
+
72
+ if (issues.length === 0) {
73
+ console.log(chalk.green.bold('\n✅ 未发现代码问题'));
74
+ }
75
+
76
+ // Association Risks
77
+ const risks = report.associationRisks || [];
78
+ if (risks.length > 0) {
79
+ logger.divider();
80
+ console.log(chalk.magenta.bold(`\n🔗 ASSOCIATION RISKS (${risks.length})`));
81
+ risks.forEach(printRisk);
82
+ }
83
+
84
+ // Suggestions
85
+ const suggestions = report.suggestions || [];
86
+ if (suggestions.length > 0) {
87
+ logger.divider();
88
+ console.log(chalk.cyan.bold('\n💡 建议'));
89
+ suggestions.forEach((s, idx) => {
90
+ console.log(chalk.gray(` ${idx + 1}. `) + chalk.white(s));
91
+ });
92
+ }
93
+
94
+ // 统计
95
+ logger.divider();
96
+ console.log(chalk.gray('\n📈 统计: ') +
97
+ chalk.red(`${errors.length} errors `) +
98
+ chalk.yellow(`${warnings.length} warnings `) +
99
+ chalk.blue(`${infos.length} info `) +
100
+ chalk.magenta(`${risks.length} risks`));
101
+
102
+ console.log('');
103
+ }
104
+
105
+ function printIssue(issue, code) {
106
+ console.log('');
107
+ console.log(chalk.gray(`[${code}] `) + chalk.white(`${issue.file}:${issue.line}`));
108
+ console.log(chalk.gray('类型: ') + chalk.white(issue.type));
109
+ console.log(chalk.gray('问题: ') + chalk.white(issue.description));
110
+
111
+ if (issue.code) {
112
+ console.log(chalk.gray('代码:'));
113
+ console.log(chalk.gray(' ┌─'));
114
+ issue.code.split('\n').forEach(line => {
115
+ console.log(chalk.gray(' │ ') + chalk.red(line));
116
+ });
117
+ console.log(chalk.gray(' └─'));
118
+ }
119
+
120
+ if (issue.suggestion) {
121
+ console.log(chalk.gray('建议: ') + chalk.green(issue.suggestion));
122
+ }
123
+
124
+ if (issue.fixPrompt) {
125
+ console.log(chalk.magenta('\n📋 修复提示词 (复制到 cc/codex):'));
126
+ console.log(chalk.gray('┌' + '─'.repeat(56) + '┐'));
127
+ issue.fixPrompt.split('\n').forEach(line => {
128
+ const paddedLine = line.length > 54 ? line.substring(0, 51) + '...' : line;
129
+ console.log(chalk.gray('│ ') + chalk.yellow(paddedLine.padEnd(54)) + chalk.gray(' │'));
130
+ });
131
+ console.log(chalk.gray('└' + '─'.repeat(56) + '┘'));
132
+ }
133
+ }
134
+
135
+ function printRisk(risk) {
136
+ console.log('');
137
+ console.log(chalk.white('修改文件: ') + chalk.cyan(risk.changedFile));
138
+ console.log(chalk.white('可能影响: ') + chalk.yellow(risk.relatedFiles?.join(', ') || '(未知)'));
139
+ console.log(chalk.white('风险: ') + chalk.red(risk.risk));
140
+
141
+ if (risk.checkPrompt) {
142
+ console.log(chalk.magenta('\n📋 检查提示词:'));
143
+ console.log(chalk.gray('┌' + '─'.repeat(56) + '┐'));
144
+ risk.checkPrompt.split('\n').forEach(line => {
145
+ const paddedLine = line.length > 54 ? line.substring(0, 51) + '...' : line;
146
+ console.log(chalk.gray('│ ') + chalk.yellow(paddedLine.padEnd(54)) + chalk.gray(' │'));
147
+ });
148
+ console.log(chalk.gray('└' + '─'.repeat(56) + '┘'));
149
+ }
150
+ }
151
+
152
+ export default {
153
+ parseAIResponse,
154
+ printReport
155
+ };
@@ -0,0 +1,54 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({
4
+ projectName: 'goodiffer',
5
+ schema: {
6
+ provider: {
7
+ type: 'string',
8
+ enum: ['claude', 'openai', 'custom'],
9
+ default: 'custom'
10
+ },
11
+ apiHost: {
12
+ type: 'string',
13
+ default: ''
14
+ },
15
+ apiKey: {
16
+ type: 'string',
17
+ default: ''
18
+ },
19
+ model: {
20
+ type: 'string',
21
+ default: ''
22
+ }
23
+ }
24
+ });
25
+
26
+ export function getConfig() {
27
+ return {
28
+ provider: config.get('provider'),
29
+ apiHost: config.get('apiHost'),
30
+ apiKey: config.get('apiKey'),
31
+ model: config.get('model')
32
+ };
33
+ }
34
+
35
+ export function setConfig(key, value) {
36
+ config.set(key, value);
37
+ }
38
+
39
+ export function setAllConfig(settings) {
40
+ for (const [key, value] of Object.entries(settings)) {
41
+ config.set(key, value);
42
+ }
43
+ }
44
+
45
+ export function isConfigured() {
46
+ const apiKey = config.get('apiKey');
47
+ return apiKey && apiKey.length > 0;
48
+ }
49
+
50
+ export function clearConfig() {
51
+ config.clear();
52
+ }
53
+
54
+ export default config;
@@ -0,0 +1,45 @@
1
+ import chalk from 'chalk';
2
+
3
+ export const logger = {
4
+ info: (msg) => console.log(chalk.blue('ℹ'), msg),
5
+ success: (msg) => console.log(chalk.green('✔'), msg),
6
+ warn: (msg) => console.log(chalk.yellow('⚠'), msg),
7
+ error: (msg) => console.log(chalk.red('✖'), msg),
8
+
9
+ title: (msg) => console.log(chalk.bold.cyan(`\n${msg}\n`)),
10
+
11
+ box: (title, content) => {
12
+ const width = 50;
13
+ const top = '╭' + '─'.repeat(width - 2) + '╮';
14
+ const bottom = '╰' + '─'.repeat(width - 2) + '╯';
15
+ const line = (text) => '│ ' + text.padEnd(width - 4) + ' │';
16
+
17
+ console.log(chalk.cyan(top));
18
+ console.log(chalk.cyan(line(chalk.bold(title))));
19
+ console.log(chalk.cyan(bottom));
20
+ if (content) {
21
+ console.log(content);
22
+ }
23
+ },
24
+
25
+ divider: () => console.log(chalk.gray('━'.repeat(50))),
26
+
27
+ code: (code, language = '') => {
28
+ console.log(chalk.gray('┌' + '─'.repeat(48) + '┐'));
29
+ code.split('\n').forEach(line => {
30
+ console.log(chalk.gray('│ ') + chalk.white(line));
31
+ });
32
+ console.log(chalk.gray('└' + '─'.repeat(48) + '┘'));
33
+ },
34
+
35
+ prompt: (text) => {
36
+ console.log(chalk.magenta('\n📋 修复提示词 (复制到 cc/codex):'));
37
+ console.log(chalk.gray('┌' + '─'.repeat(48) + '┐'));
38
+ text.split('\n').forEach(line => {
39
+ console.log(chalk.gray('│ ') + chalk.yellow(line));
40
+ });
41
+ console.log(chalk.gray('└' + '─'.repeat(48) + '┘'));
42
+ }
43
+ };
44
+
45
+ export default logger;