package com.svnlog; import okhttp3.*; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.*; import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.*; /** * 使用DeepSeek API处理SVN日志并生成工作量统计Excel */ public class DeepSeekLogProcessor { private static final String DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions"; private static final String API_KEY = "sk-48c59012c93b43a08fecbaf3e74799e7"; // 用户需要替换为实际的API Key private static final OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(60, java.util.concurrent.TimeUnit.SECONDS) .readTimeout(300, java.util.concurrent.TimeUnit.SECONDS) // 5分钟读取超时 .writeTimeout(60, java.util.concurrent.TimeUnit.SECONDS) .build(); public static void main(String[] args) { try { Scanner scanner = new Scanner(System.in); System.out.println("==========================================="); System.out.println(" SVN日志工作量统计工具(DeepSeek版)"); System.out.println(" 支持多项目汇总分析"); System.out.println("==========================================="); System.out.println(); // 读取markdown日志文件目录 System.out.print("请输入markdown日志文件所在目录路径 (回车使用当前目录): "); String dirPath = scanner.nextLine().trim(); File dir; if (dirPath.isEmpty()) { dir = new File("."); } else { dir = new File(dirPath); } if (!dir.exists() || !dir.isDirectory()) { System.err.println("错误: 目录不存在或不是有效目录!"); return; } // 扫描目录中的所有 .md 文件 File[] mdFiles = dir.listFiles((d, name) -> name.endsWith(".md")); if (mdFiles == null || mdFiles.length == 0) { System.err.println("错误: 目录中未找到任何 .md 文件!"); return; } System.out.println("找到 " + mdFiles.length + " 个日志文件:"); for (File file : mdFiles) { System.out.println(" - " + file.getName()); } System.out.println(); // 输入工作周期 SimpleDateFormat periodSdf = new SimpleDateFormat("yyyy年MM月"); String defaultPeriod = periodSdf.format(new Date()); System.out.print("请输入工作周期 (例如: 2025年12月,回车使用默认: " + defaultPeriod + "): "); String period = scanner.nextLine().trim(); if (period.isEmpty()) { period = defaultPeriod; System.out.println("使用默认工作周期: " + period); } // 读取并合并所有markdown文件 String combinedContent = readAndCombineMarkdownFiles(mdFiles); System.out.println("成功读取并合并 " + mdFiles.length + " 个日志文件,总长度: " + combinedContent.length() + " 字符"); // 提示API Key System.out.print("请输入DeepSeek API Key (留空使用代码中预设的): "); String inputApiKey = scanner.nextLine().trim(); String apiKey = inputApiKey.isEmpty() ? API_KEY : inputApiKey; if (apiKey.equals("YOUR_DEEPSEEK_API_KEY")) { System.err.println("错误: 请提供有效的DeepSeek API Key!"); return; } // 询问输出文件名 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); String defaultOutput = sdf.format(new Date()) + "工作量统计.xlsx"; System.out.print("请输入输出Excel文件名 (回车使用默认: " + defaultOutput + "): "); String outputPath = scanner.nextLine().trim(); if (outputPath.isEmpty()) { outputPath = defaultOutput; } System.out.println(); System.out.println("正在调用DeepSeek API分析日志..."); // 调用DeepSeek API处理日志 String prompt = buildPrompt(combinedContent, period); String aiResponse = callDeepSeekAPI(apiKey, prompt); if (aiResponse == null) { System.err.println("DeepSeek API调用失败!请检查网络连接和API Key。"); return; } if (aiResponse.isEmpty()) { System.err.println("DeepSeek API返回空响应!请重试或联系技术支持。"); return; } System.out.println("DeepSeek分析完成,正在生成Excel..."); // 生成Excel generateExcel(outputPath, aiResponse); System.out.println(); System.out.println("Excel文件生成成功: " + outputPath); System.out.println(); } catch (Exception e) { System.err.println("发生错误: " + e.getMessage()); e.printStackTrace(); } } /** * 读取文件内容 */ private static String readFile(String path) throws IOException { return new String(Files.readAllBytes(new File(path).toPath()), "UTF-8"); } /** * 读取并合并多个markdown文件的内容 */ private static String readAndCombineMarkdownFiles(File[] mdFiles) throws IOException { StringBuilder combinedContent = new StringBuilder(); for (File file : mdFiles) { String projectName = extractProjectName(file.getName()); String content = readFile(file.getAbsolutePath()); combinedContent.append("\n\n"); combinedContent.append("=== 项目: ").append(projectName).append(" ===\n"); combinedContent.append(content); } return combinedContent.toString(); } /** * 从文件名中提取项目名称 * 例如: svn_log_PRS-7050场站智慧管控_20260130_093348.md -> PRS-7050场站智慧管控 */ private static String extractProjectName(String fileName) { // 去掉 svn_log_ 前缀 if (fileName.startsWith("svn_log_")) { fileName = fileName.substring(8); } // 去掉 .md 后缀 if (fileName.endsWith(".md")) { fileName = fileName.substring(0, fileName.length() - 3); } // 去掉时间戳部分 (格式: _YYYYMMDD_HHMMSS) int lastUnderscore = fileName.lastIndexOf('_'); if (lastUnderscore > 0) { // 检查是否是时间戳格式 String timestampPart = fileName.substring(lastUnderscore + 1); if (timestampPart.matches("\\d{8}_\\d{6}")) { fileName = fileName.substring(0, lastUnderscore); } } return fileName; } /** * 构建发送给DeepSeek的提示词 */ private static String buildPrompt(String markdownContent, String period) { return "你是一个专业的项目管理助手。请分析以下多个项目的SVN日志,并生成工作量统计数据。\n\n" + "日志内容包含多个项目,每个项目之间用 === 项目: xxx === 标识。\n" + "工作周期: " + period + "\n\n" + "SVN日志内容:\n" + markdownContent + "\n\n" + "请按照以下JSON格式返回工作量统计数据:\n" + "{\n" + " \"team\": \"所属班组\",\n" + " \"contact\": \"技术对接人\",\n" + " \"developer\": \"开发人员\",\n" + " \"period\": \"" + period + "\",\n" + " \"records\": [\n" + " {\n" + " \"sequence\": 1,\n" + " \"project\": \"项目1/项目2/项目3\",\n" + " \"content\": \"# 项目1\\n1.工作内容1\\n2.工作内容2\\n\\n# 项目2\\n1.工作内容1\\n2.工作内容2\\n\\n# 项目3\\n1.工作内容1\\n2.工作内容2\"\n" + " }\n" + " ]\n" + "}\n\n" + "重要要求:\n" + "1. 根据日志作者确定开发人员\n" + "2. 将所有项目的工作内容合并到一条记录中\n" + "3. 项目名称字段(project):使用 / 分隔多个项目,例如:\"PRS7050场站系统/PRS7950智能巡视现场问题/PRS7950电科院测试\"\n" + "4. 具体工作内容字段(content):使用 # 作为项目分类标识,格式为:\"# 项目名称\\n1.工作内容\\n2.工作内容\\n\\n# 下一个项目\\n1.工作内容\"\n" + "5. 不同项目之间用空行分隔\n" + "6. 只返回JSON,不要有其他文字\n" + "7. 提取具体工作内容,要详细和有条理\n" + "8. 项目名称要简洁明确,去掉多余的前缀和后缀"; } /** * 调用DeepSeek API(流式输出) */ private static String callDeepSeekAPI(String apiKey, String prompt) throws IOException { JSONObject requestBody = new JSONObject(); requestBody.put("model", "deepseek-chat"); // 创建消息对象,包含 role 和 content 字段 JSONObject messageObj = new JSONObject(); messageObj.put("role", "user"); messageObj.put("content", prompt); // 创建消息数组 com.google.gson.JsonArray messagesArray = new com.google.gson.JsonArray(); messagesArray.add(messageObj.jsonObject); requestBody.put("messages", messagesArray); requestBody.put("temperature", 0.7); requestBody.put("max_tokens", 4000); requestBody.put("stream", Optional.of(true)); // 启用流式输出 Request request = new Request.Builder() .url(DEEPSEEK_API_URL) .addHeader("Authorization", "Bearer " + apiKey) .addHeader("Content-Type", "application/json") .post(RequestBody.create(requestBody.toString(), MediaType.parse("application/json"))) .build(); StringBuilder fullResponse = new StringBuilder(); int chunkCount = 0; try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { System.err.println("API调用失败: " + response.code() + " " + response.message()); String errorResponse = response.body().string(); System.err.println("响应: " + errorResponse); return null; } // 读取流式响应 try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream()))) { String line; while ((line = reader.readLine()) != null) { if (line.startsWith("data: ")) { String data = line.substring(6); if (data.equals("[DONE]")) { break; } try { JSONObject chunk = new JSONObject(data); if (chunk.has("choices") && chunk.getJSONArray("choices").size() > 0) { JSONObject choice = chunk.getJSONArray("choices").get(0); if (choice.has("delta")) { JSONObject delta = choice.getJSONObject("delta"); if (delta.has("content")) { String content = delta.optString("content", ""); fullResponse.append(content); chunkCount++; // 实时打印处理进度 System.out.print(content); System.out.flush(); } } } } catch (Exception e) { // 忽略解析错误,继续处理下一行 } } } } } catch (Exception e) { System.err.println("API调用过程中发生异常: " + e.getMessage()); e.printStackTrace(); return null; } System.out.println(); // 换行 System.out.println("收到 " + chunkCount + " 个数据块"); if (fullResponse.length() == 0) { System.err.println("警告: 未收到任何响应内容"); } return fullResponse.toString(); } /** * 从响应中提取纯 JSON 内容 */ private static String extractJson(String response) { String trimmed = response.trim(); // 去除 ```json 标记 if (trimmed.startsWith("```json")) { trimmed = trimmed.substring(7); } else if (trimmed.startsWith("```")) { trimmed = trimmed.substring(3); } // 去除 ``` 结束标记 if (trimmed.endsWith("```")) { trimmed = trimmed.substring(0, trimmed.length() - 3); } return trimmed.trim(); } /** * 生成Excel文件 */ private static void generateExcel(String outputPath, String jsonResponse) throws IOException { // 提取纯 JSON 内容(去除 ```json 和 ``` 标记) String cleanJson = extractJson(jsonResponse); // 解析JSON响应 JSONObject data = new JSONObject(cleanJson); // 创建工作簿 Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("工作表1"); // 创建样式 CellStyle headerStyle = createHeaderStyle(workbook); CellStyle contentStyle = createContentStyle(workbook); CellStyle workContentStyle = createWorkContentStyle(workbook); // 创建表头(7列,与参考文件一致) Row headerRow = sheet.createRow(0); headerRow.setHeightInPoints(14.25f); // 表头行高 String[] headers = {"序号", "所属班组", "技术对接", "开发人员", "工作周期", "开发项目名称", "具体工作内容"}; for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 设置固定列宽(与参考文件一致) sheet.setColumnWidth(0, 2048); // 序号:8.00字符 sheet.setColumnWidth(1, 3328); // 所属班组:13.00字符 sheet.setColumnWidth(2, 4608); // 技术对接:18.00字符 sheet.setColumnWidth(3, 3840); // 开发人员:15.00字符 sheet.setColumnWidth(4, 5888); // 工作周期:23.00字符 sheet.setColumnWidth(5, 14080); // 开发项目名称:55.00字符 sheet.setColumnWidth(6, 43991); // 具体工作内容:171.84字符 // 获取记录 String team = data.optString("team", ""); String contact = data.optString("contact", ""); String developer = data.optString("developer", ""); String period = data.optString("period", ""); if (data.has("records")) { JSONArray recordsArray = data.getJSONArray("records"); int rowNum = 1; for (int i = 0; i < recordsArray.size(); i++) { JSONObject record = recordsArray.get(i); Row row = sheet.createRow(rowNum++); row.setHeightInPoints(16.50f); // 内容行高 // 序号 Cell cell0 = row.createCell(0); cell0.setCellValue(record.optDouble("sequence", i + 1)); cell0.setCellStyle(contentStyle); // 所属班组 Cell cell1 = row.createCell(1); cell1.setCellValue(team); cell1.setCellStyle(contentStyle); // 技术对接 Cell cell2 = row.createCell(2); cell2.setCellValue(contact); cell2.setCellStyle(contentStyle); // 开发人员 Cell cell3 = row.createCell(3); cell3.setCellValue(developer); cell3.setCellStyle(contentStyle); // 工作周期 Cell cell4 = row.createCell(4); cell4.setCellValue(period); cell4.setCellStyle(contentStyle); // 项目名称(多个项目用 / 分隔) Cell cell5 = row.createCell(5); cell5.setCellValue(record.optString("project", "")); cell5.setCellStyle(contentStyle); // 工作内容(支持换行,用 # 标识不同项目) Cell cell6 = row.createCell(6); cell6.setCellValue(record.optString("content", "")); cell6.setCellStyle(workContentStyle); // 使用工作内容样式 } } // 写入文件 try (FileOutputStream fos = new FileOutputStream(outputPath)) { workbook.write(fos); } workbook.close(); } /** * 创建表头样式 */ private static CellStyle createHeaderStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setFontName("SimSun"); // 字体名称:SimSun font.setFontHeightInPoints((short) 11); // 字体大小:11磅 font.setBold(false); // 不粗体 font.setColor(IndexedColors.BLACK.getIndex()); // 黑色 style.setFont(font); style.setAlignment(HorizontalAlignment.GENERAL); // 水平对齐:常规 style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直对齐:居中 style.setFillPattern(FillPatternType.NO_FILL); // 无填充 style.setBorderTop(BorderStyle.THIN); style.setBorderBottom(BorderStyle.THIN); style.setBorderLeft(BorderStyle.THIN); style.setBorderRight(BorderStyle.THIN); style.setTopBorderColor(IndexedColors.BLACK.getIndex()); style.setBottomBorderColor(IndexedColors.BLACK.getIndex()); style.setLeftBorderColor(IndexedColors.BLACK.getIndex()); style.setRightBorderColor(IndexedColors.BLACK.getIndex()); style.setWrapText(false); // 不换行 return style; } /** * 创建普通内容样式(列A-F) */ private static CellStyle createContentStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setFontName("宋体"); // 字体名称:宋体 font.setFontHeightInPoints((short) 11); // 字体大小:11磅 font.setBold(false); // 不粗体 style.setFont(font); style.setAlignment(HorizontalAlignment.GENERAL); // 水平对齐:常规 style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直对齐:居中 style.setFillPattern(FillPatternType.NO_FILL); // 无填充 style.setBorderTop(BorderStyle.THIN); style.setBorderBottom(BorderStyle.NONE); style.setBorderLeft(BorderStyle.NONE); style.setBorderRight(BorderStyle.NONE); style.setTopBorderColor(IndexedColors.BLACK.getIndex()); style.setWrapText(false); // 不换行 return style; } /** * 创建工作内容样式(列G) */ private static CellStyle createWorkContentStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setFontName("NSimSun"); // 字体名称:新宋体 font.setFontHeightInPoints((short) 14); // 字体大小:14磅 font.setBold(true); // 粗体 font.setColor(IndexedColors.BLACK.getIndex()); // 黑色 style.setFont(font); style.setAlignment(HorizontalAlignment.LEFT); // 水平对齐:左对齐 style.setVerticalAlignment(VerticalAlignment.TOP); // 垂直对齐:顶部 style.setFillForegroundColor(IndexedColors.YELLOW.getIndex()); // 黄色背景 style.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 实心填充 style.setBorderTop(BorderStyle.THIN); style.setBorderBottom(BorderStyle.NONE); style.setBorderLeft(BorderStyle.NONE); style.setBorderRight(BorderStyle.NONE); style.setTopBorderColor(IndexedColors.BLACK.getIndex()); style.setWrapText(true); // 自动换行 return style; } /** * 简单的JSON工具类 */ static class JSONObject { private final com.google.gson.JsonObject jsonObject; public JSONObject() { this.jsonObject = new com.google.gson.JsonObject(); } public JSONObject(String jsonString) { com.google.gson.Gson gson = new com.google.gson.Gson(); this.jsonObject = gson.fromJson(jsonString, com.google.gson.JsonObject.class); } public JSONObject(String key, String value) { this(); put(key, value); } public void put(String key, String value) { jsonObject.addProperty(key, value); } public void put(String key, int value) { jsonObject.addProperty(key, String.valueOf(value)); } public void put(String key, double value) { jsonObject.addProperty(key, String.valueOf(value)); } public void put(String key, Object value) { com.google.gson.Gson gson = new com.google.gson.Gson(); jsonObject.add(key, gson.toJsonTree(value)); } public String optString(String key, String defaultValue) { if (jsonObject.has(key) && !jsonObject.get(key).isJsonNull()) { return jsonObject.get(key).getAsString(); } return defaultValue; } public double optDouble(String key, double defaultValue) { if (jsonObject.has(key) && !jsonObject.get(key).isJsonNull()) { return jsonObject.get(key).getAsDouble(); } return defaultValue; } public boolean has(String key) { return jsonObject.has(key); } public JSONArray getJSONArray(String key) { return new JSONArray(jsonObject.get(key).getAsJsonArray()); } public JSONObject getJSONObject(String key) { return new JSONObject(jsonObject.get(key).getAsJsonObject().toString()); } @Override public String toString() { return jsonObject.toString(); } } /** * 简单的JSONArray工具类 */ static class JSONArray { private final com.google.gson.JsonArray jsonArray; public JSONArray(com.google.gson.JsonArray jsonArray) { this.jsonArray = jsonArray; } public int size() { return jsonArray.size(); } public JSONObject get(int index) { return new JSONObject(jsonArray.get(index).getAsJsonObject().toString()); } @SuppressWarnings("unchecked") public java.util.List toList() { java.util.List list = new ArrayList<>(); for (int i = 0; i < jsonArray.size(); i++) { list.add(get(i)); } return list; } } }