Skip to content

Commit

Permalink
feat: 增加一个功能,生成缓存区文件变更的提交信息,并与 OpenAI API 集成 (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
xun082 committed Jun 23, 2024
1 parent f6b6fce commit 30efdc8
Show file tree
Hide file tree
Showing 10 changed files with 3,010 additions and 2,487 deletions.
5,266 changes: 2,889 additions & 2,377 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

23 changes: 10 additions & 13 deletions src/core/commit/help.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { execSync } from 'node:child_process';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// const __dirname = fileURLToPath(import.meta.url);
/**
* Check if a file has changed in the git index
*/

export function getFilesChangedInGitAdd() {
const gitDiff = execSync('git diff --cached --name-only', { encoding: 'utf-8' });
return gitDiff.split('\n');
}
const files = gitDiff.split('\n');

// export function getOpenAIkey() {
// // TODO: 拿到openAI的key,目前没有可使用的key。这里后续在进行安排
// const filePath = path.join(__dirname, '..', '..', '..', '.env');
// const content = fs.readFileSync(filePath, { encoding: 'utf-8' });
// return content.split('=')[1].replace('\n', '');
// }
// 过滤掉 lock 文件
const ignoredPatterns = [/package-lock\.json$/, /yarn\.lock$/, /pnpm-lock\.yaml$/];
const filteredFiles = files.filter(
(file) => file && !ignoredPatterns.some((pattern) => pattern.test(file)),
);

return filteredFiles;
}

