feat: 完善 Prompt 管理与生成流程配置
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
package com.ruoyi.generator.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "factory.prompt-registry")
|
||||
public class PromptRegistryProperties
|
||||
{
|
||||
private Boolean databaseEnabled = false;
|
||||
private Boolean builtinFallbackEnabled = false;
|
||||
|
||||
public Boolean getDatabaseEnabled() { return databaseEnabled; }
|
||||
public void setDatabaseEnabled(Boolean databaseEnabled) { this.databaseEnabled = databaseEnabled; }
|
||||
public boolean isDatabaseEnabled() { return Boolean.TRUE.equals(databaseEnabled); }
|
||||
public Boolean getBuiltinFallbackEnabled() { return builtinFallbackEnabled; }
|
||||
public void setBuiltinFallbackEnabled(Boolean builtinFallbackEnabled) { this.builtinFallbackEnabled = builtinFallbackEnabled; }
|
||||
public boolean isBuiltinFallbackEnabled() { return Boolean.TRUE.equals(builtinFallbackEnabled); }
|
||||
}
|
||||
@@ -91,12 +91,4 @@ public class PromptTemplateController extends BaseController
|
||||
promptVersionId, getUsername()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:prompt:version')")
|
||||
@Log(title = "Prompt V1 初始化", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/initialize-v1")
|
||||
public AjaxResult initializeV1()
|
||||
{
|
||||
int count = promptTemplateService.initializeBuiltinV1(getUsername());
|
||||
return AjaxResult.success("已初始化或修复 " + count + " 个 Prompt V1", count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
package com.ruoyi.generator.factory.ai.prompt;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
|
||||
@Service
|
||||
public class BuiltinPromptRegistry implements PromptRegistry
|
||||
{
|
||||
public static final String DEFAULT_SYSTEM_PROMPT = load("default", "system");
|
||||
public static final String REQUIREMENT_SYSTEM_PROMPT = load("requirement", "system");
|
||||
public static final String REQUIREMENT_USER_PROMPT_TEMPLATE = load("requirement", "user");
|
||||
public static final String APP_BLUEPRINT_SYSTEM_PROMPT = load("app_blueprint", "system");
|
||||
public static final String APP_BLUEPRINT_USER_PROMPT_TEMPLATE = load("app_blueprint", "user");
|
||||
public static final String FLOW_CONFIG_SYSTEM_PROMPT = load("flow_config", "system");
|
||||
public static final String FLOW_CONFIG_USER_PROMPT_TEMPLATE = load("flow_config", "user");
|
||||
public static final String DATABASE_SYSTEM_PROMPT = load("database", "system");
|
||||
public static final String DATABASE_USER_PROMPT_TEMPLATE = load("database", "user");
|
||||
public static final String DIAGRAM_SYSTEM_PROMPT = load("diagram_spec", "system");
|
||||
public static final String DIAGRAM_USER_PROMPT_TEMPLATE = load("diagram_spec", "user");
|
||||
public static final String PAPER_OUTLINE_SYSTEM_PROMPT = load("paper_outline", "system");
|
||||
public static final String PAPER_OUTLINE_USER_PROMPT_TEMPLATE = load("paper_outline", "user");
|
||||
public static final String PAPER_DRAFT_SYSTEM_PROMPT = load("paper_draft", "system");
|
||||
public static final String PAPER_DRAFT_USER_PROMPT_TEMPLATE = load("paper_draft", "user");
|
||||
public static final String CODE_ANALYSIS_SYSTEM_PROMPT = load("code_analysis", "system");
|
||||
public static final String CODE_ANALYSIS_USER_PROMPT_TEMPLATE = load("code_analysis", "user");
|
||||
public static final String DATABASE_CHANGE_SYNC_SYSTEM_PROMPT = load("database_change_sync", "system");
|
||||
public static final String DATABASE_CHANGE_SYNC_USER_PROMPT_TEMPLATE = load("database_change_sync", "user");
|
||||
public static final String ONE_CLICK_PROJECT_SYSTEM_PROMPT = load("one_click_project", "system");
|
||||
public static final String ONE_CLICK_PROJECT_USER_PROMPT_TEMPLATE = load("one_click_project", "user");
|
||||
public static final String VERSION = "1.0.0";
|
||||
public static final String PIPELINE_RELEASE_CODE = "builtin-one-click-1.0.0";
|
||||
|
||||
private final Map<String, PromptRelease> byGenerateType;
|
||||
private final Map<String, PromptRelease> byRelease;
|
||||
|
||||
@Autowired
|
||||
public BuiltinPromptRegistry(PromptFingerprintService fingerprintService)
|
||||
{
|
||||
Map<String, PromptRelease> types = new LinkedHashMap<String, PromptRelease>();
|
||||
register(types, release(fingerprintService, "requirement", REQUIREMENT_SYSTEM_PROMPT,
|
||||
"context-json:requirement:v1", REQUIREMENT_USER_PROMPT_TEMPLATE));
|
||||
register(types, release(fingerprintService, "app_blueprint", APP_BLUEPRINT_SYSTEM_PROMPT,
|
||||
"context-json:app_blueprint:v1", APP_BLUEPRINT_USER_PROMPT_TEMPLATE));
|
||||
register(types, release(fingerprintService, "flow_config", FLOW_CONFIG_SYSTEM_PROMPT,
|
||||
"context-json:flow_config:v1", FLOW_CONFIG_USER_PROMPT_TEMPLATE));
|
||||
register(types, release(fingerprintService, "database", DATABASE_SYSTEM_PROMPT,
|
||||
"context-json:database:v1", DATABASE_USER_PROMPT_TEMPLATE));
|
||||
register(types, release(fingerprintService, "diagram_spec", DIAGRAM_SYSTEM_PROMPT,
|
||||
"context-json:diagram_spec:v1", DIAGRAM_USER_PROMPT_TEMPLATE));
|
||||
register(types, release(fingerprintService, "paper_outline", PAPER_OUTLINE_SYSTEM_PROMPT,
|
||||
"context-json:paper_outline:v1", PAPER_OUTLINE_USER_PROMPT_TEMPLATE));
|
||||
register(types, release(fingerprintService, "paper_draft", PAPER_DRAFT_SYSTEM_PROMPT,
|
||||
"context-json:paper_draft:v1", PAPER_DRAFT_USER_PROMPT_TEMPLATE));
|
||||
register(types, release(fingerprintService, "code_analysis", CODE_ANALYSIS_SYSTEM_PROMPT,
|
||||
"context-json:code_analysis:v1", CODE_ANALYSIS_USER_PROMPT_TEMPLATE));
|
||||
register(types, release(fingerprintService, "database_change_sync", DATABASE_CHANGE_SYNC_SYSTEM_PROMPT,
|
||||
"context-json:database_change_sync:v1", DATABASE_CHANGE_SYNC_USER_PROMPT_TEMPLATE));
|
||||
register(types, pipelineRelease(fingerprintService));
|
||||
byGenerateType = Collections.unmodifiableMap(types);
|
||||
|
||||
Map<String, PromptRelease> releases = new LinkedHashMap<String, PromptRelease>();
|
||||
for (PromptRelease item : types.values())
|
||||
{
|
||||
releases.put(key(item.getPromptCode(), item.getVersion()), item);
|
||||
}
|
||||
byRelease = Collections.unmodifiableMap(releases);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PromptRelease resolve(String generateType)
|
||||
{
|
||||
PromptRelease release = byGenerateType.get(StringUtils.defaultString(generateType));
|
||||
if (release == null)
|
||||
{
|
||||
throw new ServiceException("No published Prompt release for generation type: "
|
||||
+ StringUtils.defaultString(generateType));
|
||||
}
|
||||
return release;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PromptRelease resolve(String promptCode, String version)
|
||||
{
|
||||
PromptRelease release = byRelease.get(key(promptCode, version));
|
||||
if (release == null)
|
||||
{
|
||||
throw new ServiceException("Prompt release does not exist: "
|
||||
+ StringUtils.defaultString(promptCode) + "@" + StringUtils.defaultString(version));
|
||||
}
|
||||
return release;
|
||||
}
|
||||
|
||||
public String managementSystemPrompt(String generateType)
|
||||
{
|
||||
return resolve(generateType).getSystemPrompt();
|
||||
}
|
||||
|
||||
public String managementUserPromptTemplate(String generateType)
|
||||
{
|
||||
return resolve(generateType).getUserPromptTemplate();
|
||||
}
|
||||
|
||||
private PromptRelease pipelineRelease(PromptFingerprintService fingerprintService)
|
||||
{
|
||||
return release(fingerprintService, "one_click_project", ONE_CLICK_PROJECT_SYSTEM_PROMPT,
|
||||
"pipeline:app_blueprint,flow_config,database,generate,preview:v2",
|
||||
ONE_CLICK_PROJECT_USER_PROMPT_TEMPLATE);
|
||||
}
|
||||
|
||||
private PromptRelease release(PromptFingerprintService fingerprintService,
|
||||
String generateType, String systemPrompt, String userPromptContract,
|
||||
String userPromptTemplate)
|
||||
{
|
||||
PromptRelease release = new PromptRelease();
|
||||
release.setGenerateType(generateType);
|
||||
release.setPromptCode("front." + generateType);
|
||||
release.setVersion(VERSION);
|
||||
release.setSystemPrompt(systemPrompt);
|
||||
release.setUserPromptContract(userPromptContract);
|
||||
release.setUserPromptTemplate(userPromptTemplate);
|
||||
release.setProviderCode("deepseek");
|
||||
release.setPipelineReleaseCode(PIPELINE_RELEASE_CODE);
|
||||
|
||||
release.setFingerprint(fingerprintService.fingerprint(release.getGenerateType(),
|
||||
release.getPromptCode(), release.getVersion(), release.getSystemPrompt(),
|
||||
release.getUserPromptContract(), release.getUserPromptTemplate()));
|
||||
return release;
|
||||
}
|
||||
|
||||
private void register(Map<String, PromptRelease> releases, PromptRelease release)
|
||||
{
|
||||
if (releases.put(release.getGenerateType(), release) != null)
|
||||
{
|
||||
throw new ServiceException("Duplicate built-in Prompt generation type: " + release.getGenerateType());
|
||||
}
|
||||
}
|
||||
|
||||
private String key(String promptCode, String version)
|
||||
{
|
||||
return StringUtils.defaultString(promptCode) + "@" + StringUtils.defaultString(version);
|
||||
}
|
||||
|
||||
private static String load(String generateType, String role)
|
||||
{
|
||||
String path = "/factory-prompts/" + generateType + "/" + role + ".txt";
|
||||
try (InputStream input = BuiltinPromptRegistry.class.getResourceAsStream(path))
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
throw new ServiceException("Built-in Prompt resource does not exist: " + path);
|
||||
}
|
||||
return StreamUtils.copyToString(input, StandardCharsets.UTF_8).trim();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new ServiceException("Unable to read built-in Prompt resource: " + path)
|
||||
.setDetailMessage(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.config.PromptRegistryProperties;
|
||||
import com.ruoyi.generator.factory.ai.prompt.model.PromptVersion;
|
||||
import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
|
||||
|
||||
@@ -14,33 +13,23 @@ import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
|
||||
public class DatabasePromptRegistry implements PromptRegistry
|
||||
{
|
||||
private final PromptRegistryMapper mapper;
|
||||
private final BuiltinPromptRegistry builtinRegistry;
|
||||
private final PromptFingerprintService fingerprintService;
|
||||
private final PromptRegistryProperties properties;
|
||||
|
||||
@Autowired
|
||||
public DatabasePromptRegistry(PromptRegistryMapper mapper,
|
||||
BuiltinPromptRegistry builtinRegistry,
|
||||
PromptFingerprintService fingerprintService,
|
||||
PromptRegistryProperties properties)
|
||||
PromptFingerprintService fingerprintService)
|
||||
{
|
||||
this.mapper = mapper;
|
||||
this.builtinRegistry = builtinRegistry;
|
||||
this.fingerprintService = fingerprintService;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PromptRelease resolve(String generateType)
|
||||
{
|
||||
if (!properties.isDatabaseEnabled())
|
||||
{
|
||||
return builtinRegistry.resolve(generateType);
|
||||
}
|
||||
PromptVersion version = mapper.selectPublishedByGenerateType(generateType);
|
||||
if (version == null)
|
||||
{
|
||||
return missingDatabaseRelease(generateType, null);
|
||||
throw missingDatabaseRelease(generateType, null);
|
||||
}
|
||||
return toRelease(version);
|
||||
}
|
||||
@@ -48,27 +37,16 @@ public class DatabasePromptRegistry implements PromptRegistry
|
||||
@Override
|
||||
public PromptRelease resolve(String promptCode, String version)
|
||||
{
|
||||
if (!properties.isDatabaseEnabled())
|
||||
{
|
||||
return builtinRegistry.resolve(promptCode, version);
|
||||
}
|
||||
PromptVersion stored = mapper.selectExecutableByCodeAndVersion(promptCode, version);
|
||||
if (stored == null)
|
||||
{
|
||||
return missingDatabaseRelease(promptCode, version);
|
||||
throw missingDatabaseRelease(promptCode, version);
|
||||
}
|
||||
return toRelease(stored);
|
||||
}
|
||||
|
||||
private PromptRelease toRelease(PromptVersion version)
|
||||
{
|
||||
PromptRelease supported = builtinRegistry.resolve(version.getGenerateType());
|
||||
if (!StringUtils.equals(supported.getUserPromptContract(),
|
||||
version.getUserPromptContract()))
|
||||
{
|
||||
throw new ServiceException("Stored User Prompt contract is not implemented: "
|
||||
+ version.getUserPromptContract());
|
||||
}
|
||||
String actualHash = fingerprintService.fingerprint(version.getGenerateType(),
|
||||
version.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
|
||||
version.getUserPromptContract(), version.getUserPromptTemplate());
|
||||
@@ -91,15 +69,10 @@ public class DatabasePromptRegistry implements PromptRegistry
|
||||
return release;
|
||||
}
|
||||
|
||||
private PromptRelease missingDatabaseRelease(String codeOrType, String version)
|
||||
private ServiceException missingDatabaseRelease(String codeOrType, String version)
|
||||
{
|
||||
if (properties.isBuiltinFallbackEnabled())
|
||||
{
|
||||
return version == null ? builtinRegistry.resolve(codeOrType)
|
||||
: builtinRegistry.resolve(codeOrType, version);
|
||||
}
|
||||
String identity = version == null ? StringUtils.defaultString(codeOrType)
|
||||
: StringUtils.defaultString(codeOrType) + "@" + StringUtils.defaultString(version);
|
||||
throw new ServiceException("Database Prompt release does not exist: " + identity);
|
||||
return new ServiceException("Published managed Prompt release does not exist: " + identity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ package com.ruoyi.generator.factory.ai.prompt.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.ruoyi.generator.factory.ai.prompt.PromptRelease;
|
||||
|
||||
public class PromptTemplateDetail
|
||||
{
|
||||
private PromptTemplate template;
|
||||
private List<PromptVersion> versions = new ArrayList<PromptVersion>();
|
||||
private PromptRelease builtinRelease;
|
||||
|
||||
public PromptTemplate getTemplate() { return template; }
|
||||
public void setTemplate(PromptTemplate template) { this.template = template; }
|
||||
@@ -17,6 +15,4 @@ public class PromptTemplateDetail
|
||||
{
|
||||
this.versions = versions == null ? new ArrayList<PromptVersion>() : versions;
|
||||
}
|
||||
public PromptRelease getBuiltinRelease() { return builtinRelease; }
|
||||
public void setBuiltinRelease(PromptRelease builtinRelease) { this.builtinRelease = builtinRelease; }
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateFlowConfigRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationResult;
|
||||
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
|
||||
import com.ruoyi.generator.service.front.FlowConfigPageBindingReconciler;
|
||||
import com.ruoyi.generator.service.front.IAiGenerateService;
|
||||
|
||||
/** One visible design step: application blueprint plus the small flow_config. */
|
||||
@@ -16,11 +17,14 @@ import com.ruoyi.generator.service.front.IAiGenerateService;
|
||||
public class DesignStageHandler implements OneClickGenerationStageHandler
|
||||
{
|
||||
private final IAiGenerateService aiGenerateService;
|
||||
private final FlowConfigPageBindingReconciler pageBindingReconciler;
|
||||
|
||||
@Autowired
|
||||
public DesignStageHandler(IAiGenerateService aiGenerateService)
|
||||
public DesignStageHandler(IAiGenerateService aiGenerateService,
|
||||
FlowConfigPageBindingReconciler pageBindingReconciler)
|
||||
{
|
||||
this.aiGenerateService = aiGenerateService;
|
||||
this.pageBindingReconciler = pageBindingReconciler;
|
||||
}
|
||||
|
||||
public String stageCode() { return OneClickProjectGenerationResult.STAGE_DESIGN; }
|
||||
@@ -49,6 +53,8 @@ public class DesignStageHandler implements OneClickGenerationStageHandler
|
||||
flowRequest.setAppBlueprint(context.getAppBlueprintJson());
|
||||
SimpleFlowConfig flowConfig = aiGenerateService.generateFlowConfig(context.getTask().getUserId(),
|
||||
context.getTask().getProjectId(), flowRequest);
|
||||
pageBindingReconciler.reconcile(flowConfig, app);
|
||||
context.setAppBlueprintJson(JSON.toJSONString(app));
|
||||
context.setFlowConfig(flowConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,51 @@ public class FlowConfigSanitizer
|
||||
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
|
||||
{
|
||||
repairMetricStatuses(flow);
|
||||
repairSelfUpdateRelationFields(flow);
|
||||
repairEffectValues(flow);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* An update_related effect aimed at its owner row still needs the owner's
|
||||
* primary-key field so the renderer can reload that row. AI responses often
|
||||
* omit it because no cross-table relationship is involved. Infer it only
|
||||
* from the action's request fields and only when target and owner are the
|
||||
* same table; cross-table relationships must remain explicit.
|
||||
*/
|
||||
private void repairSelfUpdateRelationFields(SimpleFlowConfig config)
|
||||
{
|
||||
for (SimpleFlowConfig.Action action : config.getActions())
|
||||
{
|
||||
if (action == null) continue;
|
||||
String ownerTable = StringUtils.defaultIfBlank(action.getOwnerTable(), config.getMainTable());
|
||||
for (SimpleFlowConfig.Effect effect : action.getEffects())
|
||||
{
|
||||
if (effect == null || !"update_related".equals(effect.getType())
|
||||
|| StringUtils.isNotBlank(effect.getRelationField())
|
||||
|| !StringUtils.equals(ownerTable, effect.getTable()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String relationField = ownerRequestId(action);
|
||||
if (StringUtils.isNotBlank(relationField)) effect.setRelationField(relationField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String ownerRequestId(SimpleFlowConfig.Action action)
|
||||
{
|
||||
List<String> requestFields = action.getRequestFields();
|
||||
if (requestFields == null || requestFields.isEmpty()) return null;
|
||||
for (String requestField : requestFields)
|
||||
{
|
||||
if ("id".equals(requestField)) return requestField;
|
||||
}
|
||||
if (requestFields.size() != 1) return null;
|
||||
return StringUtils.trim(requestFields.get(0));
|
||||
}
|
||||
|
||||
private void repairEffectValues(SimpleFlowConfig config)
|
||||
{
|
||||
for (SimpleFlowConfig.Action action : config.getActions())
|
||||
|
||||
@@ -32,8 +32,6 @@ public interface PromptRegistryMapper
|
||||
|
||||
int insertPromptVersion(PromptVersion version);
|
||||
|
||||
int updateInitializedV1(PromptVersion version);
|
||||
|
||||
int retirePublishedVersions(@Param("promptTemplateId") Long promptTemplateId,
|
||||
@Param("excludeVersionId") Long excludeVersionId);
|
||||
|
||||
|
||||
@@ -20,6 +20,4 @@ public interface IPromptTemplateService
|
||||
int publishPromptVersion(Long promptTemplateId, Long promptVersionId, String operator);
|
||||
|
||||
int rollbackPromptVersion(Long promptTemplateId, Long promptVersionId, String operator);
|
||||
|
||||
int initializeBuiltinV1(String operator);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.factory.ai.model.ModelGatewayRouter;
|
||||
import com.ruoyi.generator.factory.ai.prompt.PromptFingerprintService;
|
||||
import com.ruoyi.generator.factory.ai.prompt.BuiltinPromptRegistry;
|
||||
import com.ruoyi.generator.factory.ai.prompt.PromptRelease;
|
||||
import com.ruoyi.generator.factory.ai.prompt.PromptTemplateRenderer;
|
||||
import com.ruoyi.generator.factory.ai.prompt.model.PromptTemplate;
|
||||
import com.ruoyi.generator.factory.ai.prompt.model.PromptTemplateDetail;
|
||||
@@ -22,23 +20,18 @@ import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
|
||||
public class PromptTemplateServiceImpl implements IPromptTemplateService
|
||||
{
|
||||
private static final Pattern PROMPT_CODE = Pattern.compile("^[a-z0-9._-]+$");
|
||||
private static final String INITIAL_V1_REMARK = "由当前代码内置 Prompt 初始化";
|
||||
private static final String BUSINESS_V1_REMARK = "由当前代码内置业务 Prompt 初始化";
|
||||
|
||||
private final PromptRegistryMapper mapper;
|
||||
private final PromptFingerprintService fingerprintService;
|
||||
private final ModelGatewayRouter gatewayRouter;
|
||||
private final BuiltinPromptRegistry builtinPromptRegistry;
|
||||
|
||||
@Autowired
|
||||
public PromptTemplateServiceImpl(PromptRegistryMapper mapper,
|
||||
PromptFingerprintService fingerprintService, ModelGatewayRouter gatewayRouter,
|
||||
BuiltinPromptRegistry builtinPromptRegistry)
|
||||
PromptFingerprintService fingerprintService, ModelGatewayRouter gatewayRouter)
|
||||
{
|
||||
this.mapper = mapper;
|
||||
this.fingerprintService = fingerprintService;
|
||||
this.gatewayRouter = gatewayRouter;
|
||||
this.builtinPromptRegistry = builtinPromptRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,7 +48,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
|
||||
PromptTemplateDetail detail = new PromptTemplateDetail();
|
||||
detail.setTemplate(template);
|
||||
detail.setVersions(versions == null ? Collections.<PromptVersion>emptyList() : versions);
|
||||
detail.setBuiltinRelease(builtinPromptRegistry.resolve(template.getGenerateType()));
|
||||
return detail;
|
||||
}
|
||||
|
||||
@@ -64,7 +56,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
|
||||
public int insertPromptTemplate(PromptTemplate template)
|
||||
{
|
||||
normalizeTemplate(template, true);
|
||||
builtinPromptRegistry.resolve(template.getGenerateType());
|
||||
if (mapper.selectPromptTemplateByCode(template.getPromptCode()) != null)
|
||||
{
|
||||
throw new ServiceException("Prompt code already exists: " + template.getPromptCode());
|
||||
@@ -148,96 +139,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
|
||||
return changePublication(promptTemplateId, promptVersionId, operator, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int initializeBuiltinV1(String operator)
|
||||
{
|
||||
List<PromptTemplate> templates = mapper.selectPromptTemplateList(new PromptTemplate());
|
||||
if (templates == null || templates.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
int initialized = 0;
|
||||
String initializedBy = StringUtils.defaultIfEmpty(operator, "system");
|
||||
for (PromptTemplate item : templates)
|
||||
{
|
||||
PromptTemplate template = requireLockedTemplate(item.getPromptTemplateId());
|
||||
int maxVersion = safe(mapper.selectMaxVersionNo(template.getPromptTemplateId()));
|
||||
if (maxVersion > 0)
|
||||
{
|
||||
List<PromptVersion> versions = mapper.selectPromptVersions(
|
||||
template.getPromptTemplateId());
|
||||
if (isRepairableInitializedV1(versions))
|
||||
{
|
||||
PromptVersion version = versions.get(0);
|
||||
applyBuiltinV1(template, version, initializedBy);
|
||||
version.setRemark(BUSINESS_V1_REMARK);
|
||||
if (mapper.updateInitializedV1(version) != 1)
|
||||
{
|
||||
throw new ServiceException("Unable to repair initialized Prompt V1: "
|
||||
+ template.getPromptCode());
|
||||
}
|
||||
initialized++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
PromptVersion version = new PromptVersion();
|
||||
version.setPromptTemplateId(template.getPromptTemplateId());
|
||||
version.setVersionNo(1);
|
||||
version.setVersion("V1");
|
||||
version.setStatus(PromptVersion.STATUS_DRAFT);
|
||||
applyBuiltinV1(template, version, initializedBy);
|
||||
version.setRemark(BUSINESS_V1_REMARK);
|
||||
if (mapper.insertPromptVersion(version) != 1 || version.getPromptVersionId() == null)
|
||||
{
|
||||
throw new ServiceException("Unable to initialize Prompt V1: "
|
||||
+ template.getPromptCode());
|
||||
}
|
||||
if (mapper.publishPromptVersion(version.getPromptVersionId(), initializedBy) != 1
|
||||
|| mapper.updateCurrentVersion(template.getPromptTemplateId(),
|
||||
version.getPromptVersionId(), initializedBy) != 1)
|
||||
{
|
||||
throw new ServiceException("Unable to publish initialized Prompt V1: "
|
||||
+ template.getPromptCode());
|
||||
}
|
||||
initialized++;
|
||||
}
|
||||
return initialized;
|
||||
}
|
||||
|
||||
private void applyBuiltinV1(PromptTemplate template, PromptVersion version,
|
||||
String initializedBy)
|
||||
{
|
||||
PromptRelease builtin = builtinPromptRegistry.resolve(template.getGenerateType());
|
||||
gatewayRouter.resolve(builtin.getProviderCode());
|
||||
version.setSystemPrompt(builtinPromptRegistry.managementSystemPrompt(
|
||||
template.getGenerateType()));
|
||||
version.setUserPromptContract(builtin.getUserPromptContract());
|
||||
version.setUserPromptTemplate(builtinPromptRegistry.managementUserPromptTemplate(
|
||||
template.getGenerateType()));
|
||||
version.setProviderCode(builtin.getProviderCode());
|
||||
version.setModel(StringUtils.defaultString(builtin.getModel()));
|
||||
version.setPipelineReleaseCode(builtin.getPipelineReleaseCode());
|
||||
version.setCreateBy(initializedBy);
|
||||
version.setContentHash(fingerprintService.fingerprint(template.getGenerateType(),
|
||||
template.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
|
||||
version.getUserPromptContract(), version.getUserPromptTemplate()));
|
||||
}
|
||||
|
||||
private boolean isRepairableInitializedV1(List<PromptVersion> versions)
|
||||
{
|
||||
if (versions == null || versions.size() != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
PromptVersion version = versions.get(0);
|
||||
return Integer.valueOf(1).equals(version.getVersionNo())
|
||||
&& "V1".equals(version.getVersion())
|
||||
&& StringUtils.defaultString(version.getUserPromptContract()).startsWith("legacy-builder:")
|
||||
&& (INITIAL_V1_REMARK.equals(version.getRemark())
|
||||
|| BUSINESS_V1_REMARK.equals(version.getRemark()));
|
||||
}
|
||||
|
||||
private int changePublication(Long promptTemplateId, Long promptVersionId,
|
||||
String operator, boolean rollback)
|
||||
{
|
||||
@@ -257,13 +158,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
|
||||
throw new ServiceException("Only a draft Prompt version can be published");
|
||||
}
|
||||
verifyContentHash(template, version);
|
||||
PromptRelease supportedPrompt = builtinPromptRegistry.resolve(template.getGenerateType());
|
||||
if (!StringUtils.equals(supportedPrompt.getUserPromptContract(),
|
||||
version.getUserPromptContract()))
|
||||
{
|
||||
throw new ServiceException("User Prompt contract is not implemented by this application: "
|
||||
+ version.getUserPromptContract());
|
||||
}
|
||||
gatewayRouter.resolve(version.getProviderCode());
|
||||
mapper.retirePublishedVersions(promptTemplateId, promptVersionId);
|
||||
if (mapper.publishPromptVersion(promptVersionId, StringUtils.defaultString(operator)) != 1)
|
||||
|
||||
@@ -186,10 +186,11 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
String aiContent = chatWithUsage(userId, projectId, "flow_config", prompt);
|
||||
modelAttempts.add(aiContent);
|
||||
SimpleFlowConfig config;
|
||||
AppBlueprintDesign reconciledBlueprint;
|
||||
try
|
||||
{
|
||||
config = parseFlowConfigResponse(aiContent);
|
||||
validateGeneratedFlowConfig(project, request, config);
|
||||
reconciledBlueprint = validateGeneratedFlowConfig(project, request, config);
|
||||
}
|
||||
catch (RuntimeException firstFailure)
|
||||
{
|
||||
@@ -197,9 +198,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
aiContent = chatWithUsage(userId, projectId, "flow_config", prompt);
|
||||
modelAttempts.add(aiContent);
|
||||
config = parseFlowConfigResponse(aiContent);
|
||||
validateGeneratedFlowConfig(project, request, config);
|
||||
reconciledBlueprint = validateGeneratedFlowConfig(project, request, config);
|
||||
}
|
||||
markFlowConfigGenerated(projectId, project.getDraftRevision(), config, request);
|
||||
markFlowConfigGenerated(projectId, project.getDraftRevision(), config, reconciledBlueprint, request);
|
||||
generation.setResponsePayload(aiContent);
|
||||
generation.setSuccess("1");
|
||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||
@@ -253,7 +254,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
flowConfig, flowCompilation, true);
|
||||
}
|
||||
persistDatabase(userId, projectId, response);
|
||||
markDatabaseGenerated(projectId, project.getDraftRevision(), request, flowConfig);
|
||||
markDatabaseGenerated(projectId, project.getDraftRevision(), request, appBlueprint, flowConfig);
|
||||
generation.setResponsePayload(aiContent);
|
||||
generation.setSuccess("1");
|
||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||
@@ -920,7 +921,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
return flowConfigSanitizer().sanitize(config);
|
||||
}
|
||||
|
||||
private void validateGeneratedFlowConfig(FrontProject project, GenerateFlowConfigRequest request,
|
||||
private AppBlueprintDesign validateGeneratedFlowConfig(FrontProject project, GenerateFlowConfigRequest request,
|
||||
SimpleFlowConfig config)
|
||||
{
|
||||
String blueprintJson = firstNonBlank(request == null ? null : request.getAppBlueprint(),
|
||||
@@ -934,6 +935,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
flowConfigContractService().validateDatabase(loadSavedDatabaseDesign(project.getProjectId()),
|
||||
config, compilation);
|
||||
}
|
||||
return blueprint;
|
||||
}
|
||||
|
||||
private FlowConfigSanitizer flowConfigSanitizer()
|
||||
@@ -946,8 +948,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
String appBlueprintJson = resolveConfirmedAppBlueprint(project, request);
|
||||
SimpleFlowConfig flowConfig = readFlowConfig(project,
|
||||
request == null ? null : request.getFlowConfig());
|
||||
FlowConfigCompilation compilation = flowConfigContractService().canonicalize(flowConfig,
|
||||
parseConfirmedAppBlueprint(appBlueprintJson));
|
||||
AppBlueprintDesign appBlueprint = parseConfirmedAppBlueprint(appBlueprintJson);
|
||||
FlowConfigCompilation compilation = flowConfigContractService().canonicalize(flowConfig, appBlueprint);
|
||||
appBlueprintJson = JSON.toJSONString(appBlueprint);
|
||||
String flowConfigJson = flowConfig == null ? "" : JSON.toJSONString(flowConfig);
|
||||
String businessLoopPlanJson = JSON.toJSONString(compilation.getLoopPlan());
|
||||
Map<String, Object> context = new LinkedHashMap<String, Object>();
|
||||
@@ -4140,7 +4143,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
}
|
||||
|
||||
private void markDatabaseGenerated(Long projectId, Long draftRevision, GenerateDatabaseRequest request,
|
||||
SimpleFlowConfig flowConfig)
|
||||
AppBlueprintDesign blueprint, SimpleFlowConfig flowConfig)
|
||||
{
|
||||
FrontProject project = new FrontProject();
|
||||
project.setProjectId(projectId);
|
||||
@@ -4150,6 +4153,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
|
||||
project.setProjectDesc(request == null ? null : request.getProjectDesc());
|
||||
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
|
||||
if (blueprint != null) project.setAppBlueprint(JSON.toJSONString(blueprint));
|
||||
if (flowConfig != null) project.setFlowConfig(JSON.toJSONString(flowConfig));
|
||||
applyStylePreset(project, request == null ? null : request.getStylePreset());
|
||||
if (frontProjectMapper.updateFrontProjectIfRevision(project) == 0)
|
||||
@@ -4160,13 +4164,14 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
}
|
||||
|
||||
private void markFlowConfigGenerated(Long projectId, Long draftRevision, SimpleFlowConfig config,
|
||||
GenerateFlowConfigRequest request)
|
||||
AppBlueprintDesign blueprint, GenerateFlowConfigRequest request)
|
||||
{
|
||||
FrontProject project = new FrontProject();
|
||||
project.setProjectId(projectId);
|
||||
project.setDraftRevision(draftRevision);
|
||||
project.setPreviewStatus("0");
|
||||
project.setFlowConfig(JSON.toJSONString(config));
|
||||
project.setAppBlueprint(JSON.toJSONString(blueprint));
|
||||
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
|
||||
project.setProjectDesc(request == null ? null : request.getProjectDesc());
|
||||
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
|
||||
|
||||
@@ -24,22 +24,34 @@ public class FlowConfigContractService
|
||||
private final FlowConfigCompiler flowCompiler;
|
||||
private final FlowConfigDatabaseContractValidator databaseValidator;
|
||||
private final BusinessLoopCoverageValidator coverageValidator;
|
||||
private final FlowConfigPageBindingReconciler pageBindingReconciler;
|
||||
|
||||
public FlowConfigContractService()
|
||||
{
|
||||
this(new FlowConfigValidator(), new FlowConfigCompiler(),
|
||||
new FlowConfigDatabaseContractValidator(), new BusinessLoopCoverageValidator());
|
||||
new FlowConfigDatabaseContractValidator(), new BusinessLoopCoverageValidator(),
|
||||
new FlowConfigPageBindingReconciler());
|
||||
}
|
||||
|
||||
public FlowConfigContractService(FlowConfigValidator flowValidator, FlowConfigCompiler flowCompiler,
|
||||
FlowConfigDatabaseContractValidator databaseValidator,
|
||||
BusinessLoopCoverageValidator coverageValidator)
|
||||
{
|
||||
this(flowValidator, flowCompiler, databaseValidator, coverageValidator,
|
||||
new FlowConfigPageBindingReconciler());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public FlowConfigContractService(FlowConfigValidator flowValidator, FlowConfigCompiler flowCompiler,
|
||||
FlowConfigDatabaseContractValidator databaseValidator,
|
||||
BusinessLoopCoverageValidator coverageValidator)
|
||||
BusinessLoopCoverageValidator coverageValidator,
|
||||
FlowConfigPageBindingReconciler pageBindingReconciler)
|
||||
{
|
||||
this.flowValidator = flowValidator;
|
||||
this.flowCompiler = flowCompiler;
|
||||
this.databaseValidator = databaseValidator;
|
||||
this.coverageValidator = coverageValidator;
|
||||
this.pageBindingReconciler = pageBindingReconciler;
|
||||
}
|
||||
|
||||
public FlowConfigCompilation compile(SimpleFlowConfig config)
|
||||
@@ -58,8 +70,9 @@ public class FlowConfigContractService
|
||||
first.getLoopPlan(), blueprint);
|
||||
if (aliases.isEmpty())
|
||||
{
|
||||
boolean bindingChanged = pageBindingReconciler.reconcile(config, blueprint);
|
||||
validatePageBindings(config, blueprint);
|
||||
return first;
|
||||
return bindingChanged ? compile(config) : first;
|
||||
}
|
||||
|
||||
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
|
||||
@@ -83,6 +96,7 @@ public class FlowConfigContractService
|
||||
if (metric != null) metric.setTable(rename(metric.getTable(), aliases));
|
||||
}
|
||||
}
|
||||
pageBindingReconciler.reconcile(config, blueprint);
|
||||
validatePageBindings(config, blueprint);
|
||||
return compile(config);
|
||||
}
|
||||
@@ -156,19 +170,25 @@ public class FlowConfigContractService
|
||||
|
||||
private void validatePageBindings(SimpleFlowConfig config, AppBlueprintDesign blueprint)
|
||||
{
|
||||
Map<String, String> pages = new java.util.LinkedHashMap<String, String>();
|
||||
Map<String, PageTarget> pages = new java.util.LinkedHashMap<String, PageTarget>();
|
||||
if (blueprint.getFrontendPages() != null)
|
||||
{
|
||||
for (com.ruoyi.generator.domain.front.dto.AppPageDesign page : blueprint.getFrontendPages())
|
||||
{
|
||||
if (page != null && StringUtils.isNotBlank(page.getCode())) pages.put(page.getCode(), page.getTableName());
|
||||
if (page != null && StringUtils.isNotBlank(page.getCode()))
|
||||
{
|
||||
pages.put(page.getCode(), new PageTarget(page.getTableName(), page.getPageType()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (blueprint.getAdminMenus() != null)
|
||||
{
|
||||
for (com.ruoyi.generator.domain.front.dto.AppMenuDesign menu : blueprint.getAdminMenus())
|
||||
{
|
||||
if (menu != null && StringUtils.isNotBlank(menu.getPageCode())) pages.put(menu.getPageCode(), menu.getTableName());
|
||||
if (menu != null && StringUtils.isNotBlank(menu.getPageCode()))
|
||||
{
|
||||
pages.put(menu.getPageCode(), new PageTarget(menu.getTableName(), menu.getPageType()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
|
||||
@@ -183,16 +203,45 @@ public class FlowConfigContractService
|
||||
}
|
||||
String ownerTable = StringUtils.defaultIfBlank(action.getOwnerTable(), flow.getMainTable());
|
||||
String bindingTable = StringUtils.defaultIfBlank(action.getButton().getTableName(), ownerTable);
|
||||
String pageTable = pages.get(pageCode);
|
||||
PageTarget page = pages.get(pageCode);
|
||||
String pageTable = page.tableName;
|
||||
if (StringUtils.isNotBlank(pageTable) && !StringUtils.equals(pageTable, bindingTable))
|
||||
{
|
||||
throw new ServiceException("flow_config button table does not match page: " + pageCode
|
||||
+ " -> " + bindingTable + ", expected " + pageTable);
|
||||
}
|
||||
validateSlotPageType(action, pageCode, action.getButton().getSlot(), page.pageType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSlotPageType(SimpleFlowConfig.Action action, String pageCode, String slot,
|
||||
String pageType)
|
||||
{
|
||||
if (StringUtils.isBlank(slot) || StringUtils.isBlank(pageType)) return;
|
||||
boolean valid = slot.startsWith("detail.") ? "detail".equals(pageType)
|
||||
: slot.startsWith("form.") ? "form".equals(pageType)
|
||||
: slot.startsWith("list.") ? StringUtils.equalsAny(pageType, "list", "current_user_list")
|
||||
: true;
|
||||
if (!valid)
|
||||
{
|
||||
throw new ServiceException("flow_config button slot does not match page type: "
|
||||
+ action.getCode() + " -> " + pageCode + " (" + pageType + ") / " + slot);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PageTarget
|
||||
{
|
||||
private final String tableName;
|
||||
private final String pageType;
|
||||
|
||||
private PageTarget(String tableName, String pageType)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
this.pageType = pageType;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> List<T> safe(List<T> items)
|
||||
{
|
||||
return items == null ? java.util.Collections.<T>emptyList() : items;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
|
||||
import com.ruoyi.generator.factory.flow.FlowConfigSupport;
|
||||
|
||||
/**
|
||||
* Keeps record-scoped business entry actions on a resource detail page.
|
||||
*
|
||||
* <p>An initiating action that loads one resource record and creates the main
|
||||
* business record is different from a transition on an existing business
|
||||
* record. The former belongs on the resource detail page; the latter may stay
|
||||
* in a list row.</p>
|
||||
*/
|
||||
@Component
|
||||
public class FlowConfigPageBindingReconciler
|
||||
{
|
||||
public boolean reconcile(SimpleFlowConfig config, AppBlueprintDesign blueprint)
|
||||
{
|
||||
if (config == null || blueprint == null || blueprint.getFrontendPages() == null) return false;
|
||||
boolean changed = false;
|
||||
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
|
||||
{
|
||||
for (SimpleFlowConfig.Action action : flow.getActions())
|
||||
{
|
||||
if (!requiresResourceDetail(flow, action)) continue;
|
||||
String ownerTable = action.getOwnerTable();
|
||||
AppPageDesign detailPage = findPage(blueprint, ownerTable, "detail", null);
|
||||
AppPageDesign sourcePage = findSourceListPage(blueprint, ownerTable,
|
||||
action.getButton().getPageCode());
|
||||
if (detailPage == null && sourcePage != null)
|
||||
{
|
||||
detailPage = createDetailPage(blueprint, sourcePage, ownerTable);
|
||||
blueprint.getFrontendPages().add(detailPage);
|
||||
changed = true;
|
||||
}
|
||||
if (detailPage == null) continue;
|
||||
|
||||
SimpleFlowConfig.Button button = action.getButton();
|
||||
if (!StringUtils.equals(button.getPageCode(), detailPage.getCode()))
|
||||
{
|
||||
button.setPageCode(detailPage.getCode());
|
||||
changed = true;
|
||||
}
|
||||
if (!StringUtils.equals(button.getTableName(), ownerTable))
|
||||
{
|
||||
button.setTableName(ownerTable);
|
||||
changed = true;
|
||||
}
|
||||
if (!"detail.primaryActions".equals(button.getSlot()))
|
||||
{
|
||||
button.setSlot("detail.primaryActions");
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
boolean requiresResourceDetail(SimpleFlowConfig flow, SimpleFlowConfig.Action action)
|
||||
{
|
||||
if (flow == null || action == null || action.getButton() == null) return false;
|
||||
if (StringUtils.isBlank(action.getOwnerTable())
|
||||
|| StringUtils.equals(action.getOwnerTable(), flow.getMainTable())) return false;
|
||||
if (StringUtils.isNotBlank(action.getFrom()) || StringUtils.isNotBlank(action.getTo())) return false;
|
||||
if (isAdmin(action.getActor()) || isAdmin(action.getButton().getScope())) return false;
|
||||
for (SimpleFlowConfig.Effect effect : action.getEffects())
|
||||
{
|
||||
if (effect != null && "create_record".equals(effect.getType())
|
||||
&& StringUtils.equals(flow.getMainTable(), effect.getTable())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private AppPageDesign findSourceListPage(AppBlueprintDesign blueprint, String tableName, String pageCode)
|
||||
{
|
||||
AppPageDesign bound = findPage(blueprint, tableName, "list", pageCode);
|
||||
return bound == null ? findPage(blueprint, tableName, "list", null) : bound;
|
||||
}
|
||||
|
||||
private AppPageDesign findPage(AppBlueprintDesign blueprint, String tableName, String pageType,
|
||||
String preferredCode)
|
||||
{
|
||||
AppPageDesign fallback = null;
|
||||
for (AppPageDesign page : blueprint.getFrontendPages())
|
||||
{
|
||||
if (page == null || !StringUtils.equals(tableName, page.getTableName())
|
||||
|| !StringUtils.equals(pageType, page.getPageType())) continue;
|
||||
if (StringUtils.isNotBlank(preferredCode) && StringUtils.equals(preferredCode, page.getCode()))
|
||||
{
|
||||
return page;
|
||||
}
|
||||
if (fallback == null) fallback = page;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private AppPageDesign createDetailPage(AppBlueprintDesign blueprint, AppPageDesign sourcePage,
|
||||
String tableName)
|
||||
{
|
||||
AppPageDesign detail = new AppPageDesign();
|
||||
detail.setCode(uniquePageCode(blueprint, detailPageCode(tableName)));
|
||||
detail.setName(detailPageName(sourcePage.getName()));
|
||||
detail.setMenuCode(sourcePage.getMenuCode());
|
||||
detail.setParentPageCode(sourcePage.getCode());
|
||||
detail.setPath(detailPath(sourcePage.getPath(), tableName));
|
||||
detail.setPageType("detail");
|
||||
detail.setTableName(tableName);
|
||||
return detail;
|
||||
}
|
||||
|
||||
private String uniquePageCode(AppBlueprintDesign blueprint, String baseCode)
|
||||
{
|
||||
Set<String> used = new HashSet<String>();
|
||||
for (AppPageDesign page : blueprint.getFrontendPages())
|
||||
{
|
||||
if (page != null && StringUtils.isNotBlank(page.getCode())) used.add(page.getCode());
|
||||
}
|
||||
if (!used.contains(baseCode)) return baseCode;
|
||||
int suffix = 2;
|
||||
while (used.contains(baseCode + "_" + suffix)) suffix++;
|
||||
return baseCode + "_" + suffix;
|
||||
}
|
||||
|
||||
private String detailPageCode(String tableName)
|
||||
{
|
||||
String base = StringUtils.defaultString(tableName).toLowerCase(Locale.ENGLISH)
|
||||
.replaceAll("(_info|_records|_record|_table|_data|_list)$", "");
|
||||
if (StringUtils.isBlank(base)) base = "resource";
|
||||
return base + "_detail_page";
|
||||
}
|
||||
|
||||
private String detailPageName(String sourceName)
|
||||
{
|
||||
String name = StringUtils.defaultIfBlank(sourceName, "资源").trim();
|
||||
String replaced = name.replaceFirst("(目录|列表)$", "详情")
|
||||
.replaceFirst("(?i)(catalog|list)$", "Detail");
|
||||
return StringUtils.equals(replaced, name) ? name + "详情" : replaced;
|
||||
}
|
||||
|
||||
private String detailPath(String sourcePath, String tableName)
|
||||
{
|
||||
String path = StringUtils.defaultString(sourcePath).trim().replaceAll("/+$", "");
|
||||
if (StringUtils.isBlank(path))
|
||||
{
|
||||
String resource = detailPageCode(tableName).replaceFirst("_detail_page$", "").replace('_', '-');
|
||||
path = "/" + resource;
|
||||
}
|
||||
return path + "/:id";
|
||||
}
|
||||
|
||||
private boolean isAdmin(String value)
|
||||
{
|
||||
String normalized = StringUtils.defaultString(value).trim().toLowerCase(Locale.ENGLISH);
|
||||
return "admin".equals(normalized) || "admin_frontend".equals(normalized);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.config.DeepSeekProperties;
|
||||
import com.ruoyi.generator.factory.ai.prompt.BuiltinPromptRegistry;
|
||||
|
||||
@Component
|
||||
public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
@@ -64,19 +63,11 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
}
|
||||
};
|
||||
|
||||
private static final String DEFAULT_SYSTEM_PROMPT = BuiltinPromptRegistry.DEFAULT_SYSTEM_PROMPT;
|
||||
|
||||
@Autowired
|
||||
private DeepSeekProperties deepSeekProperties;
|
||||
@Autowired(required = false)
|
||||
private AiUsageRecorder aiUsageRecorder;
|
||||
|
||||
@Override
|
||||
public String chat(String prompt)
|
||||
{
|
||||
return chat(DEFAULT_SYSTEM_PROMPT, prompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(String systemPrompt, String prompt)
|
||||
{
|
||||
@@ -86,6 +77,7 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
@Override
|
||||
public String chatModel(String model, String systemPrompt, String prompt)
|
||||
{
|
||||
requireManagedSystemPrompt(systemPrompt);
|
||||
if (StringUtils.isBlank(deepSeekProperties.getApiKey()))
|
||||
{
|
||||
throw new ServiceException("DeepSeek API Key 未配置");
|
||||
@@ -100,12 +92,6 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
return parseContent(response.getBody(), model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(String prompt, final StreamHandler handler)
|
||||
{
|
||||
chatStream(DEFAULT_SYSTEM_PROMPT, prompt, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatStream(String systemPrompt, String prompt, final StreamHandler handler)
|
||||
{
|
||||
@@ -116,6 +102,7 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
public void chatStreamModel(String model, String systemPrompt, String prompt,
|
||||
final StreamHandler handler)
|
||||
{
|
||||
requireManagedSystemPrompt(systemPrompt);
|
||||
if (StringUtils.isBlank(deepSeekProperties.getApiKey()))
|
||||
{
|
||||
throw new ServiceException("DeepSeek API Key 未配置");
|
||||
@@ -140,11 +127,6 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
});
|
||||
}
|
||||
|
||||
private JSONObject requestBody(String prompt, boolean stream)
|
||||
{
|
||||
return requestBody(DEFAULT_SYSTEM_PROMPT, prompt, stream);
|
||||
}
|
||||
|
||||
private JSONObject requestBody(String systemPrompt, String prompt, boolean stream)
|
||||
{
|
||||
return requestBody(deepSeekProperties.getModel(), systemPrompt, prompt, stream);
|
||||
@@ -152,10 +134,11 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
|
||||
private JSONObject requestBody(String model, String systemPrompt, String prompt, boolean stream)
|
||||
{
|
||||
requireManagedSystemPrompt(systemPrompt);
|
||||
JSONObject request = new JSONObject();
|
||||
request.put("model", StringUtils.defaultIfEmpty(model, deepSeekProperties.getModel()));
|
||||
JSONArray messages = new JSONArray();
|
||||
messages.add(message("system", StringUtils.defaultIfEmpty(systemPrompt, DEFAULT_SYSTEM_PROMPT)));
|
||||
messages.add(message("system", systemPrompt));
|
||||
messages.add(message("user", prompt));
|
||||
request.put("messages", messages);
|
||||
request.put("temperature", 0.2);
|
||||
@@ -169,6 +152,14 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
return request;
|
||||
}
|
||||
|
||||
private void requireManagedSystemPrompt(String systemPrompt)
|
||||
{
|
||||
if (StringUtils.isBlank(systemPrompt))
|
||||
{
|
||||
throw new ServiceException("System Prompt must come from a published managed Prompt version");
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject message(String role, String content)
|
||||
{
|
||||
JSONObject message = new JSONObject();
|
||||
|
||||
@@ -9,14 +9,10 @@ public interface IDeepSeekClient
|
||||
void onContent(String content) throws IOException;
|
||||
}
|
||||
|
||||
String chat(String prompt);
|
||||
|
||||
String chat(String systemPrompt, String prompt);
|
||||
|
||||
String chatModel(String model, String systemPrompt, String prompt);
|
||||
|
||||
void chatStream(String prompt, StreamHandler handler);
|
||||
|
||||
void chatStream(String systemPrompt, String prompt, StreamHandler handler);
|
||||
|
||||
void chatStreamModel(String model, String systemPrompt, String prompt, StreamHandler handler);
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.domain.front.AiGenerationTask;
|
||||
import com.ruoyi.generator.domain.front.FrontProject;
|
||||
import com.ruoyi.generator.domain.front.dto.AiGenerationTaskCreateRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationException;
|
||||
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationReport;
|
||||
@@ -38,6 +39,8 @@ public class OneClickProjectGenerationServiceImpl implements IOneClickProjectGen
|
||||
private FlowConfigDatabaseContractValidator flowConfigDatabaseContractValidator;
|
||||
@Autowired(required = false)
|
||||
private BusinessRequirementCoverageValidator businessRequirementCoverageValidator;
|
||||
@Autowired(required = false)
|
||||
private FlowConfigPageBindingReconciler pageBindingReconciler;
|
||||
|
||||
@Override
|
||||
public OneClickProjectGenerationResult generate(AiGenerationTask task, AiGenerationTaskCreateRequest request)
|
||||
@@ -106,6 +109,11 @@ public class OneClickProjectGenerationServiceImpl implements IOneClickProjectGen
|
||||
{
|
||||
throw new ServiceException("Flow config is missing in the design checkpoint");
|
||||
}
|
||||
AppBlueprintDesign blueprint = JSON.parseObject(context.getAppBlueprintJson(), AppBlueprintDesign.class);
|
||||
if (pageBindingReconciler().reconcile(context.getFlowConfig(), blueprint))
|
||||
{
|
||||
throw new ServiceException("Flow config resource actions require regenerated detail pages");
|
||||
}
|
||||
requirementCoverageValidator().validate(requirement(context.getRequest()), context.getFlowConfig());
|
||||
return true;
|
||||
}
|
||||
@@ -204,6 +212,11 @@ public class OneClickProjectGenerationServiceImpl implements IOneClickProjectGen
|
||||
? new BusinessRequirementCoverageValidator() : businessRequirementCoverageValidator;
|
||||
}
|
||||
|
||||
private FlowConfigPageBindingReconciler pageBindingReconciler()
|
||||
{
|
||||
return pageBindingReconciler == null ? new FlowConfigPageBindingReconciler() : pageBindingReconciler;
|
||||
}
|
||||
|
||||
private String requirement(AiGenerationTaskCreateRequest request)
|
||||
{
|
||||
if (request == null) return "";
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
你是应用蓝图架构师。只输出符合用户消息结构的 JSON,规划业务角色、前台菜单与页面、后台菜单和运营看板。保持 pageCode、tableName、角色数据范围和登录要求前后一致;只描述静态蓝图,不生成数据库、权限表、菜单表或代码。角色名称使用简体中文,技术标识使用稳定的小写 snake_case。
|
||||
@@ -1,22 +0,0 @@
|
||||
根据项目输入 JSON 生成应用蓝图,只返回 JSON,不要 Markdown 围栏或解释。
|
||||
输出结构:{"roles":[{"code":"reader","name":"读者","description":""}],"frontendMenus":[{"code":"book_catalog","name":"图书目录","path":"/books","pageCode":"book_catalog_page","requiresLogin":false,"visibleRoles":[],"dataScope":"PUBLIC"}],"frontendPages":[{"code":"book_catalog_page","name":"图书目录","menuCode":"book_catalog","path":"/books","pageType":"list","tableName":"book_info"}],"adminMenus":[{"code":"book_admin","name":"图书管理","path":"/admin/books","pageCode":"book_admin_page","tableName":"book_info","pageType":"list","requiresLogin":true,"visibleRoles":["admin"],"dataScope":"ROLE"}],"adminHomeDashboard":{"title":"运营总览","description":"核心业务数据","metrics":[],"charts":[]}}。
|
||||
蓝图描述目标生成项目,而不是当前 EasyCode/RuoYi 平台。不要生成数据库表、菜单表、角色表、权限表或代码。
|
||||
role.name 使用简体中文,role.code 使用小写 snake_case。只有 role.code=admin 是系统管理员;名称含“管理员”或“后台”的业务角色仍保持独立。
|
||||
role.name must be Simplified Chinese. Only role.code=admin is the system administrator.
|
||||
每个菜单必须包含 requiresLogin、visibleRoles、dataScope;dataScope 只能是 PUBLIC、CURRENT_USER 或 ROLE。
|
||||
adminMenus 必须包含 pageCode、tableName、pageType;个人资料使用 form,普通管理使用 list。
|
||||
adminMenus must include pageCode, tableName, and pageType.
|
||||
frontendMenus 顶层菜单应指向 pageCode;详情页作为子页面,不创建顶层详情菜单。
|
||||
不要为轮播图、banner、公告滚动条等首页装饰创建顶层菜单,可放在首页业务块或作为后台维护数据。
|
||||
frontendPages.pageType 只能是 list、current_user_list、detail、form,tableName 必须是页面展示的业务表。
|
||||
同一业务对象只使用一个稳定 tableName,后续数据库、闭环、页面、看板、动作必须复用,不能使用同义表名。
|
||||
adminHomeDashboard 尽量包含 4 个业务指标和至少 2 张图表(趋势图与分布图);所有表字段必须可由后续数据库步骤创建。semantic 只能使用 money、order、user、task、warning、inventory、content、default。
|
||||
adminHomeDashboard.metrics 每项结构为 {"code":"total_books","title":"总图书数","tableName":"book_info","aggregate":"count","field":"book_id","semantic":"inventory"}。
|
||||
adminHomeDashboard.charts 每项结构为 {"code":"borrow_trend","title":"借阅趋势","type":"line","tableName":"borrow_record","dimension":{"field":"borrow_date","timeUnit":"day"},"metrics":[{"field":"borrow_id","aggregate":"count","alias":"borrowCount","label":"借阅量"}],"semantic":"order"}。dimension 必须是对象,metrics 必须是数组;不要输出旧版 xField、yField、metric 或字符串 dimension。
|
||||
每个非 admin 角色使用表名等于 role.code 的独立账号表,不得创建共享 user、user_info、member 或 account 表。
|
||||
Never invent a shared user, user_info, member, or account table for role accounts.
|
||||
需要个人资料页时,为每个非 admin 角色创建独立 form 页面和菜单,tableName 绑定 role.code,pageCode/path 带角色前缀,visibleRoles 只包含该角色。
|
||||
Create one form page and one menu per non-admin role when profile pages are needed.
|
||||
个人订单、借阅、资料、预约、支付等页面必须 requiresLogin=true 且 dataScope=CURRENT_USER。
|
||||
项目输入 JSON:
|
||||
{{input}}
|
||||
@@ -1 +0,0 @@
|
||||
你是资深 Java/Vue 毕设项目代码解读专家。只依据用户消息中提供的源码、文件清单和项目元信息生成正式 Markdown 报告,引用真实类名、方法名、组件、表和字段;无法确认的内容必须明确说明,不得臆造模块或流程。报告应覆盖项目概述、技术栈、架构模块、核心业务、数据库设计和答辩准备。
|
||||
@@ -1,12 +0,0 @@
|
||||
根据输入 JSON 中的精选核心源码和项目元信息生成正式 Markdown 代码解读报告,不要代码围栏或 JSON。
|
||||
只能依据已展开源码和文件清单,不要编造未出现的模块、接口、表或流程;无法确认时明确说明。引用真实类名、方法名、组件名、表名和字段名。
|
||||
无法从精选源码确认的内容必须明确标注“无法从精选源码确认”。
|
||||
必须按顺序包含:
|
||||
## 1. 项目概述与技术栈
|
||||
## 2. 系统架构与核心模块
|
||||
## 3. 核心业务流程
|
||||
## 4. 数据库设计
|
||||
## 5. 答辩准备
|
||||
若 input.mode=continue,在保留主题和章节结构的前提下根据 previousMarkdown 补充、修正和续写,避免重复无变化的长段。
|
||||
输入 JSON:
|
||||
{{input}}
|
||||
@@ -1 +0,0 @@
|
||||
你是 MySQL 业务数据库设计专家。只输出符合用户消息结构的 JSON,根据已确认的应用蓝图和业务闭环生成可直接落地的表、字段、主外键、字典和表操作策略。字段必须业务化且类型合法,所有蓝图与闭环引用必须有真实表字段承载;不得生成菜单、权限关系、代码或业务动作。flowConfig.actions[].audit 中非空的 userField 和 timeField 必须是 mainTable 的真实字段,userField 使用 bigint(20),timeField 使用 datetime。
|
||||
@@ -1,30 +0,0 @@
|
||||
根据项目输入 JSON 生成 MySQL 数据库设计 JSON,只返回 JSON,不要 Markdown 围栏或解释。
|
||||
Confirmed app blueprint JSON and Confirmed business loop plan JSON are supplied in the input.
|
||||
Database generation constraints from the business loop plan must be enforced.
|
||||
Every confirmedBusinessLoopPlan.metrics tableName, field, and filters.field must exist in the generated schema. Numeric sum/avg/min/max metric fields must use a compatible numeric column type. Status-filter metric values must remain compatible with the matching stateMachines.states.
|
||||
If input.mode is correction, repair input.originalResponse according to input.validationError and return the complete corrected database JSON. Do not drop unrelated valid tables or fields.
|
||||
输出结构:{"tables":[{"tableName":"car_info","tableComment":"车辆信息表","adminMenuName":"车辆管理","operations":{"add":true,"edit":true,"remove":true,"removeType":"physical","reason":""},"columns":[{"columnName":"id","columnLabel":"ID","columnComment":"主键ID","columnType":"bigint(20)","isPk":"1","isIncrement":"1","isRequired":"1"}]}]}。
|
||||
不要使用 field_1 等占位字段;每个表必须包含非空 columns,以及 adminMenuName 和 operations.add/edit/remove/removeType/reason。
|
||||
adminMenuName 必须来自本表 tableComment/tableName,不得复用其他表或宽泛模块名称。
|
||||
adminMenuName must come from that table's own tableComment or tableName.
|
||||
字段数组名称只能是 columns,不能使用 fields、fieldDesign、fieldList、columnList。
|
||||
Every table must include a non-empty columns array; use columns exactly.
|
||||
每个字段必须包含 columnName、columnLabel、columnComment、columnType、isPk、isIncrement、isRequired。columnLabel 是短 UI 标签,columnComment 是完整数据库说明。
|
||||
可提供 javaType、htmlType;htmlType 只能是 input、textarea、select、radio、checkbox、datetime、imageUpload、fileUpload、editor。
|
||||
select/radio/checkbox 应提供 dictType 和 dictOptions。关联选择使用 remote:target_table:value_field:label_field,且目标表和值/标签字段必须都存在于本次 tables JSON。
|
||||
非主键外键字段应在目标表存在时使用 remote select;无目标表的分类、类型、状态使用本地 dictOptions 或普通输入。
|
||||
普通主数据表除唯一主键外至少包含 4 个业务字段。
|
||||
订单、支付、借还、审批、流程、任务、库存记录等动作记录表使用仅新增;关系、审计、日志、历史表使用只读;普通主数据表使用完整 CRUD。
|
||||
不要生成 businessActions、业务动作、工作流、代码、菜单表、user_role、角色权限表或权限表。
|
||||
Do not generate menu tables, user_role tables, role-permission tables, or permission tables.
|
||||
表名字段名只使用小写字母、数字、下划线;每表必须且只能有一个主键。字段类型只能使用 bigint(20)、int(11)、varchar(n)、char(n)、decimal(p,s)、datetime、date、text、longtext。
|
||||
为 confirmedAppBlueprint.roles 中每个角色创建独立账号表:admin 使用 admin,其他角色使用 role.code。不得使用共享用户表或 role_id/role_code/user_role。
|
||||
角色账号表包含 `<role_code>_id` 自增主键、username、password_hash、nickname、status、create_time。admin 只能登录后台,非 admin 角色登录前台。
|
||||
生成 frontendPages.tableName 引用的全部表;个人资料页必须绑定对应非 admin 角色账号表。requiresLogin=true 且 dataScope=CURRENT_USER 的业务数据通常包含 user_id。
|
||||
canonicalTableRegistry 中的名称是最终规范表名。confirmedBusinessLoopPlan 若出现与其指向同一业务对象的同义表名,必须使用 canonicalTableRegistry 中的名称,不得同时生成两张同义表。
|
||||
根据 confirmedBusinessLoopPlan 创建 coreObjects.tableName、actions.ownerTable;补齐 stateMachines.statusField、quantityRules.quantityField 以及 recordRules 和动作前置条件需要的字段。状态值不得超出 stateMachines.states。
|
||||
严格实现 confirmedBusinessLoopPlan.businessPolicies:TIME_LIMIT 的 startField、dueField 使用 datetime;OVERDUE_FEE 的 dueField 使用 datetime、daysTargetField 使用 int(11)、targetField 使用 decimal(10,2);QUANTITY_LIMIT 的 targetField 使用 int(11) 且默认值为 0。所有政策字段必须位于政策指定的 tableName,禁止只把期限或公式写进 columnComment 而不创建字段。
|
||||
Do not invent status values outside stateMachines.states.
|
||||
严格遵守 input.limits.maxTables 和 maxColumnsPerTable。
|
||||
项目输入 JSON:
|
||||
{{input}}
|
||||
@@ -1 +0,0 @@
|
||||
你是数据库变更同步专家。根据已确认的数据库变更分析受影响的蓝图、业务闭环、页面、动作和生成产物,保持表字段引用及业务语义一致。只提出可验证的同步结果,不虚构不存在的表、字段或模块。
|
||||
@@ -1,3 +0,0 @@
|
||||
根据数据库变更和项目上下文同步受影响的生成结果。严格遵守输入中的阶段契约。
|
||||
输入 JSON:
|
||||
{{input}}
|
||||
@@ -1 +0,0 @@
|
||||
你是企业管理系统生成助手。严格遵守用户消息中的任务、输出格式和语言要求。
|
||||
@@ -1 +0,0 @@
|
||||
你是软件设计图专家。只输出符合用户消息结构的 JSON,同时提供有效 Mermaid、节点和连线数据。根据指定图类型生成简体中文、结构清晰且规模适中的流程图、用例图、时序图、活动图、状态图、数据流图或架构图;不得虚构与当前项目无关的系统。
|
||||
@@ -1,6 +0,0 @@
|
||||
根据输入 JSON 生成软件设计图,只返回 JSON,不要 Markdown 围栏或解释。
|
||||
输出结构:{"diagramType":"flowchart","title":"...","description":"...","mermaid":"flowchart TD...","markdown":"...","nodes":[{"id":"start","label":"开始","type":"start","lane":"用户","description":""}],"edges":[{"source":"start","target":"next","label":""}]}。
|
||||
支持 flowchart、use_case、sequence、activity、state、data_flow、architecture。用例图使用 actor/use_case/system_boundary;流程、活动、状态图使用过程和判断节点;架构图使用 frontend/backend/database/deployment 节点。
|
||||
标签使用简体中文,节点 id 只使用小写字母、数字、下划线。保持 6 到 14 个节点、6 到 18 条边,不虚构无关系统。
|
||||
输入 JSON:
|
||||
{{input}}
|
||||
@@ -1,55 +0,0 @@
|
||||
你是业务流程配置设计器。只输出一个合法 JSON 对象,不输出 Markdown、解释或 SQL。
|
||||
|
||||
目标:用最小 flow_config 描述业务发起、状态流转、显式 Java 业务动作、多表数量联动、按钮和统计。不要输出普通 CRUD,不要输出通用规则 DSL。创建主业务记录的“借书/预约/申请/下单”等发起动作属于业务闭环,必须输出。项目存在预约、借阅等多个独立状态对象时,必须分别放入 flows,禁止只选择其中一个局部流程。
|
||||
|
||||
固定结构:
|
||||
{
|
||||
"version":"1.1",
|
||||
"code":"library_circulation",
|
||||
"name":"图书流通",
|
||||
"flows":[{
|
||||
"code":"reservation_flow","name":"图书预约","mainTable":"reservation","statusField":"status",
|
||||
"notFoundMessage":"预约记录不存在",
|
||||
"states":[{"code":"pending","label":"待处理"},{"code":"cancelled","label":"已取消"}],
|
||||
"actions":[{
|
||||
"code":"reserve_book","name":"预约","actor":"reader","ownerTable":"book_info","requestFields":["id"],
|
||||
"method":"POST","path":"/book-info/reserve","successMessage":"预约成功",
|
||||
"effects":[{"type":"create_record","table":"reservation","values":{"book_id":"record.id","reader_id":"currentUserId","status":"pending","reserve_date":"now","create_time":"now"}}],
|
||||
"button":{"scope":"reader","pageCode":"book_detail_page","tableName":"book_info","slot":"detail.primaryActions","type":"primary","confirm":"确认预约吗?","order":10}
|
||||
},{
|
||||
"code":"cancel_reservation","name":"取消预约","from":"pending","to":"cancelled","actor":"reader",
|
||||
"ownerTable":"reservation","requestFields":["id"],"method":"POST","path":"/reservation/cancel",
|
||||
"button":{"scope":"reader","pageCode":"my_reservations_page","tableName":"reservation","slot":"list.rowActions","type":"danger","confirm":"确认取消预约吗?","order":10}
|
||||
}],
|
||||
"metrics":[]
|
||||
},{
|
||||
"code":"borrowing_flow","name":"图书借阅","mainTable":"borrowing","statusField":"status",
|
||||
"notFoundMessage":"借阅记录不存在",
|
||||
"states":[{"code":"borrowing","label":"借阅中"},{"code":"returned","label":"已归还"}],
|
||||
"actions":[{
|
||||
"code":"borrow_book","name":"借书","actor":"reader","ownerTable":"book_info","requestFields":["id"],
|
||||
"method":"POST","path":"/book-info/borrow","successMessage":"借书成功",
|
||||
"effects":[{"type":"create_record","table":"borrowing","values":{"book_id":"record.id","reader_id":"currentUserId","status":"borrowing","borrow_date":"now","create_time":"now"}},
|
||||
{"type":"decrease","table":"book_info","relationField":"id","field":"stock","amount":"1","min":"0","errorMessage":"图书库存不足"}],
|
||||
"button":{"scope":"reader","pageCode":"book_detail_page","tableName":"book_info","slot":"detail.primaryActions","type":"success","confirm":"确认借书吗?","order":20}
|
||||
},{
|
||||
"code":"return_book","name":"还书","from":"borrowing","to":"returned","actor":"reader","ownerTable":"borrowing","requestFields":["id"],
|
||||
"method":"POST","path":"/borrowing/return","effects":[{"type":"increase","table":"book_info","relationField":"book_id","field":"stock","amount":"1"}],
|
||||
"button":{"scope":"reader","pageCode":"my_borrowings_page","tableName":"borrowing","slot":"list.rowActions","type":"primary","confirm":"确认还书吗?","order":10}
|
||||
}],
|
||||
"metrics":[{"code":"borrowing_count","name":"借阅中数量","table":"borrowing","aggregate":"count","field":"id","status":"borrowing","unit":"条"}]
|
||||
}]
|
||||
}
|
||||
|
||||
约束:
|
||||
1. 根对象固定使用 version=1.1 和 flows 数组;每个独立状态主表对应一个 flow。code、表名、字段名全部使用小写 snake_case;状态 code 使用稳定英文编码,label 使用中文。
|
||||
2. 每个 action 必须给 ownerTable;requestFields 只列页面传入字段,通常为 ownerTable 主键 id,currentUserId 不能作为前端可传字段。
|
||||
3. 除发起型 action 外,每个 action 必须有 from 和 to,二者存在于 states 中且不相同,ownerTable 必须为 mainTable。
|
||||
4. 创建主业务记录的发起型 action 必须省略 from/to,并 create_record 到 mainTable;values 必须给 statusField 写入 states 中的初始状态。借书、预约、申请、报名、下单、提交工单等需求不得误判为普通 CRUD 而省略。
|
||||
5. effects.type 只能是 decrease、increase、update_related、create_record。
|
||||
6. decrease/increase 必须给 table、relationField、field、amount;decrease 还要给 min。
|
||||
7. update_related/create_record 使用 values;值可为 currentUserId、now、record.字段名或常量。record 表示 ownerTable 当前记录。create_record 必须覆盖目标表全部非自增、无默认值的必填字段;create_time、created_at 等创建时间字段必须写为 now。
|
||||
8. 审核人等状态动作身份通过 audit.userField 表达;发起型动作的用户和时间通过 create_record.values 的 currentUserId、now 表达,均由后端登录态注入。
|
||||
9. requiredActionCodes 中的每个动作都必须在 actions 中出现,并绑定到 appBlueprint 已有页面。覆盖需求中的完整正向闭环、业务发起动作和关键回退/拒绝动作,但不要为无业务语义的普通 CRUD 创建 action。
|
||||
10. metrics.status 如果填写,必须完全等于 states 中某个 code;统计全状态数据或不能确定状态时省略 status。
|
||||
11. 每个面向用户或管理员的显式动作必须有 button。button.pageCode 必须引用 appBlueprint 中已有页面;button.tableName 必须是该页面绑定表,省略时等于 ownerTable;button.slot 只能是 list.toolbarActions、list.rowActions、detail.primaryActions、detail.secondaryActions、form.footerActions。详情页主操作使用 detail.primaryActions,列表记录操作使用 list.rowActions,禁止使用 page.actions 等自定义值。
|
||||
@@ -1,2 +0,0 @@
|
||||
请根据以下 JSON 上下文生成 flow_config。appBlueprint 是已确认的页面、角色和菜单范围,必须与它保持一致:
|
||||
{{input}}
|
||||
@@ -1 +0,0 @@
|
||||
你是一键项目生成流水线编排专家。按照应用蓝图、业务闭环、数据库和业务动作的固定阶段顺序工作,保证各阶段 Prompt 身份、输入输出和引用关系可追踪、可恢复、可复现。不跨阶段生成不属于当前阶段的内容。
|
||||
@@ -1,3 +0,0 @@
|
||||
按照一键生成阶段契约处理项目输入,当前阶段只输出该阶段要求的内容。
|
||||
输入 JSON:
|
||||
{{input}}
|
||||
@@ -1 +0,0 @@
|
||||
你是中文软件工程本科毕业论文写作助手。只输出论文正文 Markdown,首个非空字符必须是 #,不要确认语、解释、JSON 或代码围栏。必须围绕当前项目上下文写作,不套用无关行业示例。
|
||||
@@ -1,13 +0,0 @@
|
||||
根据输入 JSON 撰写软件工程本科论文 Markdown 初稿。
|
||||
Return Markdown only. Do not include acknowledgements, confirmations, explanations, or preamble before the thesis content. The first non-blank character of the response must be #.
|
||||
首个非空字符必须是 #,正文前不要确认、解释或前言。使用简体中文,篇幅参考 input.targetWords,以章节连贯性优先。
|
||||
严格按以下大纲生成,不得跳过或调换:系统设定、第 1 至第 7 章、参考文献、致谢。
|
||||
必须严格按照以下大纲生成论文初稿:系统设定、第 1 章 绪论、第 2 章 相关技术介绍、第 3 章 系统需求分析、第 4 章 系统设计、第 5 章 系统实现、第 6 章 系统测试、第 7 章 总结与展望、参考文献与致谢。
|
||||
系统设定应结合项目名称、需求、应用蓝图、闭环和数据库,说明模块、角色、核心表与流程。
|
||||
第 1 章 绪论;第 2 章 相关技术;第 3 章 系统需求分析;第 4 章 系统设计;第 5 章 系统实现;第 6 章 系统测试;第 7 章 总结与展望;参考文献;致谢。
|
||||
模块、角色、表和流程必须来自当前项目。除非项目确实包含,否则不得套用图书、读者、借阅等示例。
|
||||
Use module names, roles, and tables from the current project context. Do not use unrelated sample domain terms.
|
||||
图表位置使用“[此处插入 XX 图]”占位。不得虚构不可核验引用,明确提醒学生核验参考文献。
|
||||
合理使用标题、段落和表格;除配置片段外避免代码块。
|
||||
输入 JSON:
|
||||
{{input}}
|
||||
@@ -1 +0,0 @@
|
||||
你是中文软件工程本科毕业论文导师。只输出用户要求的 JSON,不要 Markdown 代码块或解释。
|
||||
@@ -1,16 +0,0 @@
|
||||
根据输入 JSON 生成软件工程本科论文提纲,只返回 JSON,不要 Markdown 围栏或解释。
|
||||
输出结构:{"title":"...","abstractDraft":"...","keywords":["Spring Boot","Vue"],"chapters":[{"title":"系统设定","goal":"...","sections":["模块设定","角色设定","数据表设定"]}]}。
|
||||
使用简体中文,不虚构参考文献;模块、角色、表名必须来自当前项目,不使用无关行业示例。
|
||||
chapters 必须严格按以下顺序,每项包含 2 到 5 个具体小节:
|
||||
系统设定:结合项目名称、需求描述、应用蓝图、业务闭环和数据库表,推导系统模块、用户角色、核心数据表与关键业务流程。
|
||||
第 1 章 绪论:研究背景、研究意义、国内外研究现状、研究内容与论文结构。
|
||||
第 2 章 相关技术介绍:围绕实际技术栈介绍关键技术。
|
||||
第 3 章 系统需求分析:可行性、用户角色、功能需求、非功能需求和业务流程。
|
||||
第 4 章 系统设计:总体架构、功能模块、数据库、接口和安全设计。
|
||||
第 5 章 系统实现:围绕当前项目实际模块展开。
|
||||
第 6 章 系统测试:测试环境、方法、功能测试、异常测试、兼容性测试和结论。
|
||||
第 7 章 总结与展望:成果、不足和后续优化。
|
||||
参考文献与致谢:提醒学生人工核验参考文献真实性与学校格式。
|
||||
Target a later draft of about 10000 Chinese characters.
|
||||
输入 JSON:
|
||||
{{input}}
|
||||
@@ -1 +0,0 @@
|
||||
你是资深软件需求分析师。面向本科毕设或课程设计规模,在保留用户原始描述的前提下补全真实业务闭环、角色、核心对象和可执行业务规则。规则必须给出明确的时间、数量、金额、状态、公式、上限和异常边界;用户未提供关键数值时,应给出合理默认值并标注“AI假设”,不得使用“按实际情况”“超时后处理”等模糊表述。不要把需求扩张成大型企业平台,也不要用增删改查或运维功能代替业务需求。只输出用户消息要求的中文纯文本,不要 Markdown、JSON 或解释。
|
||||
@@ -1,17 +0,0 @@
|
||||
请根据以下 JSON 项目输入为低代码生成器补全一份可直接驱动代码生成的项目需求。
|
||||
需求复杂度控制在本科毕设或课程设计级别,功能要完整但不要设计成大型企业平台或运维平台。
|
||||
currentDescription 是用户原始描述,必须作为事实来源保留其约束;输出补充内容,不要改写或否定用户已经明确的规则。
|
||||
只返回中文纯文本,不要 Markdown,不要 JSON,不要解释。格式必须严格如下:
|
||||
业务目标:一句话说明系统解决的问题
|
||||
角色:用顿号分隔 2 到 4 个用户角色
|
||||
核心对象:列出流程涉及的主要业务对象和记录
|
||||
核心流程:按先后顺序写出完整正向流程、逆向动作和补偿动作,不要只写增删改查
|
||||
业务规则:用分号分隔明确规则;必须覆盖适用的时间期限、起算点、状态判定、数量限制、金额公式、计费单位、取整方式、封顶值、重复操作限制
|
||||
异常边界:说明库存不足、状态不符、重复提交、取消、超时、重复支付等适用场景
|
||||
验收场景:至少给出一个包含边界数值和预期结果的完整场景
|
||||
如果输入没有给出期限、费率、上限等关键数值,必须选择合理默认值并在该条前标注“AI假设:”;禁止输出“待定”“按实际情况”“超时后罚款”等不可执行描述。
|
||||
对于借阅、租赁、预约等有期限的流程,必须明确起算时间、期限、到期判定和逾期处理;存在罚款或费用时必须明确公式、日费率、取整和上限。
|
||||
其他模块:用顿号分隔 2 到 4 个前台展示或内容维护模块,优先从轮播图、公告、新闻资讯、友情链接、留言反馈、帮助中心、系统简介中选择
|
||||
不要出现通知提醒、统计报表、数据备份、操作日志、系统监控、定时任务等后台运维或企业管理平台标配模块。
|
||||
项目输入 JSON:
|
||||
{{input}}
|
||||
@@ -152,24 +152,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
#{createBy}, sysdate(), #{remark})
|
||||
</insert>
|
||||
|
||||
<update id="updateInitializedV1">
|
||||
update factory_prompt_version
|
||||
set system_prompt = #{systemPrompt},
|
||||
user_prompt_contract = #{userPromptContract},
|
||||
user_prompt_template = #{userPromptTemplate},
|
||||
provider_code = #{providerCode},
|
||||
model = #{model},
|
||||
pipeline_release_code = #{pipelineReleaseCode},
|
||||
content_hash = #{contentHash},
|
||||
published_at = sysdate(),
|
||||
remark = #{remark}
|
||||
where prompt_version_id = #{promptVersionId}
|
||||
and version_no = 1
|
||||
and version_label = 'V1'
|
||||
and user_prompt_contract like 'legacy-builder:%'
|
||||
and remark in ('由当前代码内置 Prompt 初始化', '由当前代码内置业务 Prompt 初始化')
|
||||
</update>
|
||||
|
||||
<update id="retirePublishedVersions">
|
||||
update factory_prompt_version
|
||||
set status = 'RETIRED'
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package com.ruoyi.generator.factory.ai.prompt;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import org.junit.Test;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
|
||||
import com.ruoyi.generator.factory.generation.CanonicalFingerprintService;
|
||||
|
||||
public class BuiltinPromptRegistryTest
|
||||
{
|
||||
private final BuiltinPromptRegistry registry = new BuiltinPromptRegistry(
|
||||
new PromptFingerprintService(
|
||||
new CanonicalFingerprintService(new ProjectSpecJsonCodec())));
|
||||
|
||||
@Test
|
||||
public void resolvesLatestAndExactPublishedPromptIdentity()
|
||||
{
|
||||
PromptRelease latest = registry.resolve("database");
|
||||
PromptRelease exact = registry.resolve(latest.getPromptCode(), latest.getVersion());
|
||||
|
||||
assertEquals("front.database", latest.getPromptCode());
|
||||
assertEquals("1.0.0", latest.getVersion());
|
||||
assertEquals(latest.getFingerprint(), exact.getFingerprint());
|
||||
assertEquals(64, latest.getFingerprint().length());
|
||||
assertTrue(latest.getUserPromptContract().contains("context-json:database:v1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void paperPromptOwnsItsPublishedSystemInstruction()
|
||||
{
|
||||
PromptRelease paper = registry.resolve("paper_draft");
|
||||
PromptRelease database = registry.resolve("database");
|
||||
|
||||
assertTrue(paper.getSystemPrompt().contains("论文正文 Markdown"));
|
||||
assertFalse(paper.getFingerprint().equals(database.getFingerprint()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eachBusinessTypeOwnsTaskSpecificSystemInstruction()
|
||||
{
|
||||
String requirement = registry.managementSystemPrompt("requirement");
|
||||
String blueprint = registry.managementSystemPrompt("app_blueprint");
|
||||
String flow = registry.managementSystemPrompt("flow_config");
|
||||
String database = registry.managementSystemPrompt("database");
|
||||
|
||||
assertTrue(requirement.contains("AI假设"));
|
||||
assertTrue(requirement.contains("时间、数量、金额、状态、公式"));
|
||||
assertTrue(blueprint.contains("应用蓝图"));
|
||||
assertTrue(flow.contains("最小 flow_config"));
|
||||
assertTrue(flow.contains("decrease"));
|
||||
assertTrue(flow.contains("currentUserId"));
|
||||
assertTrue(flow.contains("statusField"));
|
||||
assertTrue(database.contains("MySQL"));
|
||||
assertTrue(registry.managementUserPromptTemplate("database").contains("QUANTITY_LIMIT"));
|
||||
assertFalse(requirement.equals(database));
|
||||
assertFalse(blueprint.equals(flow));
|
||||
assertTrue(registry.managementUserPromptTemplate("database").contains("数据库设计 JSON"));
|
||||
assertTrue(registry.managementUserPromptTemplate("database").contains("{{input}}"));
|
||||
assertFalse(registry.managementUserPromptTemplate("database").equals(
|
||||
registry.managementUserPromptTemplate("code_analysis")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requirementReleaseOwnsTheCompleteManagedPrompt()
|
||||
{
|
||||
PromptRelease requirement = registry.resolve("requirement");
|
||||
|
||||
assertEquals(BuiltinPromptRegistry.REQUIREMENT_SYSTEM_PROMPT,
|
||||
requirement.getSystemPrompt());
|
||||
assertEquals(BuiltinPromptRegistry.REQUIREMENT_USER_PROMPT_TEMPLATE,
|
||||
requirement.getUserPromptTemplate());
|
||||
assertTrue(requirement.getUserPromptTemplate().contains("业务规则"));
|
||||
assertTrue(requirement.getUserPromptTemplate().contains("验收场景"));
|
||||
assertTrue(requirement.getUserPromptTemplate().contains("期限、费率、上限"));
|
||||
assertTrue(requirement.getUserPromptTemplate().contains("{{input}}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnknownPromptRelease()
|
||||
{
|
||||
try
|
||||
{
|
||||
registry.resolve("front.database", "9.9.9");
|
||||
fail("Expected ServiceException");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("Prompt release does not exist"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import static org.mockito.Mockito.when;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.generator.config.PromptRegistryProperties;
|
||||
import com.ruoyi.generator.factory.ai.prompt.model.PromptVersion;
|
||||
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
|
||||
import com.ruoyi.generator.factory.generation.CanonicalFingerprintService;
|
||||
@@ -17,9 +16,7 @@ import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
|
||||
public class DatabasePromptRegistryTest
|
||||
{
|
||||
private PromptRegistryMapper mapper;
|
||||
private BuiltinPromptRegistry builtin;
|
||||
private PromptFingerprintService fingerprints;
|
||||
private PromptRegistryProperties properties;
|
||||
private DatabasePromptRegistry registry;
|
||||
|
||||
@Before
|
||||
@@ -28,23 +25,12 @@ public class DatabasePromptRegistryTest
|
||||
mapper = mock(PromptRegistryMapper.class);
|
||||
fingerprints = new PromptFingerprintService(
|
||||
new CanonicalFingerprintService(new ProjectSpecJsonCodec()));
|
||||
builtin = new BuiltinPromptRegistry(fingerprints);
|
||||
properties = new PromptRegistryProperties();
|
||||
registry = new DatabasePromptRegistry(mapper, builtin, fingerprints, properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledDatabaseUsesBuiltinRelease()
|
||||
{
|
||||
properties.setDatabaseEnabled(false);
|
||||
assertEquals(builtin.resolve("database").getFingerprint(),
|
||||
registry.resolve("database").getFingerprint());
|
||||
registry = new DatabasePromptRegistry(mapper, fingerprints);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesCurrentAndExactDatabaseRelease()
|
||||
{
|
||||
properties.setDatabaseEnabled(true);
|
||||
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
|
||||
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
|
||||
when(mapper.selectExecutableByCodeAndVersion("front.database", "2.0.0"))
|
||||
@@ -58,7 +44,6 @@ public class DatabasePromptRegistryTest
|
||||
@Test
|
||||
public void rejectsStoredContentHashDrift()
|
||||
{
|
||||
properties.setDatabaseEnabled(true);
|
||||
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
|
||||
version.setContentHash("tampered");
|
||||
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
|
||||
@@ -75,32 +60,22 @@ public class DatabasePromptRegistryTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void obsoleteLegacyBuilderReleaseIsRejectedInsteadOfSilentlyFallingBack()
|
||||
public void acceptsContractOwnedByManagedVersion()
|
||||
{
|
||||
properties.setDatabaseEnabled(true);
|
||||
PromptVersion version = version("1.0.0", PromptVersion.STATUS_PUBLISHED);
|
||||
version.setUserPromptContract("legacy-builder:database:v1");
|
||||
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
|
||||
version.setUserPromptContract("context-json:database:v2");
|
||||
version.setContentHash(fingerprints.fingerprint(version.getGenerateType(),
|
||||
version.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
|
||||
version.getUserPromptContract(), version.getUserPromptTemplate()));
|
||||
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
|
||||
|
||||
try
|
||||
{
|
||||
registry.resolve("database");
|
||||
fail("Expected incompatible stored contract");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("not implemented"));
|
||||
}
|
||||
assertEquals("context-json:database:v2",
|
||||
registry.resolve("database").getUserPromptContract());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingDatabaseReleaseFailsClosedByDefault()
|
||||
{
|
||||
properties.setDatabaseEnabled(true);
|
||||
|
||||
try
|
||||
{
|
||||
registry.resolve("database");
|
||||
@@ -112,16 +87,6 @@ public class DatabasePromptRegistryTest
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitBootstrapFallbackCanUseBuiltinRelease()
|
||||
{
|
||||
properties.setDatabaseEnabled(true);
|
||||
properties.setBuiltinFallbackEnabled(true);
|
||||
|
||||
assertEquals(builtin.resolve("database").getFingerprint(),
|
||||
registry.resolve("database").getFingerprint());
|
||||
}
|
||||
|
||||
private PromptVersion version(String label, String status)
|
||||
{
|
||||
PromptVersion version = new PromptVersion();
|
||||
@@ -132,7 +97,7 @@ public class DatabasePromptRegistryTest
|
||||
version.setVersion(label);
|
||||
version.setSystemPrompt("database system v2");
|
||||
version.setUserPromptContract("context-json:database:v1");
|
||||
version.setUserPromptTemplate(BuiltinPromptRegistry.DATABASE_USER_PROMPT_TEMPLATE);
|
||||
version.setUserPromptTemplate("{{input}}");
|
||||
version.setProviderCode("deepseek");
|
||||
version.setModel("deepseek-chat");
|
||||
version.setStatus(status);
|
||||
|
||||
@@ -23,17 +23,15 @@ public class PromptRegistrySchemaScriptTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapperKeepsVersionContentImmutableOutsideGuardedBootstrapRepair() throws Exception
|
||||
public void mapperKeepsManagedVersionContentImmutable() throws Exception
|
||||
{
|
||||
String mapper = read(repoRoot().resolve(
|
||||
"ruoyi-generator/src/main/resources/mapper/factory/PromptRegistryMapper.xml"));
|
||||
assertTrue(mapper.contains("insertPromptVersion"));
|
||||
assertTrue(mapper.contains("publishPromptVersion"));
|
||||
assertTrue(mapper.contains("retirePublishedVersions"));
|
||||
assertTrue(mapper.contains("updateInitializedV1"));
|
||||
assertTrue(mapper.contains("version_label = 'V1'"));
|
||||
assertTrue(mapper.contains("user_prompt_contract like 'legacy-builder:%'"));
|
||||
assertTrue(mapper.contains("remark in ('由当前代码内置 Prompt 初始化', '由当前代码内置业务 Prompt 初始化')"));
|
||||
assertFalse(mapper.contains("updateInitializedV1"));
|
||||
assertFalse(mapper.contains("legacy-builder:"));
|
||||
assertFalse(mapper.contains("updatePromptVersionContent"));
|
||||
}
|
||||
|
||||
@@ -47,7 +45,7 @@ public class PromptRegistrySchemaScriptTest
|
||||
String completeDatabase = read(root.resolve("sql/db.sql"));
|
||||
assertTrue(controller.contains("/versions/{promptVersionId}/publish"));
|
||||
assertTrue(controller.contains("/versions/{promptVersionId}/rollback"));
|
||||
assertTrue(controller.contains("/initialize-v1"));
|
||||
assertFalse(controller.contains("/initialize-v1"));
|
||||
assertTrue(menu.contains("generator:prompt:publish"));
|
||||
assertTrue(menu.contains("generator:prompt:rollback"));
|
||||
assertTrue(completeDatabase.contains("'Prompt 中心'"));
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.ruoyi.generator.factory.ai.stage.oneclick;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.Test;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.ruoyi.generator.domain.front.AiGenerationTask;
|
||||
import com.ruoyi.generator.domain.front.dto.AiGenerationTaskCreateRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateFlowConfigRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationResult;
|
||||
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
|
||||
import com.ruoyi.generator.service.front.FlowConfigPageBindingReconciler;
|
||||
import com.ruoyi.generator.service.front.IAiGenerateService;
|
||||
|
||||
public class DesignStageHandlerTest
|
||||
{
|
||||
@Test
|
||||
public void publishesReconciledBlueprintToFollowingOneClickStages()
|
||||
{
|
||||
IAiGenerateService ai = mock(IAiGenerateService.class);
|
||||
AppBlueprintDesign blueprint = blueprint();
|
||||
SimpleFlowConfig flow = flow();
|
||||
when(ai.generateAppBlueprint(eq(7L), eq(11L), any(GenerateAppBlueprintRequest.class)))
|
||||
.thenReturn(blueprint);
|
||||
when(ai.generateFlowConfig(eq(7L), eq(11L), any(GenerateFlowConfigRequest.class)))
|
||||
.thenReturn(flow);
|
||||
|
||||
AiGenerationTask task = new AiGenerationTask();
|
||||
task.setUserId(7L);
|
||||
task.setProjectId(11L);
|
||||
OneClickGenerationContext context = new OneClickGenerationContext(task,
|
||||
new AiGenerationTaskCreateRequest(), new OneClickProjectGenerationResult());
|
||||
|
||||
new DesignStageHandler(ai, new FlowConfigPageBindingReconciler()).execute(context);
|
||||
|
||||
AppBlueprintDesign reconciled = JSON.parseObject(context.getAppBlueprintJson(), AppBlueprintDesign.class);
|
||||
assertNotNull(findPage(reconciled, "event_detail_page"));
|
||||
assertEquals("event_detail_page", context.getFlowConfig().getActions().get(0).getButton().getPageCode());
|
||||
assertEquals("detail.primaryActions", context.getFlowConfig().getActions().get(0).getButton().getSlot());
|
||||
}
|
||||
|
||||
private AppBlueprintDesign blueprint()
|
||||
{
|
||||
AppBlueprintDesign blueprint = new AppBlueprintDesign();
|
||||
AppPageDesign page = new AppPageDesign();
|
||||
page.setCode("event_catalog_page");
|
||||
page.setName("活动列表");
|
||||
page.setMenuCode("events");
|
||||
page.setPath("/events");
|
||||
page.setPageType("list");
|
||||
page.setTableName("event_info");
|
||||
blueprint.getFrontendPages().add(page);
|
||||
return blueprint;
|
||||
}
|
||||
|
||||
private SimpleFlowConfig flow()
|
||||
{
|
||||
return JSON.parseObject("{\"version\":\"1.1\",\"code\":\"event_signup\","
|
||||
+ "\"name\":\"活动报名\",\"mainTable\":\"event_signup\",\"statusField\":\"status\","
|
||||
+ "\"states\":[{\"code\":\"pending\",\"label\":\"待处理\"},"
|
||||
+ "{\"code\":\"cancelled\",\"label\":\"已取消\"}],\"actions\":[{"
|
||||
+ "\"code\":\"signup\",\"name\":\"报名\",\"actor\":\"member\","
|
||||
+ "\"ownerTable\":\"event_info\",\"effects\":[{\"type\":\"create_record\","
|
||||
+ "\"table\":\"event_signup\",\"values\":{\"status\":\"pending\"}}],"
|
||||
+ "\"button\":{\"scope\":\"member\",\"pageCode\":\"event_catalog_page\","
|
||||
+ "\"tableName\":\"event_info\",\"slot\":\"list.rowActions\"}}]}",
|
||||
SimpleFlowConfig.class);
|
||||
}
|
||||
|
||||
private AppPageDesign findPage(AppBlueprintDesign blueprint, String code)
|
||||
{
|
||||
for (AppPageDesign page : blueprint.getFrontendPages())
|
||||
{
|
||||
if (code.equals(page.getCode())) return page;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -209,6 +209,56 @@ public class FlowConfigCompilerTest
|
||||
assertTrue(compilation.getLoopPlan().getMetrics().get(0).getFilters().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sanitizerDefaultsSelfUpdateRelationFieldToOwnerRequestId()
|
||||
{
|
||||
SimpleFlowConfig config = JSON.parseObject("{"
|
||||
+ "\"version\":\"1.1\",\"code\":\"book_return\",\"name\":\"Book return\","
|
||||
+ "\"mainTable\":\"borrow_record\",\"statusField\":\"status\","
|
||||
+ "\"states\":[{\"code\":\"borrowing\",\"label\":\"Borrowing\"},"
|
||||
+ "{\"code\":\"returned\",\"label\":\"Returned\"}],"
|
||||
+ "\"actions\":[{\"code\":\"return_book\",\"name\":\"Return\","
|
||||
+ "\"from\":\"borrowing\",\"to\":\"returned\",\"actor\":\"reader\","
|
||||
+ "\"ownerTable\":\"borrow_record\",\"requestFields\":[\"id\"],"
|
||||
+ "\"effects\":[{\"type\":\"update_related\",\"table\":\"borrow_record\","
|
||||
+ "\"values\":{\"return_date\":\"now\"}}]}],\"metrics\":[]}",
|
||||
SimpleFlowConfig.class);
|
||||
|
||||
new FlowConfigSanitizer().sanitize(config);
|
||||
|
||||
assertEquals("id", config.getActions().get(0).getEffects().get(0).getRelationField());
|
||||
new FlowConfigValidator().validate(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sanitizerDoesNotGuessCrossTableRelationField()
|
||||
{
|
||||
SimpleFlowConfig config = JSON.parseObject("{"
|
||||
+ "\"version\":\"1.1\",\"code\":\"book_return\",\"name\":\"Book return\","
|
||||
+ "\"mainTable\":\"borrow_record\",\"statusField\":\"status\","
|
||||
+ "\"states\":[{\"code\":\"borrowing\",\"label\":\"Borrowing\"},"
|
||||
+ "{\"code\":\"returned\",\"label\":\"Returned\"}],"
|
||||
+ "\"actions\":[{\"code\":\"return_book\",\"name\":\"Return\","
|
||||
+ "\"from\":\"borrowing\",\"to\":\"returned\",\"actor\":\"reader\","
|
||||
+ "\"ownerTable\":\"borrow_record\",\"requestFields\":[\"id\"],"
|
||||
+ "\"effects\":[{\"type\":\"update_related\",\"table\":\"fine_record\","
|
||||
+ "\"values\":{\"paid_at\":\"now\"}}]}],\"metrics\":[]}",
|
||||
SimpleFlowConfig.class);
|
||||
|
||||
new FlowConfigSanitizer().sanitize(config);
|
||||
|
||||
assertNull(config.getActions().get(0).getEffects().get(0).getRelationField());
|
||||
try
|
||||
{
|
||||
new FlowConfigValidator().validate(config);
|
||||
fail("Expected cross-table relationField validation to fail");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("effect.relationField"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compilesReservationAndBorrowingAsTwoIndependentFlows()
|
||||
{
|
||||
|
||||
@@ -7,9 +7,7 @@ import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import java.util.Collections;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
@@ -17,7 +15,6 @@ import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.generator.factory.ai.model.ModelGateway;
|
||||
import com.ruoyi.generator.factory.ai.model.ModelGatewayRouter;
|
||||
import com.ruoyi.generator.factory.ai.prompt.PromptFingerprintService;
|
||||
import com.ruoyi.generator.factory.ai.prompt.BuiltinPromptRegistry;
|
||||
import com.ruoyi.generator.factory.ai.prompt.model.PromptTemplate;
|
||||
import com.ruoyi.generator.factory.ai.prompt.model.PromptVersion;
|
||||
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
|
||||
@@ -38,8 +35,7 @@ public class PromptTemplateServiceImplTest
|
||||
gatewayRouter = mock(ModelGatewayRouter.class);
|
||||
fingerprints = new PromptFingerprintService(
|
||||
new CanonicalFingerprintService(new ProjectSpecJsonCodec()));
|
||||
service = new PromptTemplateServiceImpl(mapper, fingerprints, gatewayRouter,
|
||||
new BuiltinPromptRegistry(fingerprints));
|
||||
service = new PromptTemplateServiceImpl(mapper, fingerprints, gatewayRouter);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -126,7 +122,7 @@ public class PromptTemplateServiceImplTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnimplementedUserPromptContractBeforePublication()
|
||||
public void publishesContractDefinedByManagedVersion()
|
||||
{
|
||||
PromptVersion draft = version(202L, PromptVersion.STATUS_DRAFT);
|
||||
draft.setUserPromptContract("context-json:database:v2");
|
||||
@@ -134,88 +130,11 @@ public class PromptTemplateServiceImplTest
|
||||
draft.getVersion(), draft.getSystemPrompt(), draft.getUserPromptContract()));
|
||||
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template());
|
||||
when(mapper.selectPromptVersionById(202L)).thenReturn(draft);
|
||||
|
||||
try
|
||||
{
|
||||
service.publishPromptVersion(101L, 202L, "admin");
|
||||
fail("Expected unsupported builder contract");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("not implemented"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializesCurrentBuiltinPromptAsPublishedV1OnlyForEmptyTemplate()
|
||||
{
|
||||
PromptTemplate template = template();
|
||||
when(mapper.selectPromptTemplateList(any(PromptTemplate.class)))
|
||||
.thenReturn(Collections.singletonList(template));
|
||||
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template);
|
||||
when(mapper.selectMaxVersionNo(101L)).thenReturn(0);
|
||||
when(gatewayRouter.resolve("deepseek")).thenReturn(mock(ModelGateway.class));
|
||||
when(mapper.insertPromptVersion(any(PromptVersion.class))).thenAnswer(invocation ->
|
||||
{
|
||||
PromptVersion value = invocation.getArgument(0);
|
||||
value.setPromptVersionId(301L);
|
||||
return 1;
|
||||
});
|
||||
when(mapper.publishPromptVersion(301L, "admin")).thenReturn(1);
|
||||
when(mapper.updateCurrentVersion(101L, 301L, "admin")).thenReturn(1);
|
||||
when(mapper.publishPromptVersion(202L, "admin")).thenReturn(1);
|
||||
when(mapper.updateCurrentVersion(101L, 202L, "admin")).thenReturn(1);
|
||||
|
||||
assertEquals(1, service.initializeBuiltinV1("admin"));
|
||||
|
||||
InOrder order = inOrder(mapper);
|
||||
order.verify(mapper).insertPromptVersion(any(PromptVersion.class));
|
||||
order.verify(mapper).publishPromptVersion(301L, "admin");
|
||||
order.verify(mapper).updateCurrentVersion(101L, 301L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repairsOnlyTheOriginalGenericSystemInitializedV1()
|
||||
{
|
||||
PromptTemplate template = template();
|
||||
PromptVersion oldV1 = version(301L, PromptVersion.STATUS_PUBLISHED);
|
||||
oldV1.setVersionNo(1);
|
||||
oldV1.setVersion("V1");
|
||||
oldV1.setUserPromptContract("legacy-builder:database:v1");
|
||||
oldV1.setRemark("由当前代码内置 Prompt 初始化");
|
||||
when(mapper.selectPromptTemplateList(any(PromptTemplate.class)))
|
||||
.thenReturn(Collections.singletonList(template));
|
||||
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template);
|
||||
when(mapper.selectMaxVersionNo(101L)).thenReturn(1);
|
||||
when(mapper.selectPromptVersions(101L)).thenReturn(Collections.singletonList(oldV1));
|
||||
when(gatewayRouter.resolve("deepseek")).thenReturn(mock(ModelGateway.class));
|
||||
when(mapper.updateInitializedV1(oldV1)).thenReturn(1);
|
||||
|
||||
assertEquals(1, service.initializeBuiltinV1("admin"));
|
||||
assertTrue(oldV1.getSystemPrompt().contains("MySQL"));
|
||||
assertEquals("由当前代码内置业务 Prompt 初始化", oldV1.getRemark());
|
||||
verify(mapper).updateInitializedV1(oldV1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repairsTheOriginalBusinessSystemInitializedLegacyV1()
|
||||
{
|
||||
PromptTemplate template = template();
|
||||
PromptVersion oldV1 = version(301L, PromptVersion.STATUS_PUBLISHED);
|
||||
oldV1.setVersionNo(1);
|
||||
oldV1.setVersion("V1");
|
||||
oldV1.setUserPromptContract("legacy-builder:database:v1");
|
||||
oldV1.setRemark("由当前代码内置业务 Prompt 初始化");
|
||||
when(mapper.selectPromptTemplateList(any(PromptTemplate.class)))
|
||||
.thenReturn(Collections.singletonList(template));
|
||||
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template);
|
||||
when(mapper.selectMaxVersionNo(101L)).thenReturn(1);
|
||||
when(mapper.selectPromptVersions(101L)).thenReturn(Collections.singletonList(oldV1));
|
||||
when(gatewayRouter.resolve("deepseek")).thenReturn(mock(ModelGateway.class));
|
||||
when(mapper.updateInitializedV1(oldV1)).thenReturn(1);
|
||||
|
||||
assertEquals(1, service.initializeBuiltinV1("admin"));
|
||||
assertEquals("context-json:database:v1", oldV1.getUserPromptContract());
|
||||
assertEquals("由当前代码内置业务 Prompt 初始化", oldV1.getRemark());
|
||||
verify(mapper).updateInitializedV1(oldV1);
|
||||
assertEquals(1, service.publishPromptVersion(101L, 202L, "admin"));
|
||||
}
|
||||
|
||||
private PromptTemplate template()
|
||||
|
||||
@@ -7,11 +7,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.junit.Test;
|
||||
import com.ruoyi.generator.factory.ai.prompt.BuiltinPromptRegistry;
|
||||
import com.ruoyi.generator.factory.ai.prompt.PromptFingerprintService;
|
||||
import com.ruoyi.generator.factory.ai.prompt.PromptRelease;
|
||||
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
|
||||
import com.ruoyi.generator.factory.generation.CanonicalFingerprintService;
|
||||
|
||||
public class AiPromptExternalizationTest
|
||||
{
|
||||
@@ -30,35 +25,19 @@ public class AiPromptExternalizationTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyExecutableGenerationTypeOwnsManagedTemplates()
|
||||
public void runtimeRegistryHasNoBuiltinPromptFallback() throws Exception
|
||||
{
|
||||
BuiltinPromptRegistry registry = new BuiltinPromptRegistry(new PromptFingerprintService(
|
||||
new CanonicalFingerprintService(new ProjectSpecJsonCodec())));
|
||||
String[] types = new String[] { "requirement", "app_blueprint", "flow_config", "database",
|
||||
"diagram_spec", "paper_outline", "paper_draft", "code_analysis" };
|
||||
Path source = resolve(
|
||||
"ruoyi-generator/src/main/java/com/ruoyi/generator/factory/ai/prompt/DatabasePromptRegistry.java",
|
||||
"src/main/java/com/ruoyi/generator/factory/ai/prompt/DatabasePromptRegistry.java");
|
||||
String content = new String(Files.readAllBytes(source), StandardCharsets.UTF_8);
|
||||
|
||||
for (String type : types)
|
||||
{
|
||||
PromptRelease release = registry.resolve(type);
|
||||
assertTrue(type, release.getSystemPrompt() != null && !release.getSystemPrompt().trim().isEmpty());
|
||||
assertTrue(type, release.getUserPromptTemplate() != null
|
||||
&& release.getUserPromptTemplate().contains("{{input}}"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flowConfigPromptRequiresExplicitStateTransitions()
|
||||
{
|
||||
BuiltinPromptRegistry registry = new BuiltinPromptRegistry(new PromptFingerprintService(
|
||||
new CanonicalFingerprintService(new ProjectSpecJsonCodec())));
|
||||
|
||||
PromptRelease release = registry.resolve("flow_config");
|
||||
|
||||
assertTrue(release.getSystemPrompt().contains("每个 action 必须有 from 和 to"));
|
||||
assertTrue(release.getSystemPrompt().contains("不能作为前端可传字段"));
|
||||
assertTrue(release.getSystemPrompt().contains("metrics.status 如果填写"));
|
||||
assertTrue(release.getSystemPrompt().contains("create_time、created_at 等创建时间字段必须写为 now"));
|
||||
assertTrue(release.getSystemPrompt().contains("\"create_time\":\"now\""));
|
||||
assertTrue(content.contains("selectPublishedByGenerateType"));
|
||||
assertFalse(content.contains("BuiltinPromptRegistry"));
|
||||
assertFalse(content.contains("PromptRegistryProperties"));
|
||||
Path resources = resolve("ruoyi-generator/src/main/resources/factory-prompts",
|
||||
"src/main/resources/factory-prompts");
|
||||
assertFalse(Files.exists(resources.resolve("default/system.txt")));
|
||||
}
|
||||
|
||||
private Path resolve(String repositoryPath, String modulePath)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
|
||||
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
|
||||
import com.ruoyi.generator.factory.flow.FlowConfigCompilation;
|
||||
|
||||
public class FlowConfigPageBindingReconcilerTest
|
||||
{
|
||||
@Test
|
||||
public void createsGenericResourceDetailPageAndMovesInitiatingAction()
|
||||
{
|
||||
AppBlueprintDesign blueprint = blueprint(false);
|
||||
SimpleFlowConfig config = flowConfig();
|
||||
|
||||
FlowConfigCompilation compilation = new FlowConfigContractService().canonicalize(config, blueprint);
|
||||
|
||||
SimpleFlowConfig.Action apply = config.getActions().get(0);
|
||||
assertEquals("course_detail_page", apply.getButton().getPageCode());
|
||||
assertEquals("detail.primaryActions", apply.getButton().getSlot());
|
||||
AppPageDesign detail = page(blueprint, "course_detail_page");
|
||||
assertNotNull(detail);
|
||||
assertEquals("detail", detail.getPageType());
|
||||
assertEquals("course_info", detail.getTableName());
|
||||
assertEquals("course_catalog_page", detail.getParentPageCode());
|
||||
assertEquals("/courses/:id", detail.getPath());
|
||||
|
||||
SimpleFlowConfig.Action cancel = config.getActions().get(1);
|
||||
assertEquals("my_registrations_page", cancel.getButton().getPageCode());
|
||||
assertEquals("list.rowActions", cancel.getButton().getSlot());
|
||||
BusinessLoopPlan.BusinessLoopPageBinding binding = compilation.getLoopPlan().getPageBindings().get(0);
|
||||
assertEquals("course_detail_page", binding.getPageCode());
|
||||
assertEquals("detail.primaryActions", binding.getSlot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reusesExistingDetailPageWithoutCreatingDuplicate()
|
||||
{
|
||||
AppBlueprintDesign blueprint = blueprint(true);
|
||||
SimpleFlowConfig config = flowConfig();
|
||||
|
||||
boolean changed = new FlowConfigPageBindingReconciler().reconcile(config, blueprint);
|
||||
|
||||
assertTrue(changed);
|
||||
assertEquals("custom_course_view", config.getActions().get(0).getButton().getPageCode());
|
||||
assertEquals("detail.primaryActions", config.getActions().get(0).getButton().getSlot());
|
||||
assertEquals(3, blueprint.getFrontendPages().size());
|
||||
}
|
||||
|
||||
@Test(expected = com.ruoyi.common.exception.ServiceException.class)
|
||||
public void rejectsSlotThatDoesNotMatchBoundPageType()
|
||||
{
|
||||
AppBlueprintDesign blueprint = blueprint(false);
|
||||
SimpleFlowConfig config = flowConfig();
|
||||
SimpleFlowConfig.Action cancel = config.getActions().get(1);
|
||||
cancel.getButton().setSlot("detail.primaryActions");
|
||||
|
||||
new FlowConfigContractService().canonicalize(config, blueprint);
|
||||
}
|
||||
|
||||
private AppBlueprintDesign blueprint(boolean includeDetail)
|
||||
{
|
||||
AppBlueprintDesign blueprint = new AppBlueprintDesign();
|
||||
blueprint.getFrontendPages().add(page("course_catalog_page", "课程目录", "/courses", "list",
|
||||
"course_info", "course_catalog", ""));
|
||||
blueprint.getFrontendPages().add(page("my_registrations_page", "我的报名", "/my/registrations",
|
||||
"current_user_list", "registration", "my_registrations", ""));
|
||||
if (includeDetail)
|
||||
{
|
||||
blueprint.getFrontendPages().add(page("custom_course_view", "课程介绍", "/courses/:id",
|
||||
"detail", "course_info", "course_catalog", "course_catalog_page"));
|
||||
}
|
||||
return blueprint;
|
||||
}
|
||||
|
||||
private AppPageDesign page(String code, String name, String path, String type, String table,
|
||||
String menuCode, String parentPageCode)
|
||||
{
|
||||
AppPageDesign page = new AppPageDesign();
|
||||
page.setCode(code);
|
||||
page.setName(name);
|
||||
page.setPath(path);
|
||||
page.setPageType(type);
|
||||
page.setTableName(table);
|
||||
page.setMenuCode(menuCode);
|
||||
page.setParentPageCode(parentPageCode);
|
||||
return page;
|
||||
}
|
||||
|
||||
private AppPageDesign page(AppBlueprintDesign blueprint, String code)
|
||||
{
|
||||
for (AppPageDesign page : blueprint.getFrontendPages())
|
||||
{
|
||||
if (page != null && code.equals(page.getCode())) return page;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SimpleFlowConfig flowConfig()
|
||||
{
|
||||
return JSON.parseObject("{"
|
||||
+ "\"version\":\"1.1\",\"code\":\"course_registration\",\"name\":\"课程报名\","
|
||||
+ "\"mainTable\":\"registration\",\"statusField\":\"status\","
|
||||
+ "\"states\":[{\"code\":\"pending\",\"label\":\"待处理\"},"
|
||||
+ "{\"code\":\"cancelled\",\"label\":\"已取消\"}],"
|
||||
+ "\"actions\":[{\"code\":\"apply_course\",\"name\":\"报名\",\"actor\":\"student\","
|
||||
+ "\"ownerTable\":\"course_info\",\"requestFields\":[\"id\"],"
|
||||
+ "\"effects\":[{\"type\":\"create_record\",\"table\":\"registration\","
|
||||
+ "\"values\":{\"course_id\":\"record.id\",\"student_id\":\"currentUserId\","
|
||||
+ "\"status\":\"pending\"}}],\"button\":{\"scope\":\"student\","
|
||||
+ "\"pageCode\":\"course_catalog_page\",\"tableName\":\"course_info\","
|
||||
+ "\"slot\":\"list.rowActions\"}},{\"code\":\"cancel_registration\","
|
||||
+ "\"name\":\"取消报名\",\"actor\":\"student\",\"ownerTable\":\"registration\","
|
||||
+ "\"requestFields\":[\"id\"],\"from\":\"pending\",\"to\":\"cancelled\","
|
||||
+ "\"effects\":[],\"button\":{\"scope\":\"student\","
|
||||
+ "\"pageCode\":\"my_registrations_page\",\"tableName\":\"registration\","
|
||||
+ "\"slot\":\"list.rowActions\"}}],\"metrics\":[]}", SimpleFlowConfig.class);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.generator.config.DeepSeekProperties;
|
||||
|
||||
public class HttpDeepSeekClientTest
|
||||
@@ -130,6 +131,22 @@ public class HttpDeepSeekClientTest
|
||||
assertTrue(request.getJSONObject("stream_options").getBooleanValue("include_usage"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsInvocationWithoutManagedSystemPrompt() throws Exception
|
||||
{
|
||||
HttpDeepSeekClient client = client(false);
|
||||
|
||||
try
|
||||
{
|
||||
client.chatModel("deepseek-chat", " ", "prompt");
|
||||
fail("Expected an explicit managed system Prompt");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertTrue(e.getMessage().contains("published managed Prompt version"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseContentRecordsUsageFromNonStreamingResponse() throws Exception
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user