diff --git a/package.json b/package.json index bcf977c0..be719c77 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "site:home": "cross-env NODE_ENV=production webpack --progress --config scripts/sites/webpack.prod.home.js", "site:mobile": "cross-env NODE_ENV=production webpack --progress --config scripts/sites/webpack.prod.mobile.js", "site:pc": "cross-env NODE_ENV=production webpack --progress --config scripts/sites/webpack.prod.pc.js", + "llm:gen": "node scripts/llm/gen.mjs", "dev:demo": "node scripts/dev/dev-demo.js $@" }, "author": "taoyiyue@bytedance.com", diff --git a/scripts/llm/code.mjs b/scripts/llm/code.mjs new file mode 100644 index 00000000..c49b7337 --- /dev/null +++ b/scripts/llm/code.mjs @@ -0,0 +1,82 @@ +import _ from 'lodash'; +import { statSync, readdirSync, readFileSync } from 'fs'; +import { join } from 'path'; + +///// 辅助函数 ///// +// 递归函数来遍历文件夹 +function walkDir(dir, callback) { + readdirSync(dir).forEach(f => { + let dirPath = join(dir, f); + let isDirectory = statSync(dirPath).isDirectory(); + isDirectory ? walkDir(dirPath, callback) : callback(join(dir, f)); + }); +} + +// 源代码相关处理类,获取文件列表 +class Code { + constructor(dir, fileReg) { + this._dir = dir; + this._fileReg = fileReg; + } + // 获取文件列表 + fileList() { + const fileList = []; + walkDir(this._dir, filePath => { + if (filePath.match(this._fileReg)) { + fileList.push(filePath); + } + }); + return fileList; + } + + // 获取目录内容 + directory() { + const fileList = this.fileList(); + + // 生成目录结构 + const contents = [ + `The source code directory is ${this._dir}, which contains the following files:`, + ...fileList.map(i => '- ' + i), + '\n', + ]; + + return contents.join(','); + } + + // 获取目录内容和文件内容 + directoryAndContent() { + const fileList = this.fileList(); + + // 生成目录结构 + const contents = [ + `The source code directory is ${this.fileList}, which contains the following files:`, + ...fileList.map(i => '- ' + i), + '\n', + ]; + + // 生成每个文件内容 + for (const filePath of fileList) { + const fileContent = readFileSync(filePath, { encoding: 'utf-8' }).trim(); + contents.push(`Code for ${filePath}: `, fileContent, '\n'); + } + return contents.join('\n'); + } +} + +// React 源代码 +export class ReactCode extends Code { + constructor(comp) { + const dir = `packages/arcodesign/components/${_.snakeCase(comp)}/`; + const fileReg = /.(ts|tsx|js|jsx|less)$/; + super(dir, fileReg); + } +} + +// Vue 源代码 +export class VueCode extends Code { + constructor(comp) { + const dir = `packages/arcodesign-vue/components/${_.snakeCase(comp)}/`; + const fileReg = /.(vue|ts|js|less)$/; + super(dir, fileReg); + } +} diff --git a/scripts/llm/coze.mjs b/scripts/llm/coze.mjs new file mode 100644 index 00000000..74626126 --- /dev/null +++ b/scripts/llm/coze.mjs @@ -0,0 +1,178 @@ +import axios from 'axios'; +import _ from 'lodash'; +import { writeFileSync, appendFileSync } from 'fs'; + +///// 一些常量 ///// +const CHAT_URL = 'https://bots.byteintl.net/open_api/v2/chat'; + +// 对话日志,基础类 +class ChatLog { + constructor() { + // 初始化变量 + this.chatHistory = []; + this.chatId = `${_.now()}${_.random(100000, 999999)}`; + this.logFiles = [ + `./scripts/llm/log/realtime.log`, + `./scripts/llm/log/${new Date().toISOString()}.log`, + ]; + + // 初始化文件 + this.logFiles.forEach(i => { + writeFileSync(i, `${new Date().toISOString()} Chat Id:\n${this.chatId}\n\n`); + }); + } + + // 写入日志文件,内部方法 + _append(content) { + this.logFiles.forEach(i => appendFileSync(i, content)); + } +} + +// 对话日志(非流式) +class ChatLogNonStream extends ChatLog { + // 处理查询日志 + query(query) { + this._append(`${new Date().toISOString()} Query:\n${query} \n\n`); + this.chatHistory.push({ role: 'user', content_type: 'text', content: query }); + } + + // 处理响应日志(非流式),并返回 answer + response(data) { + this._append(`${new Date().toISOString()} `); + let answer = ''; + data?.messages?.forEach(({ type, content }) => { + if (type === 'verbose') return; + if (type === 'answer') answer += content; + this._append(`${_.startCase(type)}:\n${content}\n\n`); + this.chatHistory.push({ role: 'assistant', content_type: 'text', type, content }); + }); + return answer; + } +} + +// 对话日志(流式) +class ChatLogStream extends ChatLog { + // 处理查询日志 + query(query) { + this._append(`${new Date().toISOString()} Query:\n${query} \n\n`); + this.chatHistory.push({ role: 'user', content_type: 'text', content: query }); + } + + // 开始写入响应日志(分块) + responseChunkStart() { + this._chunk = {}; + this._append(`${new Date().toISOString()} Response:\n`); + } + // 写入响应日志(分块) + responseChunk(raw) { + const regex = /data:(\{.*?\})(?=\s|$)/g; + const matches = String(raw).match(regex); + + for (const match of matches) { + const data = JSON.parse(match.replace('data:', '')); + + const event = data?.event, + type = data?.message?.type, + content = data?.message?.content; + + // 拼接数据 + if (event === 'message' && content) { + if (!this._chunk?.[type]) this._chunk[type] = ''; + this._chunk[type] += content; + + // 处理 answer + if (type === 'answer') { + this._append(content); + } + } + } + } + // 结束响应(分块) + responseChunkEnd() { + this._append(`\n\n`); + _.forEach(this._chunk, (content, type) => { + if (type === 'verbose') return; + if (type !== 'answer') { + this._append(`${_.startCase(type)}:\n${content}\n\n`); + } + this.chatHistory.push({ role: 'assistant', content_type: 'text', type, content }); + }); + return this._chunk['answer']; + } +} + +// AI 机器人 +export class AiBotNonStream { + constructor(botId, token) { + this.log = new ChatLogNonStream(); + this.botId = botId; + this.token = token; + } + + // 非流式对话 + async chat(query, withHistory = true) { + query = query.trim(); + + const headers = { Authorization: `Bearer ${this.token}` }; + const body = { + conversation_id: this.log.chatId, + bot_id: this.botId, + user: 'Aex', + query, + chat_history: withHistory ? this.log.chatHistory : [], + // stream: true, + }; + this.log.query(query); + + // 开始请求 + const res = await axios.post(CHAT_URL, body, { headers }); + const answer = this.log.response(res.data); + + return answer; + } +} + +export class AiBot { + constructor(botId, token) { + this.log = new ChatLogStream(); + this.botId = botId; + this.token = token; + } + + // 流式对话 + async chat(query, withHistory = true) { + query = query.trim(); + + const headers = { Authorization: `Bearer ${this.token}` }; + const body = { + conversation_id: this.log.chatId, + bot_id: this.botId, + user: 'Aex', + query, + chat_history: withHistory ? this.log.chatHistory : [], + stream: true, // 将请求变为流式 + }; + this.log.query(query); + + // 开始请求 - 注意这里使用流式处理方式 + const res = await axios.post(CHAT_URL, body, { headers, responseType: 'stream' }); + + this.log.responseChunkStart(); + + // 设置一个数组来收集流式数据片段 + res.data.on('data', chunk => { + this.log.responseChunk(chunk.toString('utf-8')); + }); + + return new Promise((resolve, reject) => { + res.data.on('end', () => { + const data = this.log.responseChunkEnd(); + resolve(data); + }); + + res.data.on('error', err => { + reject(err); + }); + }); + } +} diff --git a/scripts/llm/gen-article.mjs b/scripts/llm/gen-article.mjs new file mode 100644 index 00000000..35629c32 --- /dev/null +++ b/scripts/llm/gen-article.mjs @@ -0,0 +1,20 @@ +import { AiBotNonStream } from './coze.mjs'; + +// const BOT_ID = '7372403809576009729'; // Claude 3 Opus 模型 +// const BOT_ID = '7369473402367361041'; // GPT-4 Turbo 模型 +const BOT_ID = '7373886057160753169'; // GPT-4o 模型 + +const TOKEN = process.env.COZE_TOKEN_ADM; + +const prompt_article = ` +今天又是努力工作的一天,帮我写一篇今天的日记,100 字左右 + +`; + +///// 主流程开始 ///// + +const bot = new AiBotNonStream(BOT_ID, TOKEN); +// 主指令 +const answer = await bot.chat(prompt_article); + +console.log(answer); diff --git a/scripts/llm/gen-game.mjs b/scripts/llm/gen-game.mjs new file mode 100644 index 00000000..8df19cff --- /dev/null +++ b/scripts/llm/gen-game.mjs @@ -0,0 +1,30 @@ +import { AiBot } from './coze.mjs'; + +const BOT_ID = '7372403809576009729'; // Claude 3 Opus 模型 +// const BOT_ID = '7369473402367361041'; // GPT-4 Turbo 模型 +// const BOT_ID = '7373886057160753169'; // GPT-4o 模型 +const TOKEN = process.env.COZE_TOKEN_ADM; + +// 一个游戏,测试 AI 上下文能力 +const prompt_game = ` +现在我们玩一个游戏: + +当我发送一个数字,你应该将这个数字+1,并将结果发给我。 +当我发送 "Next",你应该继续+1,并将结果发给我。 +当再次接收到数字后,游戏重新开始。 + +严格按照游戏规则回复,不要回复其他内容。 + +首先我给你一个数字是 1 + +`; + +///// 主流程开始 ///// + +const bot = new AiBot(BOT_ID, TOKEN); +// 主指令 +await bot.chat(prompt_game); +await bot.chat('Next'); +await bot.chat('Next'); +await bot.chat('Next'); +await bot.chat('Next'); diff --git a/scripts/llm/gen.mjs b/scripts/llm/gen.mjs new file mode 100644 index 00000000..dff10a35 --- /dev/null +++ b/scripts/llm/gen.mjs @@ -0,0 +1,66 @@ +import { writeFileSync } from 'fs'; +import { ReactCode, VueCode } from './code.mjs'; +import { AiBot } from './coze.mjs'; + +// const BOT_ID = '7372403809576009729'; // Claude 3 Opus 模型 +// const BOT_ID = '7369473402367361041'; // GPT-4 Turbo 模型 +const BOT_ID = '7373886057160753169'; // GPT-4o 模型 + +const TOKEN = process.env.COZE_TOKEN_ADM; + +// 指令一,以下指令为示例代码 +const prompt1 = ` + +# 角色 + +你是一个优秀的前端工程师,熟悉 React 和 Vue 框架,你的主要工作是将 Arco Design Mobile 已有的 React 组件改写为 Vue 组件。 + +接下来我为你提供 Arco Design Mobile 某些组件的源代码,你需要学习这些源代码的目录结构、代码风格和命名习惯。 + +第一个是 Notify 组件,下面是 React 版本的源代码 +${new ReactCode('notify').directoryAndContent()} +将其改写为 Vue 版本后的代码如下 +${new VueCode('notify').directoryAndContent()} + +第二个是 Cell 组件,下面是 React 版本的源代码 +${new ReactCode('cell').directoryAndContent()} +将其改写为 Vue 版本后的代码如下 +${new VueCode('cell').directoryAndContent()} + +第三个是 Loading 组件,这个组件只有 React 版本的源代码 +${new ReactCode('loading').directoryAndContent()} + +根据以上的代码,开始你的工作:你需要严格参考 Cell 组件和 Notify 组件两个版本源代码的目录结构、命名习惯,将 Loading 的 React 组件改写为 Vue 组件。 + +我帮你完成了 Loading 组件 Vue 版本的目录结构,具体如下 +${new VueCode('loading').directory()} + +在进行改写的时候,你需要严格遵守并满足以下要求: +- 组件的功能和表现应与 React 代码完全一致 +- 需要将 React 的 JSX/TSX 转换为 Vue 文件要使用模版语法 +- 生成的代码首行需要添加注释 "Note: Generated by AI, needs verification" +- 生成的代码需要添加中英文双语注释 +- 源代码要完整清晰,关键部分不要做任何的省略 +- 代码的格式和内容要符合业界最佳实践,特别要注意 TS 的一些类型 + +当你准备好了,请回复 Ready。 +接着我会引导你一步一步完成代码编写,每当我发送文件路径时,你需要直接回复该文件的代码,回复的格式满足以下要求: +- 回复的内容必须是代码,如果有额外的补充说明或解析,需要使用注释的方式 +- 除了源代码和注释本身,不要回复任何其他无关的内容 +`; + +///// 主流程开始 ///// + +const bot = new AiBot(BOT_ID, TOKEN); +// 主指令 +await bot.chat(prompt1); + +// 单文件代码生成 +for (const filePath of new VueCode('loading').fileList()) { + console.log('即将生成文件代码:', filePath); + const content = await bot.chat(filePath); + + // 匹配代码部分并写入文件 + const match = content.match(/```(?:\w*\n)?([\s\S]*?)```/); + writeFileSync(filePath, match?.[1] ?? content); +} diff --git a/scripts/llm/log/.gitignore b/scripts/llm/log/.gitignore new file mode 100644 index 00000000..bf0824e5 --- /dev/null +++ b/scripts/llm/log/.gitignore @@ -0,0 +1 @@ +*.log \ No newline at end of file