1b182c2930
- 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>
187 lines
7.3 KiB
Java
187 lines
7.3 KiB
Java
package com.svnlog.web.service;
|
|
|
|
import java.nio.file.Path;
|
|
import java.time.Instant;
|
|
|
|
import org.junit.jupiter.api.Assertions;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.io.TempDir;
|
|
|
|
import com.svnlog.web.model.TaskInfo;
|
|
import com.svnlog.web.model.TaskStatus;
|
|
|
|
import okio.Buffer;
|
|
|
|
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(
|
|
outputFileService,
|
|
new AiInputValidator(),
|
|
aiApiService,
|
|
buildExcelExportService()
|
|
);
|
|
|
|
final AiApiService.AiProviderContext context = aiApiService.resolveProviderContext(null);
|
|
|
|
Assertions.assertEquals(SettingsService.PROVIDER_DEEPSEEK, context.getProvider());
|
|
Assertions.assertEquals("deepseek-chat", context.getStageOneModel());
|
|
Assertions.assertEquals("deepseek-reasoner", context.getStageTwoModel());
|
|
}
|
|
|
|
@Test
|
|
void shouldResolveOpenAiCompatibleModelsAndUrl() {
|
|
final OutputFileService outputFileService = buildOutputFileService();
|
|
final SettingsService settingsService = buildSettingsService(outputFileService);
|
|
settingsService.updateSettings(
|
|
null,
|
|
SettingsService.PROVIDER_OPENAI_COMPATIBLE,
|
|
"http://127.0.0.1:5001/v1/",
|
|
"sk-openai-test",
|
|
"deepseek-v4-flash",
|
|
"deepseek-v4-pro",
|
|
null,
|
|
null,
|
|
null,
|
|
null
|
|
);
|
|
final AiApiService aiApiService = buildAiApiService(settingsService);
|
|
final AiWorkflowService service = new AiWorkflowService(
|
|
outputFileService, new AiInputValidator(), aiApiService, buildExcelExportService()
|
|
);
|
|
|
|
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());
|
|
Assertions.assertEquals("deepseek-v4-flash", context.getStageOneModel());
|
|
Assertions.assertEquals("deepseek-v4-pro", context.getStageTwoModel());
|
|
}
|
|
|
|
@Test
|
|
void shouldFailFastWhenOpenAiCompatibleBaseUrlMissing() {
|
|
final OutputFileService outputFileService = buildOutputFileService();
|
|
final AiApiService aiApiService = buildAiApiService(
|
|
new StubSettingsService(outputFileService, " ", "sk-openai-test")
|
|
);
|
|
|
|
final IllegalStateException error = Assertions.assertThrows(
|
|
IllegalStateException.class,
|
|
() -> aiApiService.resolveProviderContext(null)
|
|
);
|
|
|
|
Assertions.assertTrue(error.getMessage().contains("OpenAI兼容 Base URL"));
|
|
}
|
|
|
|
@Test
|
|
void shouldParseCompatibleStreamWhenOnlyContentIsReturned() throws Exception {
|
|
final OutputFileService outputFileService = buildOutputFileService();
|
|
final SettingsService settingsService = buildSettingsService(outputFileService);
|
|
settingsService.updateSettings(
|
|
null,
|
|
SettingsService.PROVIDER_OPENAI_COMPATIBLE,
|
|
"http://127.0.0.1:5001/v1",
|
|
"sk-openai-test",
|
|
"deepseek-v4-flash",
|
|
"deepseek-v4-pro",
|
|
null,
|
|
null,
|
|
null,
|
|
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")
|
|
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"{\\\"summary\\\":\\\"done\\\"}\"}}]}\n")
|
|
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"]}\"},\"finish_reason\":\"stop\"}]}\n")
|
|
.writeUtf8("data: [DONE]\n");
|
|
|
|
final AiApiService.AiStreamResult result = aiApiService.readStreamingResponse(
|
|
buffer,
|
|
taskContext,
|
|
providerContext,
|
|
"deepseek-v4-flash",
|
|
8000
|
|
);
|
|
|
|
Assertions.assertEquals("{\"items\":[{\"summary\":\"done\"}]}", result.getAnswer());
|
|
Assertions.assertEquals("stop", result.getFinishReason());
|
|
}
|
|
|
|
private OutputFileService buildOutputFileService() {
|
|
final OutputFileService outputFileService = new OutputFileService();
|
|
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
|
return outputFileService;
|
|
}
|
|
|
|
private TaskInfo buildTaskInfo() {
|
|
final TaskInfo taskInfo = new TaskInfo();
|
|
taskInfo.setTaskId("task-1");
|
|
taskInfo.setStatus(TaskStatus.RUNNING);
|
|
taskInfo.setCreatedAt(Instant.now());
|
|
taskInfo.setUpdatedAt(Instant.now());
|
|
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(new RepositoryConfigService(outputFileService)),
|
|
new RepositoryConfigService(outputFileService)
|
|
);
|
|
this.openaiBaseUrl = openaiBaseUrl;
|
|
this.openaiApiKey = openaiApiKey;
|
|
}
|
|
|
|
@Override
|
|
public String getProvider() {
|
|
return SettingsService.PROVIDER_OPENAI_COMPATIBLE;
|
|
}
|
|
|
|
@Override
|
|
public String getOpenaiBaseUrl() {
|
|
return openaiBaseUrl;
|
|
}
|
|
|
|
@Override
|
|
public String getOpenaiApiKey() {
|
|
return openaiApiKey;
|
|
}
|
|
}
|
|
}
|