AI生成应用平台 优化点 npm打包失败时发给ai重修修改
引言
最近在学鱼总的AI生成应用平台https://www.codefather.cn/course/1948291549923344386
刚学完第12期部署,我想着自己优化一下,之前在生成项目时有一次AI生成项目时构建失败了,我想着就优化这个点 总共有四个部分 灵感来源,找原因,自动发送给AI修复和结果展示
(第一次写文章,写的不好请多指教)
灵感来源于美团的nocode
鱼总在讲项目时会多次参考美团的nocode,我在用的时候发现美团发现错误时会自动修复错误,想着npm构建失败时也可以仿照这个点

获取构建失败的原因
在core.builder下
新建 BuildResult
▼java复制代码public class BuildResult { private boolean success; private String message; private String errorDetails; }
修改类 VueProjectBuilder , 主要是新增 executeCommandWithOutput 方法,以下是完整代码
▼text复制代码@Slf4j @Component public class VueProjectBuilder { public void buildProjectAsync(String projectPath) { // 在单独的线程中执行构建,避免阻塞主流程 Thread.ofVirtual().name("vue-builder-" + System.currentTimeMillis()).start(() -> { try { BuildResult result = buildProject(projectPath); if (result.isSuccess()) { log.info("异步构建成功: {}", result.getMessage()); } else { log.error("异步构建失败: {}\n详细信息: {}", result.getMessage(), result.getErrorDetails()); } } catch (Exception e) { log.error("异步构建 Vue 项目时发生异常: {}", e.getMessage(), e); } }); } /** * 构建 Vue 项目 * * @param projectPath 项目根目录路径 * @return BuildResult 构建结果 */ public BuildResult buildProject(String projectPath) { File projectDir = new File(projectPath); if (!projectDir.exists() || !projectDir.isDirectory()) { log.error("项目目录不存在: {}", projectPath); return BuildResult.failure("项目目录不存在: " + projectPath, null); } // 检查 package.json 是否存在 File packageJson = new File(projectDir, "package.json"); if (!packageJson.exists()) { log.error("package.json 文件不存在: {}", packageJson.getAbsolutePath()); return BuildResult.failure("package.json 文件不存在", null); } log.info("开始构建 Vue 项目: {}", projectPath); // 执行 npm install BuildResult installResult = executeNpmInstall(projectDir); if (!installResult.isSuccess()) { log.error("npm install 执行失败"); return BuildResult.failure( "npm install 执行失败", installResult.getErrorDetails() ); } // 执行 npm run build BuildResult buildResult = executeNpmBuild(projectDir); if (!buildResult.isSuccess()) { log.error("npm run build 执行失败"); return BuildResult.failure( "npm run build 执行失败", buildResult.getErrorDetails() ); } // 验证 dist 目录是否生成 File distDir = new File(projectDir, "dist"); if (!distDir.exists()) { log.error("构建完成但 dist 目录未生成: {}", distDir.getAbsolutePath()); return BuildResult.failure("构建完成但 dist 目录未生成", null); } log.info("Vue 项目构建成功,dist 目录: {}", distDir.getAbsolutePath()); return BuildResult.success("Vue 项目构建成功"); } /** * 执行 npm install 命令 */ private BuildResult executeNpmInstall(File projectDir) { log.info("执行 npm install..."); String command = String.format("%s install", buildCommand("npm")); return executeCommandWithOutput(projectDir, command, 300); // 5分钟超时 } /** * 执行 npm run build 命令 */ private BuildResult executeNpmBuild(File projectDir) { log.info("执行 npm run build..."); String command = String.format("%s run build", buildCommand("npm")); return executeCommandWithOutput(projectDir, command, 180); // 3分钟超时 } private String buildCommand(String baseCommand) { if (isWindows()) { return baseCommand + ".cmd"; } return baseCommand; } /** * 判断是不是window * @return */ private boolean isWindows() { return System.getProperty("os.name").toLowerCase().contains("windows"); } /** * 执行命令 * * @param workingDir 工作目录 * @param command 命令字符串 * @param timeoutSeconds 超时时间(秒) * @return 是否执行成功 */ private boolean executeCommand(File workingDir, String command, int timeoutSeconds) { try { log.info("在目录 {} 中执行命令: {}", workingDir.getAbsolutePath(), command); Process process = RuntimeUtil.exec( null, workingDir, command.split("\\s+") // 命令分割为数组 ); // 等待进程完成,设置超时 boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); if (!finished) { log.error("命令执行超时({}秒),强制终止进程", timeoutSeconds); process.destroyForcibly(); return false; } int exitCode = process.exitValue(); if (exitCode == 0) { log.info("命令执行成功: {}", command); return true; } else { log.error("命令执行失败,退出码: {}", exitCode); return false; } } catch (Exception e) { log.error("执行命令失败: {}, 错误信息: {}", command, e.getMessage()); return false; } } /** * 执行命令(带输出捕获) * * @param workingDir 工作目录 * @param command 命令字符串 * @param timeoutSeconds 超时时间(秒) * @return BuildResult 包含执行结果和错误信息 */ private BuildResult executeCommandWithOutput(File workingDir, String command, int timeoutSeconds) { try { log.info("在目录 {} 中执行命令: {}", workingDir.getAbsolutePath(), command); ProcessBuilder processBuilder = new ProcessBuilder(command.split("\\s+")); processBuilder.directory(workingDir); processBuilder.redirectErrorStream(true); // 合并标准输出和错误输出 Process process = processBuilder.start(); // 读取输出流 StringBuilder output = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); // 实时输出日志(可选) log.debug("命令输出: {}", line); } } // 等待进程完成 boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); if (!finished) { log.error("命令执行超时({}秒),强制终止进程", timeoutSeconds); process.destroyForcibly(); return BuildResult.failure( "命令执行超时(" + timeoutSeconds + "秒)", output.toString() ); } int exitCode = process.exitValue(); if (exitCode == 0) { log.info("命令执行成功: {}", command); return BuildResult.success("命令执行成功"); } else { log.error("命令执行失败,退出码: {}", exitCode); return BuildResult.failure( "命令执行失败,退出码: " + exitCode, output.toString() ); } } catch (Exception e) { log.error("执行命令失败: {}, 错误信息: {}", command, e.getMessage()); return BuildResult.failure( "执行命令时发生异常: " + e.getMessage(), e.toString() ); } } }
类 AiCodeGeneratorFacade 的 processTokenStream 中 onCompleteResponse 增加获取的代码
▼java复制代码// 创建响应消息 if (buildResult.isSuccess()) { // 构建成功 String successMessage = JSONUtil.createObj() .set("type", "build_result") .set("status", "success") .set("message", buildResult.getMessage()) .toString(); sink.next(successMessage); } else { // 构建失败,返回错误信息 String errorMessage = JSONUtil.createObj() .set("type", "build_result") .set("status", "error") .set("message", buildResult.getMessage()) .set("errorDetails", buildResult.getErrorDetails()) .toString(); sink.next(errorMessage); }
前端
▼ts复制代码eventSource.onmessage = function (event) { if (streamCompleted) return try { // 解析JSON包装的数据 const parsed = JSON.parse(event.data) // 判断消息类型 if (parsed.type === 'build_result') { // 处理构建结果消息 if (parsed.status === 'success') { message.success(' ' + (parsed.message || '项目构建成功')) } else { message.error(' ' + (parsed.message || '项目构建失败')) // 可选:在聊天窗口显示错误详情 if (parsed.errorDetails) { console.error('构建错误详情:', parsed.errorDetails) // 可选:在AI消息中追加错误信息 messages.value[aiMessageIndex].content += `\n\n⚠ 构建失败:${parsed.message}\n详细信息请查看控制台` } } return } if (parsed.type === 'build_fixing') { // 处理构建修复消息 const fixMsg = ` 构建失败,AI 正在尝试修复 (${parsed.attempt}/${parsed.maxAttempts})...` message.info(fixMsg) // 可选:在聊天窗口显示 messages.value[aiMessageIndex].content += `\n\n${fixMsg}` scrollToBottom() return } // 原有的内容处理逻辑 const content = parsed.d if (content !== undefined && content !== null) { fullContent += content messages.value[aiMessageIndex].content = fullContent messages.value[aiMessageIndex].loading = false scrollToBottom() } } catch (error) { console.error('解析消息失败:', error) handleError(error, aiMessageIndex) } }
构建失败时自动发送给AI修复
主要代码 类 AiCodeGeneratorFacade 的 processTokenStream 中 onCompleteResponse 中
▼java复制代码// 构建项目 String projectPath = AppConstant.CODE_OUTPUT_ROOT_DIR + "/vue_project_" + appId; BuildResult buildResult = vueProjectBuilder.buildProject(projectPath); if (buildResult.isSuccess()) { sink.complete(); } else { // fixAttempt防止无限递归 // AtomicInteger fixAttempt = new AtomicInteger(0); //构建失败,尝试修复 int currentAttempt = fixAttempt.incrementAndGet(); if (currentAttempt <= MAX_FIX_ATTEMPTS) { // 新方法:模拟用户发送修复请求消息 String userFixRequest = String.format( "\n\n---\n\n**[系统自动修复 %d/%d]** 构建失败,错误信息:\n\n```\n%s\n```\n\n请修复上述错误。", currentAttempt, MAX_FIX_ATTEMPTS, extractKeyErrorLines(buildResult.getErrorDetails()) ); // 将修复请求作为"用户消息"发送给前端 AiResponseMessage userMsg = new AiResponseMessage(userFixRequest); sink.next(JSONUtil.toJsonStr(userMsg)); log.warn("构建失败,开始第 {} 次修复尝试", currentAttempt); // 构造修复提示词(给 AI 的,用户看不到) String fixPrompt = buildFixPrompt(buildResult); // 调用 AI 修复 AiCodeGeneratorService aiService = aiCodeGeneratorServiceFactory .getAiCodeGeneratorService(appId, CodeGenTypeEnum.VUE_PROJECT); TokenStream fixStream = aiService.generateVueProjectCodeStream(appId, fixPrompt); // 递归处理修复流程 processTokenStreamInternal(fixStream, appId, sink, fixAttempt); } else { // 超过最大重试次数 log.error("构建失败,已达最大修复次数 {}", MAX_FIX_ATTEMPTS); String failMsg = JSONUtil.createObj() .set("type", "build_result") .set("status", "error") .set("message", "构建失败,已尝试 " + MAX_FIX_ATTEMPTS + " 次修复") .set("errorDetails", buildResult.getErrorDetails()) .toString(); sink.next(failMsg); sink.complete(); } }
▼java复制代码/** * 构造修复提示词(严格控制长度在800字符以内) */ private String buildFixPrompt(BuildResult buildResult) { StringBuilder prompt = new StringBuilder(); prompt.append("项目构建失败,请分析并修复以下错误:\n\n"); prompt.append("## 错误信息\n"); prompt.append(buildResult.getMessage()).append("\n\n"); if (buildResult.getErrorDetails() != null && !buildResult.getErrorDetails().isEmpty()) { prompt.append("## 详细错误输出\n"); prompt.append("```\n"); String errorDetails = buildResult.getErrorDetails(); if (errorDetails.length() > 700) { errorDetails = extractKeyErrorLines(errorDetails); } prompt.append(errorDetails); prompt.append("\n```\n\n"); } prompt.append("## 修复要求\n"); prompt.append("1. 使用 file_read 工具读取出错的文件\n"); prompt.append("2. 分析错误原因(如文件不存在、路径错误等)\n"); prompt.append("3. 使用 file_modify 或 file_write 工具修复代码\n"); prompt.append("4. 确保修复后的代码符合 Vue 3 和 Vite 规范\n"); return prompt.toString(); } /** * 提取关键错误行(严格限制在500字符以内) */ private String extractKeyErrorLines(String errorDetails) { StringBuilder result = new StringBuilder(); String[] lines = errorDetails.split("\n"); int lineCount = 0; for (String line : lines) { String trimmed = line.trim(); // 只保留最关键的错误行 if (trimmed.contains("error during build:") || trimmed.contains("Could not resolve") || trimmed.contains("RollupError") || trimmed.contains("Invalid end tag") || trimmed.contains("SyntaxError") || trimmed.contains("unexpected") || (trimmed.contains("file:") && (trimmed.contains(".vue") || trimmed.contains(".js")))) { // 移除 ANSI 转义序列(彩色输出控制符) String cleaned = trimmed.replaceAll("\\u001b\\[[0-9;]*m", ""); result.append(cleaned).append("\n"); lineCount++; // 关键:最多5行 if (lineCount >= 5) { break; } } } // 如果提取结果太少,返回前500字符 if (result.length() < 50) { String cleaned = errorDetails.replaceAll("\\u001b\\[[0-9;]*m", ""); return cleaned.substring(0, Math.min(500, cleaned.length())); } return result.toString(); }
类 JsonMessageStreamHandler 的 handleJsonMessageChunk 新增
▼text复制代码case BUILD_RESULT -> { JSONObject buildData = JSONUtil.parseObj(chunk); String status = buildData.getStr("status"); String message = buildData.getStr("message"); // 保存到数据库 String displayMsg; if ("success".equals(status)) { displayMsg = "\n\n " + message + "\n\n"; } else { displayMsg = "\n\n " + message + "\n\n"; } chatHistoryStringBuilder.append(displayMsg); // 返回给前端 return displayMsg; }
主要部分 递归调用
▼java复制代码// 调用 AI 修复 AiCodeGeneratorService aiService = aiCodeGeneratorServiceFactory .getAiCodeGeneratorService(appId, CodeGenTypeEnum.VUE_PROJECT); TokenStream fixStream = aiService.generateVueProjectCodeStream(appId, fixPrompt); // 递归处理修复流程 processTokenStreamInternal(fixStream, appId, sink, fixAttempt);
发现错误可以自动发给AI 依旧通过 sink 发给外层(下一层)
最外层的AppService调用
▼java复制代码//6调用AI生成代码 Flux<String> contenFlux = aiCodeGeneratorFacade.generateAndSaveCodeStream(message, codeGenTypeEnum, appId); // 7. 收集 AI 响应内容并在完成后记录到对话历史 return streamHandlerExecutor.doExecute(contenFlux, chatHistoryService, appId, loginUser, codeGenTypeEnum);
也不会影响
前端的主要修改是
▼ts复制代码// 🔥 可选:检测是否是系统修复消息,添加特殊样式 if (content.includes('[系统自动修复')) { // 可以添加特殊标记,比如改变文字颜色 messages.value[aiMessageIndex].isSystemMessage = true }
详细修改 -> 构建失败会获取失败信息,并且会调用ai修复错误信息 · ki-211/ai-mother@e8b842e (github.com)
结果展示
我故意删除这个文件

之后AI的修复是
...
成功修复了网站
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