interface Staged {
filename: string;
Expand Down
65 changes: 38 additions & 27 deletions src/core/commit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,48 @@ import { intro, outro, spinner } from '@clack/prompts';
import { allStagedFiles2Message, getFilesChangedInGitAdd } from './help';
import { createChatCompletion } from './openai';

export default async () => {
export default async function commitMessage() {
intro('-- 开始读取缓存区文件更改');

// 获取缓存区的文件列表
const files = getFilesChangedInGitAdd();
const staged = [];
for (const file of files) {
if (file) {
const fileContent = fs.readFileSync(file, { encoding: 'utf-8' });
staged.push({
filename: file,
content: fileContent,
});
}
}
// 我们可以拿到staged的内容, 需要一个判断是否为空的逻辑

// 读取文件内容,并存储在staged数组中
const staged = files.filter(Boolean).map((file) => {
const content = fs.readFileSync(file, 'utf-8');
return { filename: file, content };
});

// 判断是否有文件被缓存
if (!staged || staged.length === 0) {
throw new Error('No files in staged');
throw new Error('没有文件被缓存');
}

const s = spinner();
s.start('AI is analyzing your changes');
const content = allStagedFiles2Message(staged);
const message = await createChatCompletion(content, { locale: 'zh-CN', maxLength: 200 }).catch(
(err) => {
console.log(err);
process.exit(1);
},
);
const messageParse = JSON.parse(message);
if (messageParse?.choices?.length === 0) {
s.start('AI 正在分析您的更改');

try {
// 将缓存的文件内容转换为消息
const content = allStagedFiles2Message(staged);

// 使用 OpenAI API 生成提交信息
const message = await createChatCompletion(content, { locale: 'zh-CN', maxLength: 200 });

// 检查 OpenAI API 的响应结构
if (!message || !message.choices || !message.choices[0] || !message.choices[0].message) {
throw new Error('OpenAI API 响应结构无效');
}

const completion = message.choices[0].message.content;

// 去除不需要的字符
const result = completion.replace(/[*_`~]/g, '');

s.stop();
outro(result);
} catch (err) {
s.stop();
console.error('错误:', err);
process.exit(1);
}
const commitMessage = JSON.parse(message).choices[0].message.content;
s.stop();
outro(commitMessage);
};
}
60 changes: 9 additions & 51 deletions src/core/commit/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,8 @@ import type { ClientRequest, IncomingMessage } from 'http';

import { createChatRequest } from './prompt';

import { openAiClient } from '@/utils';

let OPENAI_API_KEY: string;

/**
* 老方法,使用https调
* @param json
*/
const callOpenAI = async (
json: string,
): Promise<{ data: string; request: ClientRequest; response: IncomingMessage }> => {
return new Promise((resolve, reject) => {
const postBody = JSON.stringify(json);
const request = https.request(
{
port: 443,
host: 'api.openai.com',
path: '/v1/chat/completions',
method: 'POST',
timeout: 20000, // 20 seconds 为了让接口有充足的时间把内容返回
headers: {
Authorization: `Bearer ${OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
},
(response) => {
const res: Buffer[] = [];
response.on('data', (chunk) => res.push(chunk));
response.on('end', () => {
const data = Buffer.concat(res).toString();
resolve({
request,
response,
data,
});
});
},
);
request.on('error', reject);
request.on('timeout', () => {
request.destroy();
reject(new Error(`Request timeout`));
});
request.write(postBody);
request.end();
});
};
import { getOpenAiClient } from '@/utils';
import { OPENAI_CHAT_COMPLETIONS_ENDPOINT } from '@/utils/constants';

/**
*
Expand All @@ -62,11 +17,14 @@ export const createChatCompletion = async (
) => {
const { locale, maxLength } = options;
const json = createChatRequest(diff, { locale, maxLength });
// 获取apikey, 并且调openai的接口
const res = await openAiClient.post('/v1/chat/completions', json);
const parseResult = JSON.parse(res.data);

const openAiClient = await getOpenAiClient();
const res = await openAiClient.post(OPENAI_CHAT_COMPLETIONS_ENDPOINT, json);

const parseResult = res.data;
if ('error' in parseResult) {
throw new Error(`OpenAI error: ${parseResult.error.message}`);
}
return res.data;

return parseResult;
};
17 changes: 13 additions & 4 deletions src/core/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { marked } from 'marked';
import path from 'path';
import fs from 'fs';
import { outro, spinner } from '@clack/prompts';

import { getUserInput } from './select';

import { UserSelection } from '@/types';
import { openAiClient, validateFileName, validatePath } from '@/utils';
import { getOpenAiClient, validateFileName, validatePath } from '@/utils';
import { OPENAI_CHAT_COMPLETIONS_ENDPOINT } from '@/utils/constants';

interface CodeBlocks {
[key: string]: string[];
Expand Down Expand Up @@ -53,7 +55,13 @@ export default async function createComponents({

const prompts = generatorComponentPrompt(input);

const response = await openAiClient.post('/v1/chat/completions', {
const openAiClient = await getOpenAiClient();

const s = spinner();

s.start('AI is generating components for you');

const response = await openAiClient.post(OPENAI_CHAT_COMPLETIONS_ENDPOINT, {
model: 'gpt-4o',
messages: [
{
Expand Down Expand Up @@ -101,15 +109,16 @@ export default async function createComponents({

for (const [key, value] of Object.entries(result)) {
if (['css', 'less', 'scss'].includes(key)) {
console.log(`${key.toUpperCase()} content found:`);
// console.log(value.join("\n\n"));
const filePath = path.join(outputDir, `index.module.${input.cssOption}`);
fs.writeFileSync(filePath, value.join('\n\n'), 'utf8');
} else {
const filePath = path.join(outputDir, `index.tsx`);
fs.writeFileSync(filePath, value.join('\n\n'), 'utf8');
}
}

s.stop();
outro('Component creation complete 🎉🎉🎉');
} catch (error) {
console.log(error);
}
Expand Down
24 changes: 24 additions & 0 deletions src/core/components/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { UserSelection } from '@/types';

function generatorComponentPrompt({
framework,
languageType,
cssOption,
userInput,
}: UserSelection): string {
const cssPreset = cssOption === 'less' || cssOption === 'scss';

const cssRequirement = cssPreset
? `要求:CSS代码和组件代码抽离出来,需要在组件里面引入,CSS代码的文件名应该是 index.module.${cssOption},生成的代码要求 css 的在最后,js的在最前面`
: '';

return `请根据以下信息生成一个${languageType}组件:
框架:${framework}
CSS预处理器:${cssPreset ? `${cssOption} module` : cssOption}
描述:这是一个基于${framework}框架的组件,使用${languageType}编写。
功能:${userInput}
要求:提供详细的代码示例,包括必要的导入、组件定义、状态管理(如果适用)、以及基本的样式。
请注意,不能再代码块里面添加文件名的注释,我不需要文件名
${cssRequirement}
要求:生成的代码应该符合TypeScript的类型检查要求,并且只返回代码部分,不要包含其他任何描述信息。代码中不需要携带有文件名的注释。`;
}
2 changes: 1 addition & 1 deletion src/core/components/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function getUserInput(): Promise<UserSelection | null> {
const cssOption = await makeSelection('请选择一个CSS选项:', cssOptions);
if (!cssOption) return null;

const userInput = await inputDefaultText('请输入内容:', '默认文本');
const userInput = await inputDefaultText('请输入该组件的需求:', '创建一个基础的按钮组件');
if (!userInput) return null;

return { framework, languageType, cssOption, userInput };
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { setConfig, getConfig, initializeProject } from './core/config';
import { osRootDirectory } from './utils';
import { ConfigItem } from './types';
import createComponents from './core/components';
import aiCommit from './core/commit';
import commitMessage from './core/commit';

async function main() {
console.log(osRootDirectory());
Expand All @@ -29,7 +29,7 @@ async function main() {
.description('Generate a commit message')
.description('AI will automatically generate submission information for you')
.action(() => {
aiCommit();
commitMessage();
});

program
Expand Down
1 change: 1 addition & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const CONFIG_FILE_NAME = 'toolkit.config.json';
export const OPENAI_CHAT_COMPLETIONS_ENDPOINT = '/v1/chat/completions';
35 changes: 23 additions & 12 deletions src/utils/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,27 @@ import { getConfig } from '@/core/config';

let openAiClient: AxiosInstance;

(async () => {
const endpoint = await getConfig('END_POINT');
const apiKey = await getConfig('OPEN_AI_KEY');
openAiClient = axios.create({
baseURL: `${endpoint}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
});
})();
const initializeOpenAiClient = async () => {
try {
const endpoint = await getConfig('END_POINT');
const apiKey = await getConfig('OPEN_AI_KEY');
openAiClient = axios.create({
baseURL: `${endpoint}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
});
} catch (error) {
console.error('Failed to initialize OpenAI client:', error);
}
};

export { openAiClient };
const getOpenAiClient = async (): Promise<AxiosInstance> => {
if (!openAiClient) {
await initializeOpenAiClient();
}
return openAiClient;
};

export { getOpenAiClient };

0 comments on commit 30efdc8

Please sign in to comment.