rltgjqm
Version:
Google Gemini API를 사용하여 자연어로 Git 명령어를 생성하는 CLI 도구 - 인터랙티브 메뉴와 자동 API 키 설정 지원
394 lines (348 loc) • 13.2 kB
JavaScript
const axios = require('axios');
const chalk = require('chalk');
const usageTracker = require('./usageTracker');
/**
* 통합 AI 서비스 클래스 - ChatGPT와 Gemini 지원
*/
class AIService {
constructor() {
this.supportedProviders = {
'chatgpt': {
name: 'ChatGPT (OpenAI)',
modelName: 'gpt-4o-mini',
endpoint: 'https://api.openai.com/v1/chat/completions'
},
'gemini': {
name: 'Gemini (Google)',
modelName: 'gemini-1.5-flash',
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'
}
};
}
/**
* 설정된 AI 플랫폼과 API 키 가져오기
*/
getConfig() {
try {
// 임시 설정이 있으면 우선 사용 (테스트용)
if (this.tempConfig) {
return this.tempConfig;
}
const config = require('./config');
return config.getAIConfig();
} catch (error) {
return { provider: null, apiKey: null };
}
}
/**
* AI 플랫폼별 Git 명령어 생성
* @param {string} prompt - 완성된 프롬프트
* @returns {Promise<{response: string, usageInfo: object}>} 생성된 응답 텍스트와 사용량 정보
*/
async generateCommand(prompt) {
const { provider, apiKey } = this.getConfig();
if (!provider || !apiKey) {
throw new Error('AI 플랫폼이 설정되지 않았습니다.');
}
let outputMode = 'detail'; // 기본값
let debugMode = false; // 기본값
try {
const config = require('./config');
outputMode = config.getOutputMode();
debugMode = config.getDebugMode();
} catch (error) {
// config 로딩 실패시 기본값 사용
}
if (outputMode === 'detail') {
console.log(chalk.white('🤖 AI 명령어 생성 중...'));
console.log(chalk.white(`플랫폼: ${this.supportedProviders[provider].name}`));
console.log(chalk.white(`모델: ${this.supportedProviders[provider].modelName}`));
}
// 디버그 모드에서 전달되는 프롬프트 표시
if (debugMode) {
console.log(chalk.magenta('\n🔍 [DEBUG] 전달되는 프롬프트:'));
console.log(chalk.gray('─'.repeat(50)));
console.log(chalk.gray(prompt));
console.log(chalk.gray('─'.repeat(50)));
}
try {
switch (provider) {
case 'chatgpt':
return await this.callChatGPT(prompt, apiKey);
case 'gemini':
return await this.callGemini(prompt, apiKey);
default:
throw new Error(`지원하지 않는 AI 플랫폼: ${provider}`);
}
} catch (error) {
console.error(chalk.red('❌ AI API 호출 실패:'));
throw error;
}
}
/**
* ChatGPT API 호출
*/
async callChatGPT(prompt, apiKey) {
let outputMode = 'detail'; // 기본값
let debugMode = false; // 기본값
try {
const config = require('./config');
outputMode = config.getOutputMode();
debugMode = config.getDebugMode();
} catch (error) {
// config 로딩 실패시 기본값 사용
}
try {
const response = await axios.post(
this.supportedProviders.chatgpt.endpoint,
{
model: this.supportedProviders.chatgpt.modelName,
messages: [
{
role: 'user',
content: prompt
}
],
temperature: 0.1,
max_tokens: 2048
},
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
timeout: 30000
}
);
if (response.data && response.data.choices && response.data.choices.length > 0) {
const generatedText = response.data.choices[0].message.content;
const usageInfo = response.data.usage;
console.log(chalk.green('✅ ChatGPT 응답 받음'));
// 디버그 모드에서 받은 응답 표시
if (outputMode === 'detail' && debugMode) {
console.log(chalk.magenta('\n🔍 [DEBUG] 받은 응답:'));
console.log(chalk.gray('─'.repeat(50)));
console.log(chalk.gray(generatedText));
console.log(chalk.gray('─'.repeat(50)));
}
// 사용량 기록
const recordedUsage = usageTracker.recordChatGPTUsage(usageInfo);
return {
response: generatedText,
usageInfo: usageInfo
};
} else {
throw new Error('ChatGPT API 응답 형식이 올바르지 않습니다.');
}
} catch (error) {
this.handleChatGPTError(error);
throw error;
}
}
/**
* Gemini API 호출
*/
async callGemini(prompt, apiKey) {
let outputMode = 'detail'; // 기본값
let debugMode = false; // 기본값
try {
const config = require('./config');
outputMode = config.getOutputMode();
debugMode = config.getDebugMode();
} catch (error) {
// config 로딩 실패시 기본값 사용
}
try {
const requestData = {
contents: [
{
parts: [
{
text: prompt
}
]
}
],
generationConfig: {
temperature: 0.1,
topK: 40,
topP: 0.95,
maxOutputTokens: 2048,
stopSequences: []
},
safetySettings: [
{
category: "HARM_CATEGORY_HARASSMENT",
threshold: "BLOCK_NONE"
},
{
category: "HARM_CATEGORY_HATE_SPEECH",
threshold: "BLOCK_NONE"
},
{
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
threshold: "BLOCK_NONE"
},
{
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
threshold: "BLOCK_NONE"
}
]
};
const response = await axios.post(
`${this.supportedProviders.gemini.endpoint}?key=${apiKey}`,
requestData,
{
headers: {
'Content-Type': 'application/json',
},
timeout: 30000
}
);
if (response.data && response.data.candidates && response.data.candidates.length > 0) {
const candidate = response.data.candidates[0];
if (candidate.finishReason === 'SAFETY') {
throw new Error('안전성 필터로 인해 응답이 차단되었습니다. 다른 방식으로 질문해보세요.');
}
if (candidate.finishReason === 'RECITATION') {
throw new Error('저작권 문제로 응답이 차단되었습니다. 다른 방식으로 질문해보세요.');
}
if (!candidate.content || !candidate.content.parts || candidate.content.parts.length === 0) {
throw new Error('빈 응답을 받았습니다. 다시 시도해보세요.');
}
const generatedText = candidate.content.parts[0].text;
// 정확한 사용량 정보 추출 (Gemini API는 usageMetadata 제공)
const usageMetadata = response.data.usageMetadata || {};
const actualUsage = {
prompt_tokens: usageMetadata.promptTokenCount || 0,
completion_tokens: usageMetadata.candidatesTokenCount || 0,
total_tokens: usageMetadata.totalTokenCount || 0
};
console.log(chalk.green('✅ Gemini 응답 받음'));
if (outputMode === 'detail') {
console.log(chalk.white(`📊 실제 사용량: ${actualUsage.prompt_tokens} → ${actualUsage.completion_tokens} (총 ${actualUsage.total_tokens} 토큰)`));
}
// 디버그 모드에서 받은 응답 표시
if (outputMode === 'detail' && debugMode) {
console.log(chalk.magenta('\n🔍 [DEBUG] 받은 응답:'));
console.log(chalk.gray('─'.repeat(50)));
console.log(chalk.gray(generatedText));
console.log(chalk.gray('─'.repeat(50)));
}
// 정확한 사용량 정보로 기록
const usageInfo = usageTracker.recordGeminiUsageWithActualData(actualUsage);
return {
response: generatedText,
usageInfo: usageInfo
};
} else {
throw new Error('Gemini API 응답 형식이 올바르지 않습니다.');
}
} catch (error) {
this.handleGeminiError(error);
throw error;
}
}
/**
* ChatGPT 에러 처리
*/
handleChatGPTError(error) {
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
console.error(chalk.red('ChatGPT API 키가 유효하지 않습니다. rltgjqm config로 API 키를 확인하세요.'));
} else if (status === 429) {
console.error(chalk.red('ChatGPT API 호출 한도를 초과했습니다. 잠시 후 다시 시도하세요.'));
} else if (status === 500) {
console.error(chalk.red('ChatGPT API 서버 오류입니다. 잠시 후 다시 시도하세요.'));
} else {
console.error(chalk.red(`ChatGPT HTTP ${status}: ${error.message}`));
}
} else if (error.code === 'ECONNABORTED') {
console.error(chalk.red('요청 시간 초과. 인터넷 연결을 확인하세요.'));
} else if (error.code === 'ENOTFOUND') {
console.error(chalk.red('인터넷 연결을 확인하세요.'));
} else {
console.error(chalk.red(error.message));
}
}
/**
* Gemini 에러 처리
*/
handleGeminiError(error) {
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 400) {
console.error(chalk.red('Gemini 요청 형식이 올바르지 않습니다.'));
if (data.error && data.error.message) {
console.error(chalk.red(`상세: ${data.error.message}`));
}
} else if (status === 401) {
console.error(chalk.red('Gemini API 키가 유효하지 않습니다. rltgjqm config로 API 키를 확인하세요.'));
} else if (status === 403) {
console.error(chalk.red('Gemini API 접근 권한이 없습니다. API 키 권한을 확인하세요.'));
} else if (status === 404) {
console.error(chalk.red(`Gemini 모델을 찾을 수 없습니다. 현재 모델: ${this.supportedProviders.gemini.modelName}`));
} else if (status === 429) {
console.error(chalk.red('Gemini API 호출 한도를 초과했습니다. 잠시 후 다시 시도하세요.'));
} else if (status === 500) {
console.error(chalk.red('Gemini API 서버 오류입니다. 잠시 후 다시 시도하세요.'));
} else {
console.error(chalk.red(`Gemini HTTP ${status}: ${error.message}`));
}
} else if (error.code === 'ECONNABORTED') {
console.error(chalk.red('요청 시간 초과. 인터넷 연결을 확인하세요.'));
} else if (error.code === 'ENOTFOUND') {
console.error(chalk.red('인터넷 연결을 확인하세요.'));
} else {
console.error(chalk.red(error.message));
}
}
/**
* 임시 AI 설정 (테스트용)
*/
setTempConfig(provider, apiKey) {
this.tempConfig = { provider, apiKey };
}
/**
* 임시 설정 초기화
*/
clearTempConfig() {
this.tempConfig = null;
}
/**
* AI 플랫폼별 API 키 유효성 검사
* @param {string} provider - AI 플랫폼 ('chatgpt' | 'gemini')
* @param {string} apiKey - API 키
* @returns {Promise<boolean>} API 키 유효성 여부
*/
async validateApiKey(provider, apiKey) {
try {
console.log(chalk.white(`🔑 ${this.supportedProviders[provider].name} API 키 유효성 검사 중...`));
const testPrompt = `간단한 테스트입니다. "git status" 명령어만 출력해주세요.`;
// 임시로 설정하여 테스트
const originalConfig = this.getConfig();
// 테스트를 위해 임시 설정
this.setTempConfig(provider, apiKey);
const result = await this.generateCommand(testPrompt);
// 원래 설정 복구
this.setTempConfig(originalConfig.provider, originalConfig.apiKey);
console.log(chalk.green(`✅ ${this.supportedProviders[provider].name} API 키가 유효합니다.`));
return true;
} catch (error) {
console.log(chalk.red(`❌ ${this.supportedProviders[provider].name} API 키가 유효하지 않습니다.`));
// 임시 설정 초기화
this.clearTempConfig();
return false;
}
}
/**
* 지원하는 AI 플랫폼 목록 반환
*/
getSupportedProviders() {
return this.supportedProviders;
}
}
module.exports = new AIService();