workspaceFiles;
+
+ @Data
+ public static class WorkspaceFileTemplate {
+ private String filename;
+ private String content;
+ private Boolean enabled;
+ private Integer sortOrder;
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java b/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java
new file mode 100644
index 00000000..cc5621cc
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java
@@ -0,0 +1,112 @@
+package vip.mate.agent.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import vip.mate.agent.AgentService;
+import vip.mate.agent.model.AgentEntity;
+import vip.mate.agent.model.TemplateDTO;
+import vip.mate.exception.MateClawException;
+import vip.mate.workspace.document.WorkspaceFileService;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Agent 模板服务
+ *
+ * 扫描 classpath:templates/*.json 下的模板文件,
+ * 支持列出模板和应用模板创建 Agent。
+ *
+ * @author MateClaw Team
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TemplateService {
+
+ private final AgentService agentService;
+ private final WorkspaceFileService workspaceFileService;
+ private final ObjectMapper objectMapper;
+
+ /**
+ * 列出所有可用模板
+ */
+ public List listTemplates() {
+ List templates = new ArrayList<>();
+ PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
+
+ try {
+ Resource[] resources = resolver.getResources("classpath:templates/*.json");
+ for (Resource resource : resources) {
+ try (InputStream is = resource.getInputStream()) {
+ TemplateDTO dto = objectMapper.readValue(is, TemplateDTO.class);
+ templates.add(dto);
+ } catch (IOException e) {
+ log.warn("Failed to parse template file: {}", resource.getFilename(), e);
+ }
+ }
+ } catch (IOException e) {
+ log.error("Failed to scan template files", e);
+ }
+
+ templates.sort(Comparator.comparing(TemplateDTO::getId));
+ return templates;
+ }
+
+ /**
+ * 应用模板创建 Agent 及其工作区文件
+ *
+ * @param templateId 模板 ID
+ * @return 创建的 AgentEntity
+ */
+ @Transactional
+ public AgentEntity applyTemplate(String templateId) {
+ TemplateDTO template = listTemplates().stream()
+ .filter(t -> t.getId().equals(templateId))
+ .findFirst()
+ .orElseThrow(() -> new MateClawException("模板不存在: " + templateId));
+
+ // 1. 创建 Agent
+ AgentEntity agent = new AgentEntity();
+ agent.setName(template.getName());
+ agent.setDescription(template.getDescription());
+ agent.setAgentType(template.getAgentType());
+ agent.setIcon(template.getIcon());
+ agent.setTags(template.getTags());
+ agent.setMaxIterations(template.getMaxIterations());
+ AgentEntity created = agentService.createAgent(agent);
+
+ // 2. 创建工作区文件
+ if (template.getWorkspaceFiles() != null && !template.getWorkspaceFiles().isEmpty()) {
+ for (TemplateDTO.WorkspaceFileTemplate wf : template.getWorkspaceFiles()) {
+ workspaceFileService.saveFile(created.getId(), wf.getFilename(), wf.getContent());
+ }
+
+ // 3. 启用指定文件并按 sortOrder 排序(setPromptFiles 用列表索引作为排序值)
+ List enabledFilenames = template.getWorkspaceFiles().stream()
+ .filter(wf -> Boolean.TRUE.equals(wf.getEnabled()))
+ .sorted((a, b) -> {
+ int sa = a.getSortOrder() != null ? a.getSortOrder() : 0;
+ int sb = b.getSortOrder() != null ? b.getSortOrder() : 0;
+ return Integer.compare(sa, sb);
+ })
+ .map(TemplateDTO.WorkspaceFileTemplate::getFilename)
+ .toList();
+
+ if (!enabledFilenames.isEmpty()) {
+ workspaceFileService.setPromptFiles(created.getId(), enabledFilenames);
+ }
+ }
+
+ return created;
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/system/controller/SetupController.java b/mateclaw-server/src/main/java/vip/mate/system/controller/SetupController.java
index 2bbc47b5..0e0f2b72 100644
--- a/mateclaw-server/src/main/java/vip/mate/system/controller/SetupController.java
+++ b/mateclaw-server/src/main/java/vip/mate/system/controller/SetupController.java
@@ -8,6 +8,12 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import vip.mate.common.result.R;
import vip.mate.config.DatabaseBootstrapRunner;
+import vip.mate.llm.service.ModelConfigService;
+import vip.mate.llm.service.ModelDiscoveryService;
+import vip.mate.llm.service.ModelProviderService;
+
+import java.util.List;
+import java.util.Map;
/**
* Setup API for first-run initialization.
@@ -23,6 +29,9 @@ import vip.mate.config.DatabaseBootstrapRunner;
public class SetupController {
private final DatabaseBootstrapRunner bootstrapRunner;
+ private final ModelConfigService modelConfigService;
+ private final ModelDiscoveryService modelDiscoveryService;
+ private final ModelProviderService modelProviderService;
/**
* Check whether the application has been initialized.
@@ -60,6 +69,39 @@ public class SetupController {
return R.ok("Initialized with " + language);
}
+ /**
+ * Onboarding status: whether the system has a usable model configured.
+ * Used by the frontend to decide whether to show the onboarding wizard.
+ */
+ @GetMapping("/onboarding-status")
+ public R