feat(web): 增强任务治理与系统诊断能力

新增任务持久化、筛选分页、取消任务、健康检查与 AI 输入校验,并完善前端历史管理交互与容错重试机制。补充对应单元测试,提升系统稳定性和可运维性。
This commit is contained in:
2026-03-08 23:35:36 +08:00
parent e26fb9cebb
commit bdf6367404
21 changed files with 1049 additions and 34 deletions

View File

@@ -51,15 +51,22 @@ public class AiWorkflowService {
private final OutputFileService outputFileService;
private final SettingsService settingsService;
private final AiInputValidator aiInputValidator;
private final RetrySupport retrySupport = new RetrySupport();
public AiWorkflowService(OutputFileService outputFileService, SettingsService settingsService) {
public AiWorkflowService(OutputFileService outputFileService,
SettingsService settingsService,
AiInputValidator aiInputValidator) {
this.outputFileService = outputFileService;
this.settingsService = settingsService;
this.aiInputValidator = aiInputValidator;
}
public TaskResult analyzeAndExport(AiAnalyzeRequest request, TaskContext context) throws Exception {
context.setProgress(10, "正在读取 Markdown 文件");
final String content = readMarkdownFiles(request.getFilePaths());
final List<Path> markdownFiles = resolveUserFiles(request.getFilePaths());
aiInputValidator.validate(markdownFiles);
final String content = readMarkdownFiles(markdownFiles);
context.setProgress(35, "正在请求 DeepSeek 分析");
final String period = request.getPeriod() != null && !request.getPeriod().trim().isEmpty()
@@ -88,10 +95,9 @@ public class AiWorkflowService {
return result;
}
private String readMarkdownFiles(List<String> filePaths) throws IOException {
private String readMarkdownFiles(List<Path> filePaths) throws IOException {
final StringBuilder builder = new StringBuilder();
for (String filePath : filePaths) {
final Path path = resolveUserFile(filePath);
for (Path path : filePaths) {
final String content = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
builder.append("\n\n=== 文件: ").append(path.getFileName().toString()).append(" ===\n");
builder.append(content);
@@ -99,6 +105,17 @@ public class AiWorkflowService {
return builder.toString();
}
private List<Path> resolveUserFiles(List<String> userPaths) throws IOException {
java.util.ArrayList<Path> files = new java.util.ArrayList<Path>();
if (userPaths == null) {
return files;
}
for (String userPath : userPaths) {
files.add(resolveUserFile(userPath));
}
return files;
}
private Path resolveUserFile(String userPath) throws IOException {
if (userPath == null || userPath.trim().isEmpty()) {
throw new IllegalArgumentException("文件路径不能为空");
@@ -135,6 +152,16 @@ public class AiWorkflowService {
}
private String callDeepSeek(String apiKey, String prompt) throws IOException {
try {
return retrySupport.execute(() -> callDeepSeekOnce(apiKey, prompt), 3, 1000L);
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
private String callDeepSeekOnce(String apiKey, String prompt) throws Exception {
final JsonObject message = new JsonObject();
message.addProperty("role", "user");
message.addProperty("content", prompt);
@@ -161,10 +188,14 @@ public class AiWorkflowService {
if (response.body() != null) {
errorBody = response.body().string();
}
throw new IllegalStateException("DeepSeek API 调用失败: " + response.code() + " " + errorBody);
String detail = "DeepSeek API 调用失败: " + response.code() + " " + errorBody;
if (response.code() == 429 || response.code() >= 500) {
throw new RetrySupport.RetryableException(detail);
}
throw new IllegalStateException(detail);
}
if (response.body() == null) {
throw new IllegalStateException("DeepSeek API 返回空响应体");
throw new RetrySupport.RetryableException("DeepSeek API 返回空响应体");
}
final String raw = response.body().string();