feat: v2 Vue3 frontend + multiple optimizations
- New Vue 3 + Vite frontend at /v2/ (OLED dark theme, Fira Sans/Code) - Date selector: support day/week/month range (backend unchanged) - SSE auto-reconnect (up to 3 retries) - Visibility polling pause (dashboard pauses when tab hidden) - Friendly Chinese HTTP error messages - Cancel task with confirmation in Dashboard - Split AiWorkflowService (1700->845 lines): - AiApiService: AI API calls + streaming - ExcelExportService: POI Excel generation - Dockerfile: 3-stage build (Node frontend -> Maven -> JRE) - WebApplication.java: System.out -> Logger - .gitignore: v2 build output, backup dirs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,19 +17,27 @@ class AiWorkflowServiceTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private AiApiService buildAiApiService(SettingsService settingsService) {
|
||||
return new AiApiService(settingsService);
|
||||
}
|
||||
|
||||
private ExcelExportService buildExcelExportService() {
|
||||
return new ExcelExportService();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveDeepSeekProviderByDefault() {
|
||||
final OutputFileService outputFileService = buildOutputFileService();
|
||||
final SettingsService settingsService = buildSettingsService(outputFileService);
|
||||
final AiApiService aiApiService = buildAiApiService(settingsService);
|
||||
final AiWorkflowService service = new AiWorkflowService(
|
||||
buildOutputFileService(),
|
||||
new SettingsService(
|
||||
buildOutputFileService(),
|
||||
new SettingsPersistenceService(),
|
||||
new SvnPresetService()
|
||||
),
|
||||
new AiInputValidator()
|
||||
outputFileService,
|
||||
new AiInputValidator(),
|
||||
aiApiService,
|
||||
buildExcelExportService()
|
||||
);
|
||||
|
||||
final AiWorkflowService.AiProviderContext context = service.resolveProviderContext(null);
|
||||
final AiApiService.AiProviderContext context = aiApiService.resolveProviderContext(null);
|
||||
|
||||
Assertions.assertEquals(SettingsService.PROVIDER_DEEPSEEK, context.getProvider());
|
||||
Assertions.assertEquals("deepseek-chat", context.getStageOneModel());
|
||||
@@ -39,11 +47,7 @@ class AiWorkflowServiceTest {
|
||||
@Test
|
||||
void shouldResolveOpenAiCompatibleModelsAndUrl() {
|
||||
final OutputFileService outputFileService = buildOutputFileService();
|
||||
final SettingsService settingsService = new SettingsService(
|
||||
outputFileService,
|
||||
new SettingsPersistenceService(),
|
||||
new SvnPresetService()
|
||||
);
|
||||
final SettingsService settingsService = buildSettingsService(outputFileService);
|
||||
settingsService.updateSettings(
|
||||
null,
|
||||
SettingsService.PROVIDER_OPENAI_COMPATIBLE,
|
||||
@@ -56,9 +60,12 @@ class AiWorkflowServiceTest {
|
||||
null,
|
||||
null
|
||||
);
|
||||
final AiWorkflowService service = new AiWorkflowService(outputFileService, settingsService, new AiInputValidator());
|
||||
final AiApiService aiApiService = buildAiApiService(settingsService);
|
||||
final AiWorkflowService service = new AiWorkflowService(
|
||||
outputFileService, new AiInputValidator(), aiApiService, buildExcelExportService()
|
||||
);
|
||||
|
||||
final AiWorkflowService.AiProviderContext context = service.resolveProviderContext(null);
|
||||
final AiApiService.AiProviderContext context = aiApiService.resolveProviderContext(null);
|
||||
|
||||
Assertions.assertEquals(SettingsService.PROVIDER_OPENAI_COMPATIBLE, context.getProvider());
|
||||
Assertions.assertEquals("http://127.0.0.1:5001/v1/chat/completions", context.getApiUrl());
|
||||
@@ -68,15 +75,14 @@ class AiWorkflowServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldFailFastWhenOpenAiCompatibleBaseUrlMissing() {
|
||||
final AiWorkflowService service = new AiWorkflowService(
|
||||
buildOutputFileService(),
|
||||
new StubSettingsService(buildOutputFileService(), " ", "sk-openai-test"),
|
||||
new AiInputValidator()
|
||||
final OutputFileService outputFileService = buildOutputFileService();
|
||||
final AiApiService aiApiService = buildAiApiService(
|
||||
new StubSettingsService(outputFileService, " ", "sk-openai-test")
|
||||
);
|
||||
|
||||
final IllegalStateException error = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> service.resolveProviderContext(null)
|
||||
() -> aiApiService.resolveProviderContext(null)
|
||||
);
|
||||
|
||||
Assertions.assertTrue(error.getMessage().contains("OpenAI兼容 Base URL"));
|
||||
@@ -85,11 +91,7 @@ class AiWorkflowServiceTest {
|
||||
@Test
|
||||
void shouldParseCompatibleStreamWhenOnlyContentIsReturned() throws Exception {
|
||||
final OutputFileService outputFileService = buildOutputFileService();
|
||||
final SettingsService settingsService = new SettingsService(
|
||||
outputFileService,
|
||||
new SettingsPersistenceService(),
|
||||
new SvnPresetService()
|
||||
);
|
||||
final SettingsService settingsService = buildSettingsService(outputFileService);
|
||||
settingsService.updateSettings(
|
||||
null,
|
||||
SettingsService.PROVIDER_OPENAI_COMPATIBLE,
|
||||
@@ -102,8 +104,8 @@ class AiWorkflowServiceTest {
|
||||
null,
|
||||
null
|
||||
);
|
||||
final AiWorkflowService service = new AiWorkflowService(outputFileService, settingsService, new AiInputValidator());
|
||||
final AiWorkflowService.AiProviderContext providerContext = service.resolveProviderContext(null);
|
||||
final AiApiService aiApiService = buildAiApiService(settingsService);
|
||||
final AiApiService.AiProviderContext providerContext = aiApiService.resolveProviderContext(null);
|
||||
final TaskContext taskContext = new TaskContext(buildTaskInfo(), null, null);
|
||||
final Buffer buffer = new Buffer()
|
||||
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"{\\\"items\\\":[\"}}]}\n")
|
||||
@@ -111,7 +113,7 @@ class AiWorkflowServiceTest {
|
||||
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"]}\"},\"finish_reason\":\"stop\"}]}\n")
|
||||
.writeUtf8("data: [DONE]\n");
|
||||
|
||||
final AiWorkflowService.AiStreamResult result = service.readStreamingResponse(
|
||||
final AiApiService.AiStreamResult result = aiApiService.readStreamingResponse(
|
||||
buffer,
|
||||
taskContext,
|
||||
providerContext,
|
||||
@@ -138,12 +140,30 @@ class AiWorkflowServiceTest {
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
private SettingsService buildSettingsService(OutputFileService outputFileService) {
|
||||
final RepositoryConfigService repositoryConfigService = buildRepositoryConfigService(outputFileService);
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
|
||||
svnPresetService.init();
|
||||
return new SettingsService(outputFileService, new SettingsPersistenceService(), svnPresetService, repositoryConfigService);
|
||||
}
|
||||
|
||||
private RepositoryConfigService buildRepositoryConfigService(OutputFileService outputFileService) {
|
||||
final RepositoryConfigService repositoryConfigService = new RepositoryConfigService(outputFileService);
|
||||
repositoryConfigService.init();
|
||||
return repositoryConfigService;
|
||||
}
|
||||
|
||||
private static final class StubSettingsService extends SettingsService {
|
||||
private final String openaiBaseUrl;
|
||||
private final String openaiApiKey;
|
||||
|
||||
private StubSettingsService(OutputFileService outputFileService, String openaiBaseUrl, String openaiApiKey) {
|
||||
super(outputFileService, new SettingsPersistenceService(), new SvnPresetService());
|
||||
super(
|
||||
outputFileService,
|
||||
new SettingsPersistenceService(),
|
||||
new SvnPresetService(new RepositoryConfigService(outputFileService)),
|
||||
new RepositoryConfigService(outputFileService)
|
||||
);
|
||||
this.openaiBaseUrl = openaiBaseUrl;
|
||||
this.openaiApiKey = openaiApiKey;
|
||||
}
|
||||
|
||||
@@ -26,10 +26,15 @@ class HealthServiceTest {
|
||||
void shouldReturnDetailedHealthAfterSettingsExpansion() throws Exception {
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
||||
final RepositoryConfigService repositoryConfigService = new RepositoryConfigService(outputFileService);
|
||||
repositoryConfigService.init();
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
|
||||
svnPresetService.init();
|
||||
final SettingsService settingsService = new SettingsService(
|
||||
outputFileService,
|
||||
new SettingsPersistenceService(),
|
||||
new SvnPresetService()
|
||||
svnPresetService,
|
||||
repositoryConfigService
|
||||
);
|
||||
taskService = new TaskService(new TaskPersistenceService(), outputFileService);
|
||||
final HealthService healthService = new HealthService(outputFileService, settingsService, taskService);
|
||||
|
||||
@@ -9,11 +9,15 @@ import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import com.svnlog.web.model.RepositoryConfig;
|
||||
import com.svnlog.web.util.CryptoUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
@@ -195,12 +199,58 @@ class SettingsServiceTest {
|
||||
assertNotNull(settings.get("apiKeySource"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveSvnCredentialsFromRepositoryConfigWhenSettingsAreEmpty() throws IOException {
|
||||
useTempWorkingDirectory();
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
||||
writeRepositoryConfig(outputFileService.getOutputRoot().resolve("repository-configs.json"));
|
||||
|
||||
final RepositoryConfigService repositoryConfigService = new RepositoryConfigService(outputFileService);
|
||||
repositoryConfigService.init();
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
|
||||
svnPresetService.init();
|
||||
final SettingsService settingsService = new SettingsService(
|
||||
outputFileService,
|
||||
new SettingsPersistenceService(),
|
||||
svnPresetService,
|
||||
repositoryConfigService
|
||||
);
|
||||
|
||||
final SettingsService.SvnCredentials credentials =
|
||||
settingsService.resolveSvnCredentials(null, null, "preset-json");
|
||||
|
||||
assertEquals("json-user", credentials.getUsername());
|
||||
assertEquals("json-pass", credentials.getPassword());
|
||||
assertTrue(svnPresetService.containsPresetId("preset-json"));
|
||||
assertEquals("JSON SVN", svnPresetService.getById("preset-json").getName());
|
||||
}
|
||||
|
||||
private SettingsService newSettingsService() {
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
||||
final SettingsPersistenceService settingsPersistenceService = new SettingsPersistenceService();
|
||||
final SvnPresetService svnPresetService = new SvnPresetService();
|
||||
return new SettingsService(outputFileService, settingsPersistenceService, svnPresetService);
|
||||
final RepositoryConfigService repositoryConfigService = new RepositoryConfigService(outputFileService);
|
||||
repositoryConfigService.init();
|
||||
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
|
||||
svnPresetService.init();
|
||||
return new SettingsService(outputFileService, settingsPersistenceService, svnPresetService, repositoryConfigService);
|
||||
}
|
||||
|
||||
private void writeRepositoryConfig(Path configPath) throws IOException {
|
||||
final RepositoryConfig config = new RepositoryConfig();
|
||||
config.setId("preset-json");
|
||||
config.setName("JSON SVN");
|
||||
config.setType("SVN");
|
||||
config.setEnabled(true);
|
||||
config.setSvnUrl("https://example.invalid/svn/project");
|
||||
config.setSvnUsername("json-user");
|
||||
config.setSvnPasswordEncrypted(CryptoUtils.encrypt("json-pass"));
|
||||
|
||||
Files.createDirectories(configPath.getParent());
|
||||
final String json = new GsonBuilder().setPrettyPrinting().create()
|
||||
.toJson(java.util.Collections.singletonList(config));
|
||||
Files.write(configPath, json.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private void useTempWorkingDirectory() {
|
||||
|
||||
Reference in New Issue
Block a user