feat: 完善 Prompt 管理与生成流程配置
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
{"type":"server-started","port":64796,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:64796","screen_dir":"D:\\codes\\yidaima\\RuoYi-Vue\\.superpowers\\brainstorm\\1712695621-1782569508\\content","state_dir":"D:\\codes\\yidaima\\RuoYi-Vue\\.superpowers\\brainstorm\\1712695621-1782569508\\state"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
13900
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
37624
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"reason":"idle timeout","timestamp":1781882785790}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
807
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"reason":"idle timeout","timestamp":1783312098015}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"reason":"idle timeout","timestamp":1783410216217}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
31444
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"type":"server-started","port":60598,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:60598","screen_dir":"D:\\codes\\yidaima\\RuoYi-Vue\\.superpowers\\brainstorm\\codex-22612-20260707133826\\content","state_dir":"D:\\codes\\yidaima\\RuoYi-Vue\\.superpowers\\brainstorm\\codex-22612-20260707133826\\state"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
18352
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
35088
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
13008
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
49784
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"type":"server-started","port":51050,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:51050","screen_dir":"D:\\codes\\yidaima\\RuoYi-Vue\\.superpowers\\brainstorm\\codex-frontend-reorg-1779977251\\content","state_dir":"D:\\codes\\yidaima\\RuoYi-Vue\\.superpowers\\brainstorm\\codex-frontend-reorg-1779977251\\state"}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{"type":"click","text":"A. 集成到现有生成工作台\n \n \n \n 菜单树\n 图书目录\n 我的借阅\n 订单记录\n \n \n 页面画布\n 搜索区:书名 / 分类 / 状态\n 工具栏:新增 / 导出 / 批量删除\n 表格列:书名、作者、库存、状态、操作\n \n \n 属性面板\n \n \n 保存设计\n \n \n \n \n \n \n A. 轻量集成版(推荐先做)\n 在 easycode-web 的生成页中新增“页面设计”步骤,复用现有 appBlueprint、database、businessActions。实现最快,风险最低。","choice":"a","id":null,"timestamp":1779862739299}
|
||||||
|
{"type":"click","text":"A. 集成到现有生成工作台\n \n \n \n 菜单树\n 图书目录\n 我的借阅\n 订单记录\n \n \n 页面画布\n 搜索区:书名 / 分类 / 状态\n 工具栏:新增 / 导出 / 批量删除\n 表格列:书名、作者、库存、状态、操作\n \n \n 属性面板\n \n \n 保存设计\n \n \n \n \n \n \n A. 轻量集成版(推荐先做)\n 在 easycode-web 的生成页中新增“页面设计”步骤,复用现有 appBlueprint、database、businessActions。实现最快,风险最低。","choice":"a","id":null,"timestamp":1779862742233}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"type":"server-started","port":57643,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:57643","screen_dir":"D:\\codes\\yidaima\\RuoYi-Vue\\.superpowers\\brainstorm\\manual-31932-20260527140727\\content","state_dir":"D:\\codes\\yidaima\\RuoYi-Vue\\.superpowers\\brainstorm\\manual-31932-20260527140727\\state"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
19976
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
29288
|
||||||
@@ -10,11 +10,9 @@
|
|||||||
- 在线调试:草稿、已发布和已退役版本均可测试,记录输入、输出、模型、耗时、成功状态和人工评分。
|
- 在线调试:草稿、已发布和已退役版本均可测试,记录输入、输出、模型、耗时、成功状态和人工评分。
|
||||||
- A/B 测试:同一模板下选择两个不可变版本,用相同输入运行对比,统计成功次数、平均耗时与人工评分。
|
- A/B 测试:同一模板下选择两个不可变版本,用相同输入运行对比,统计成功次数、平均耗时与人工评分。
|
||||||
|
|
||||||
## 初始化 V1
|
## 唯一来源
|
||||||
|
|
||||||
首次启用后,在 Prompt 中心点击“初始化 V1”。系统会把每个生成类型各自的业务 Prompt 写成 `V1` 并立即发布,包括对应任务、输出格式和业务约束的 System Prompt、Builder 合约、`{{input}}` User Prompt 模板和模型网关配置。
|
Prompt 中心的数据库发布版是运行时唯一来源。系统不提供代码内置 Prompt、初始化 V1 或缺失回退;模板没有当前已发布版本、被停用或版本内容校验失败时,生成任务会明确失败。首个版本应通过部署 SQL 导入,或由管理员在 Prompt 中心创建并发布。
|
||||||
|
|
||||||
初始化是幂等的:处理完全没有版本的模板;早期由系统生成且内容同质化的唯一 V1 会被安全修复。已有人工草稿、后续版本或实验版本的模板都会跳过,不覆盖后台维护内容。
|
|
||||||
|
|
||||||
## 数据库升级
|
## 数据库升级
|
||||||
|
|
||||||
@@ -25,13 +23,13 @@
|
|||||||
3. `sql/factory_prompt_lab.sql`
|
3. `sql/factory_prompt_lab.sql`
|
||||||
4. `sql/prompt_registry_menu.sql`
|
4. `sql/prompt_registry_menu.sql`
|
||||||
|
|
||||||
然后将 `factory.prompt-registry.database-enabled`(环境变量 `FACTORY_PROMPT_REGISTRY_DATABASE_ENABLED`)设为 `true`,数据库中的已发布版本才会替代内置 Prompt。关闭时仍使用应用内置资源版本作为安全后备。
|
完成升级后,确认每个正在使用的生成类型都有当前已发布版本。运行时始终读取数据库,不存在切换到代码内置 Prompt 的配置项。
|
||||||
|
|
||||||
## 结构化上下文契约
|
## 结构化上下文契约
|
||||||
|
|
||||||
前台 AI 生成入口只向 Prompt Registry 传递 JSON 动态上下文。角色设定、输出格式、字段约束、禁止项和纠错规则均由 Prompt 中心的 System Prompt 与 User Prompt 模板控制,User Prompt 模板必须保留 `{{input}}`。
|
前台 AI 生成入口只向 Prompt Registry 传递 JSON 动态上下文。角色设定、输出格式、字段约束、禁止项和纠错规则均由 Prompt 中心的 System Prompt 与 User Prompt 模板控制,User Prompt 模板必须保留 `{{input}}`。
|
||||||
|
|
||||||
当前可执行契约使用 `context-json:<generateType>:v1`。升级前发布的 `legacy-builder:*` 版本依赖 Java 内置规则,系统会将其视为过期版本并安全回退到资源模板;需要在 Prompt 中心基于新的 JSON 契约创建并发布新版本,才能重新由数据库版本接管。
|
当前版本通常使用 `context-json:<generateType>:v1`。契约和 Prompt 正文均随后台版本保存;升级前的 `legacy-builder:*` 版本不会触发代码回退,应在 Prompt 中心创建并发布完整的新版本。
|
||||||
|
|
||||||
## 权限
|
## 权限
|
||||||
|
|
||||||
|
|||||||
@@ -1195,20 +1195,20 @@ P1-B 已完成,见下方记录。
|
|||||||
|
|
||||||
- 新增 `factory_prompt_template` 与 `factory_prompt_version`:模板保存稳定 Prompt code、生成类型、启用状态和当前发布指针,版本保存不可变的 system Prompt、user Prompt builder 合约、provider/model、规范化 SHA-256 和发布状态。
|
- 新增 `factory_prompt_template` 与 `factory_prompt_version`:模板保存稳定 Prompt code、生成类型、启用状态和当前发布指针,版本保存不可变的 system Prompt、user Prompt builder 合约、provider/model、规范化 SHA-256 和发布状态。
|
||||||
- Prompt 版本正文创建后不可更新;版本号在模板行锁内单调递增,草稿发布与历史版本回滚会在同一事务中退役旧版本、切换状态并移动当前指针。
|
- Prompt 版本正文创建后不可更新;版本号在模板行锁内单调递增,草稿发布与历史版本回滚会在同一事务中退役旧版本、切换状态并移动当前指针。
|
||||||
- 新增 `PromptFingerprintService`,内置发布、数据库发布和发布服务复用同一规范化指纹算法;运行时会重新计算数据库内容哈希并拒绝漂移。
|
- 新增 `PromptFingerprintService`,数据库发布和发布服务复用同一规范化指纹算法;运行时会重新计算数据库内容哈希并拒绝漂移。
|
||||||
- 新增主运行实现 `DatabasePromptRegistry`:显式开启数据库开关后按生成类型解析当前发布,并支持按 Prompt code/version 精确解析已发布或已退役版本,保证排队任务可重放。
|
- 新增主运行实现 `DatabasePromptRegistry`:始终按生成类型解析数据库当前发布,并支持按 Prompt code/version 精确解析已发布或已退役版本,保证排队任务可重放。
|
||||||
- 数据库功能未开启、没有当前数据库发布或模板被停用时继续回落到内置 Prompt,确保迁移前和逐步启用期间不阻断现有生成链路。
|
- 没有当前数据库发布或模板被停用时直接失败,不回落到代码内置 Prompt。
|
||||||
- 发布与回滚前校验内容指纹、当前代码真实实现的 user Prompt builder 合约和已注册 ModelGateway provider,避免发布无法执行的虚假版本。
|
- 发布与回滚前校验内容指纹和已注册 ModelGateway provider,避免发布无法执行的虚假版本。
|
||||||
- 新增带权限控制的模板列表、详情、创建、元数据编辑、版本创建、发布和回滚 API;Prompt code 与生成类型创建后不可变。
|
- 新增带权限控制的模板列表、详情、创建、元数据编辑、版本创建、发布和回滚 API;Prompt code 与生成类型创建后不可变。
|
||||||
- 新增 RuoYi Prompt 管理页面和 API:可搜索模板、查看当前发布与指纹、创建不可变草稿、发布草稿、回滚已退役版本,并从内置发布预填首个数据库版本。
|
- 新增 RuoYi Prompt 管理页面和 API:可搜索模板、查看当前发布与指纹、创建不可变草稿、发布草稿、回滚已退役版本;新版本只从当前后台版本复制内容。
|
||||||
- 新增 `prompt_registry_menu.sql` 菜单权限脚本;`factory_prompt_registry.sql` 提供现有库升级和 11 类内置生成用途的模板元数据种子,完整建库脚本同步更新。
|
- 新增 `prompt_registry_menu.sql` 菜单权限脚本;`factory_prompt_registry.sql` 提供现有库升级和 11 类内置生成用途的模板元数据种子,完整建库脚本同步更新。
|
||||||
|
|
||||||
启用顺序:
|
启用顺序:
|
||||||
|
|
||||||
1. 执行 `sql/factory_prompt_registry.sql` 创建表并写入模板元数据。
|
1. 执行 `sql/factory_prompt_registry.sql` 创建表并写入模板元数据。
|
||||||
2. 执行 `sql/prompt_registry_menu.sql` 创建 Prompt 管理菜单和权限。
|
2. 执行 `sql/prompt_registry_menu.sql` 创建 Prompt 管理菜单和权限。
|
||||||
3. 设置 `FACTORY_PROMPT_REGISTRY_DATABASE_ENABLED=true`。
|
3. 在管理页或部署 SQL 中为每个生成类型创建并发布数据库版本。
|
||||||
4. 重启后端;首个数据库版本可在管理页基于内置发布创建并发布。
|
4. 启动生成任务;缺少发布版时系统会明确报错。
|
||||||
|
|
||||||
验证结果:
|
验证结果:
|
||||||
|
|
||||||
|
|||||||
8
RuoYi-Vue/easycode-web/.vite/deps/_metadata.json
Normal file
8
RuoYi-Vue/easycode-web/.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hash": "8f672fc6",
|
||||||
|
"configHash": "35975680",
|
||||||
|
"lockfileHash": "ed79ecfc",
|
||||||
|
"browserHash": "7a57d982",
|
||||||
|
"optimized": {},
|
||||||
|
"chunks": {}
|
||||||
|
}
|
||||||
3
RuoYi-Vue/easycode-web/.vite/deps/package.json
Normal file
3
RuoYi-Vue/easycode-web/.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
@@ -63,9 +63,24 @@
|
|||||||
/>
|
/>
|
||||||
<el-collapse v-model="oneClickAdvancedSections" class="advanced-settings">
|
<el-collapse v-model="oneClickAdvancedSections" class="advanced-settings">
|
||||||
<el-collapse-item title="高级设置" name="advanced">
|
<el-collapse-item title="高级设置" name="advanced">
|
||||||
<el-row :gutter="16">
|
<template #title>
|
||||||
<el-col :span="24">
|
<div class="advanced-settings-title">
|
||||||
<el-form-item label="视觉方案">
|
<span class="advanced-settings-title-icon" aria-hidden="true" />
|
||||||
|
<span class="advanced-settings-title-copy">
|
||||||
|
<strong>高级设置</strong>
|
||||||
|
<small>视觉风格、生成范围与代码模板</small>
|
||||||
|
</span>
|
||||||
|
<span class="advanced-settings-summary">
|
||||||
|
{{ selectedStylePreset?.label || '未选择视觉方案' }}
|
||||||
|
<i />
|
||||||
|
{{ projectForm.frontendEnabled ? '包含用户前台' : '仅后台管理' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="advanced-settings-body">
|
||||||
|
<el-form-item label="视觉方案" class="advanced-setting-section visual-theme-setting">
|
||||||
|
<p class="advanced-setting-help">统一前台与后台的导航、色彩和内容布局。</p>
|
||||||
<el-radio-group v-model="projectForm.stylePreset" class="style-theme-group">
|
<el-radio-group v-model="projectForm.stylePreset" class="style-theme-group">
|
||||||
<el-radio
|
<el-radio
|
||||||
v-for="preset in availablePageStylePresets"
|
v-for="preset in availablePageStylePresets"
|
||||||
@@ -101,18 +116,24 @@
|
|||||||
</el-radio>
|
</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
<div class="advanced-secondary-grid">
|
||||||
<el-form-item label="用户前台">
|
<el-form-item label="用户前台" class="advanced-setting-section frontend-setting-section">
|
||||||
|
<p class="advanced-setting-help">选择完整双端项目,或只保留后台管理端。</p>
|
||||||
|
<div class="frontend-setting-control">
|
||||||
|
<span class="frontend-setting-copy">
|
||||||
|
<strong>{{ projectForm.frontendEnabled ? '生成用户前台' : '仅后台管理' }}</strong>
|
||||||
|
<small>{{ projectForm.frontendEnabled ? '生成面向用户的页面与菜单' : '不生成用户侧页面和菜单' }}</small>
|
||||||
|
</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="projectForm.frontendEnabled"
|
v-model="projectForm.frontendEnabled"
|
||||||
active-text="生成用户前台"
|
aria-label="生成用户前台"
|
||||||
inactive-text="仅后台管理"
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
<el-form-item label="代码模板" class="advanced-setting-section code-template-setting">
|
||||||
<el-form-item label="代码模板">
|
<p class="advanced-setting-help">决定生成项目的技术栈和基础目录结构。</p>
|
||||||
<div class="code-template-panel" :style="themePreviewStyle(selectedStylePreset)">
|
<div class="code-template-panel" :style="themePreviewStyle(selectedStylePreset)">
|
||||||
<div class="code-template-cover">
|
<div class="code-template-cover">
|
||||||
<img
|
<img
|
||||||
@@ -120,11 +141,16 @@
|
|||||||
:src="templatePreviewUrl(selectedCodeTemplateBundle)"
|
:src="templatePreviewUrl(selectedCodeTemplateBundle)"
|
||||||
:alt="selectedCodeTemplateBundle.bundleName || selectedCodeTemplateBundle.bundleCode"
|
:alt="selectedCodeTemplateBundle.bundleName || selectedCodeTemplateBundle.bundleCode"
|
||||||
/>
|
/>
|
||||||
<span v-else>未上传展示图</span>
|
<span v-else class="code-template-placeholder">
|
||||||
|
<i aria-hidden="true" />
|
||||||
|
<small>未上传展示图</small>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="code-template-control">
|
<div class="code-template-control">
|
||||||
|
<span class="code-template-copy">
|
||||||
<strong>{{ selectedCodeTemplateBundle.bundleName || selectedCodeTemplateBundle.bundleCode }}</strong>
|
<strong>{{ selectedCodeTemplateBundle.bundleName || selectedCodeTemplateBundle.bundleCode }}</strong>
|
||||||
<small>{{ selectedCodeTemplateBundle.description || '按当前模板生成后端、前台、后台和 SQL' }}</small>
|
<small>{{ selectedCodeTemplateBundle.description || '按当前模板生成后端、前台、后台和 SQL' }}</small>
|
||||||
|
</span>
|
||||||
<el-select v-model="projectForm.codeTemplate" class="code-template-select" filterable>
|
<el-select v-model="projectForm.codeTemplate" class="code-template-select" filterable>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="bundle in codeTemplateBundles"
|
v-for="bundle in codeTemplateBundles"
|
||||||
@@ -151,8 +177,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
</el-row>
|
</div>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
<el-form-item class="one-click-action-item">
|
<el-form-item class="one-click-action-item">
|
||||||
@@ -3350,8 +3376,198 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.advanced-settings {
|
.advanced-settings {
|
||||||
|
overflow: hidden;
|
||||||
max-width: 1180px;
|
max-width: 1180px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #dfe7f1;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
:deep(.el-collapse-item__header) {
|
||||||
|
min-height: 64px;
|
||||||
|
height: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom-color: #e7edf5;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse-item__header:not(.is-active)) {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse-item__arrow) {
|
||||||
|
margin-left: 16px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse-item__wrap) {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse-item__content) {
|
||||||
|
padding-bottom: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings-title-icon {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: linear-gradient(145deg, #eff6ff, #dbeafe);
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
width: 16px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #3b82f6;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: 10px;
|
||||||
|
box-shadow: 0 9px 0 #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
top: 8px;
|
||||||
|
left: 17px;
|
||||||
|
width: 4px;
|
||||||
|
height: 7px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: -6px 9px 0 #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings-title-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #172033;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: #8793a5;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
i {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f7f9fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-setting-section {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #e3eaf3;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__content) {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-setting-help {
|
||||||
|
margin: 5px 0 14px;
|
||||||
|
color: #7a8799;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-secondary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(280px, 0.72fr) minmax(520px, 1.28fr);
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frontend-setting-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 72px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid #e8eef6;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frontend-setting-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #243047;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: #7a8799;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.one-click-primary-actions {
|
.one-click-primary-actions {
|
||||||
@@ -3439,69 +3655,101 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.code-template-panel {
|
.code-template-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(190px, 260px) minmax(0, 1fr);
|
grid-template-columns: 156px minmax(0, 1fr);
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 980px;
|
min-width: 0;
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid var(--theme-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--theme-bg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-template-cover {
|
.code-template-cover {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 132px;
|
height: 112px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--theme-border);
|
border: 1px solid var(--theme-border);
|
||||||
border-radius: var(--theme-radius);
|
border-radius: 8px;
|
||||||
background: var(--theme-surface);
|
background: var(--theme-soft);
|
||||||
box-shadow: var(--theme-shadow);
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
.code-template-placeholder {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 8px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 13px;
|
|
||||||
|
i {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 38px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid var(--theme-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--theme-surface);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
bottom: 6px;
|
||||||
|
left: 6px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: linear-gradient(135deg, transparent 34%, var(--theme-primary) 35% 58%, var(--theme-accent) 59%);
|
||||||
|
opacity: 0.5;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-template-control {
|
.code-template-control {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
align-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-template-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-content: center;
|
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #172033;
|
color: #172033;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
small {
|
small {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
color: #667085;
|
color: #667085;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.style-theme-group {
|
.style-theme-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1180px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.style-theme-alert {
|
.style-theme-alert {
|
||||||
@@ -3512,7 +3760,7 @@ onBeforeUnmount(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 112px;
|
min-height: 138px;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
|
||||||
@@ -3530,20 +3778,38 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
&.is-checked .style-theme-card {
|
&.is-checked .style-theme-card {
|
||||||
border-color: var(--theme-primary);
|
border-color: var(--theme-primary);
|
||||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.14);
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-primary) 15%, transparent);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--theme-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
content: '✓';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.style-theme-card {
|
.style-theme-card {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 88px minmax(0, 1fr);
|
grid-template-columns: 92px minmax(0, 1fr);
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
border: 1px solid #dce5ef;
|
border: 1px solid #dce5ef;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
|
|
||||||
@@ -3556,8 +3822,9 @@ onBeforeUnmount(() => {
|
|||||||
.style-theme-preview {
|
.style-theme-preview {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
width: 88px;
|
width: 92px;
|
||||||
height: 68px;
|
height: 76px;
|
||||||
|
box-sizing: border-box;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid var(--theme-border);
|
border: 1px solid var(--theme-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -3667,16 +3934,17 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.style-theme-copy {
|
.style-theme-copy {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 35px auto;
|
grid-template-rows: auto minmax(34px, auto) auto;
|
||||||
gap: 4px;
|
gap: 7px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding-right: 22px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #172033;
|
color: #172033;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -3694,19 +3962,24 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.style-solution-tags {
|
.style-solution-tags {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
margin-top: 2px;
|
justify-items: start;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
em {
|
em {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 100%;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
|
overflow: hidden;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
color: var(--theme-primary);
|
color: var(--theme-primary);
|
||||||
background: var(--theme-soft);
|
background: var(--theme-soft);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4078,11 +4351,25 @@ onBeforeUnmount(() => {
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.advanced-secondary-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.code-template-panel {
|
.code-template-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-template-cover {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings-summary {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mode-switcher-panel {
|
.mode-switcher-panel {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -4111,4 +4398,29 @@ onBeforeUnmount(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.advanced-settings-body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-setting-section {
|
||||||
|
padding: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings-title-copy small {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-theme-card {
|
||||||
|
grid-template-columns: 82px minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-theme-preview {
|
||||||
|
width: 82px;
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -440,12 +440,15 @@ test('generate page keeps visual themes compact and removes the large impact pre
|
|||||||
assert.doesNotMatch(source, /class="style-theme-note"/)
|
assert.doesNotMatch(source, /class="style-theme-note"/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('generate page gives every visual theme card the same dimensions', () => {
|
test('generate page keeps visual theme cards readable and aligned in the new settings grid', () => {
|
||||||
const source = readSource('views/GenerateView.vue')
|
const source = readSource('views/GenerateView.vue')
|
||||||
|
|
||||||
assert.match(source, /grid-template-columns: repeat\(auto-fill, minmax\(min\(220px, 100%\), 1fr\)\)/)
|
assert.match(source, /class="advanced-settings-body"/)
|
||||||
assert.match(source, /\.style-theme-option \{[\s\S]*?width: 100%;[\s\S]*?height: 112px;/)
|
assert.match(source, /class="advanced-secondary-grid"/)
|
||||||
|
assert.match(source, /grid-template-columns: repeat\(auto-fill, minmax\(min\(260px, 100%\), 1fr\)\)/)
|
||||||
|
assert.match(source, /\.style-theme-option \{[\s\S]*?width: 100%;[\s\S]*?min-height: 138px;/)
|
||||||
assert.match(source, /\.style-theme-card \{[\s\S]*?width: 100%;[\s\S]*?height: 100%;/)
|
assert.match(source, /\.style-theme-card \{[\s\S]*?width: 100%;[\s\S]*?height: 100%;/)
|
||||||
|
assert.match(source, /\.style-solution-tags \{[\s\S]*?display: grid;/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('generate page displays one-click business loop report', () => {
|
test('generate page displays one-click business loop report', () => {
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ easycode:
|
|||||||
pids-limit: ${EASYCODE_PREVIEW_PIDS_LIMIT:256}
|
pids-limit: ${EASYCODE_PREVIEW_PIDS_LIMIT:256}
|
||||||
|
|
||||||
factory:
|
factory:
|
||||||
prompt-registry:
|
|
||||||
database-enabled: ${FACTORY_PROMPT_REGISTRY_DATABASE_ENABLED:true}
|
|
||||||
builtin-fallback-enabled: ${FACTORY_PROMPT_REGISTRY_BUILTIN_FALLBACK_ENABLED:false}
|
|
||||||
plugin-registry:
|
plugin-registry:
|
||||||
database-enabled: ${FACTORY_PLUGIN_REGISTRY_DATABASE_ENABLED:false}
|
database-enabled: ${FACTORY_PLUGIN_REGISTRY_DATABASE_ENABLED:false}
|
||||||
plugin-execution:
|
plugin-execution:
|
||||||
|
|||||||
@@ -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()));
|
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 org.springframework.stereotype.Service;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
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.factory.ai.prompt.model.PromptVersion;
|
||||||
import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
|
import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
|
||||||
|
|
||||||
@@ -14,33 +13,23 @@ import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
|
|||||||
public class DatabasePromptRegistry implements PromptRegistry
|
public class DatabasePromptRegistry implements PromptRegistry
|
||||||
{
|
{
|
||||||
private final PromptRegistryMapper mapper;
|
private final PromptRegistryMapper mapper;
|
||||||
private final BuiltinPromptRegistry builtinRegistry;
|
|
||||||
private final PromptFingerprintService fingerprintService;
|
private final PromptFingerprintService fingerprintService;
|
||||||
private final PromptRegistryProperties properties;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DatabasePromptRegistry(PromptRegistryMapper mapper,
|
public DatabasePromptRegistry(PromptRegistryMapper mapper,
|
||||||
BuiltinPromptRegistry builtinRegistry,
|
PromptFingerprintService fingerprintService)
|
||||||
PromptFingerprintService fingerprintService,
|
|
||||||
PromptRegistryProperties properties)
|
|
||||||
{
|
{
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
this.builtinRegistry = builtinRegistry;
|
|
||||||
this.fingerprintService = fingerprintService;
|
this.fingerprintService = fingerprintService;
|
||||||
this.properties = properties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PromptRelease resolve(String generateType)
|
public PromptRelease resolve(String generateType)
|
||||||
{
|
{
|
||||||
if (!properties.isDatabaseEnabled())
|
|
||||||
{
|
|
||||||
return builtinRegistry.resolve(generateType);
|
|
||||||
}
|
|
||||||
PromptVersion version = mapper.selectPublishedByGenerateType(generateType);
|
PromptVersion version = mapper.selectPublishedByGenerateType(generateType);
|
||||||
if (version == null)
|
if (version == null)
|
||||||
{
|
{
|
||||||
return missingDatabaseRelease(generateType, null);
|
throw missingDatabaseRelease(generateType, null);
|
||||||
}
|
}
|
||||||
return toRelease(version);
|
return toRelease(version);
|
||||||
}
|
}
|
||||||
@@ -48,27 +37,16 @@ public class DatabasePromptRegistry implements PromptRegistry
|
|||||||
@Override
|
@Override
|
||||||
public PromptRelease resolve(String promptCode, String version)
|
public PromptRelease resolve(String promptCode, String version)
|
||||||
{
|
{
|
||||||
if (!properties.isDatabaseEnabled())
|
|
||||||
{
|
|
||||||
return builtinRegistry.resolve(promptCode, version);
|
|
||||||
}
|
|
||||||
PromptVersion stored = mapper.selectExecutableByCodeAndVersion(promptCode, version);
|
PromptVersion stored = mapper.selectExecutableByCodeAndVersion(promptCode, version);
|
||||||
if (stored == null)
|
if (stored == null)
|
||||||
{
|
{
|
||||||
return missingDatabaseRelease(promptCode, version);
|
throw missingDatabaseRelease(promptCode, version);
|
||||||
}
|
}
|
||||||
return toRelease(stored);
|
return toRelease(stored);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PromptRelease toRelease(PromptVersion version)
|
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(),
|
String actualHash = fingerprintService.fingerprint(version.getGenerateType(),
|
||||||
version.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
|
version.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
|
||||||
version.getUserPromptContract(), version.getUserPromptTemplate());
|
version.getUserPromptContract(), version.getUserPromptTemplate());
|
||||||
@@ -91,15 +69,10 @@ public class DatabasePromptRegistry implements PromptRegistry
|
|||||||
return release;
|
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)
|
String identity = version == null ? StringUtils.defaultString(codeOrType)
|
||||||
: StringUtils.defaultString(codeOrType) + "@" + StringUtils.defaultString(version);
|
: 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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.ruoyi.generator.factory.ai.prompt.PromptRelease;
|
|
||||||
|
|
||||||
public class PromptTemplateDetail
|
public class PromptTemplateDetail
|
||||||
{
|
{
|
||||||
private PromptTemplate template;
|
private PromptTemplate template;
|
||||||
private List<PromptVersion> versions = new ArrayList<PromptVersion>();
|
private List<PromptVersion> versions = new ArrayList<PromptVersion>();
|
||||||
private PromptRelease builtinRelease;
|
|
||||||
|
|
||||||
public PromptTemplate getTemplate() { return template; }
|
public PromptTemplate getTemplate() { return template; }
|
||||||
public void setTemplate(PromptTemplate template) { this.template = template; }
|
public void setTemplate(PromptTemplate template) { this.template = template; }
|
||||||
@@ -17,6 +15,4 @@ public class PromptTemplateDetail
|
|||||||
{
|
{
|
||||||
this.versions = versions == null ? new ArrayList<PromptVersion>() : versions;
|
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.GenerateFlowConfigRequest;
|
||||||
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationResult;
|
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationResult;
|
||||||
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
|
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
|
||||||
|
import com.ruoyi.generator.service.front.FlowConfigPageBindingReconciler;
|
||||||
import com.ruoyi.generator.service.front.IAiGenerateService;
|
import com.ruoyi.generator.service.front.IAiGenerateService;
|
||||||
|
|
||||||
/** One visible design step: application blueprint plus the small flow_config. */
|
/** 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
|
public class DesignStageHandler implements OneClickGenerationStageHandler
|
||||||
{
|
{
|
||||||
private final IAiGenerateService aiGenerateService;
|
private final IAiGenerateService aiGenerateService;
|
||||||
|
private final FlowConfigPageBindingReconciler pageBindingReconciler;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DesignStageHandler(IAiGenerateService aiGenerateService)
|
public DesignStageHandler(IAiGenerateService aiGenerateService,
|
||||||
|
FlowConfigPageBindingReconciler pageBindingReconciler)
|
||||||
{
|
{
|
||||||
this.aiGenerateService = aiGenerateService;
|
this.aiGenerateService = aiGenerateService;
|
||||||
|
this.pageBindingReconciler = pageBindingReconciler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String stageCode() { return OneClickProjectGenerationResult.STAGE_DESIGN; }
|
public String stageCode() { return OneClickProjectGenerationResult.STAGE_DESIGN; }
|
||||||
@@ -49,6 +53,8 @@ public class DesignStageHandler implements OneClickGenerationStageHandler
|
|||||||
flowRequest.setAppBlueprint(context.getAppBlueprintJson());
|
flowRequest.setAppBlueprint(context.getAppBlueprintJson());
|
||||||
SimpleFlowConfig flowConfig = aiGenerateService.generateFlowConfig(context.getTask().getUserId(),
|
SimpleFlowConfig flowConfig = aiGenerateService.generateFlowConfig(context.getTask().getUserId(),
|
||||||
context.getTask().getProjectId(), flowRequest);
|
context.getTask().getProjectId(), flowRequest);
|
||||||
|
pageBindingReconciler.reconcile(flowConfig, app);
|
||||||
|
context.setAppBlueprintJson(JSON.toJSONString(app));
|
||||||
context.setFlowConfig(flowConfig);
|
context.setFlowConfig(flowConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,51 @@ public class FlowConfigSanitizer
|
|||||||
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
|
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
|
||||||
{
|
{
|
||||||
repairMetricStatuses(flow);
|
repairMetricStatuses(flow);
|
||||||
|
repairSelfUpdateRelationFields(flow);
|
||||||
repairEffectValues(flow);
|
repairEffectValues(flow);
|
||||||
}
|
}
|
||||||
return config;
|
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)
|
private void repairEffectValues(SimpleFlowConfig config)
|
||||||
{
|
{
|
||||||
for (SimpleFlowConfig.Action action : config.getActions())
|
for (SimpleFlowConfig.Action action : config.getActions())
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ public interface PromptRegistryMapper
|
|||||||
|
|
||||||
int insertPromptVersion(PromptVersion version);
|
int insertPromptVersion(PromptVersion version);
|
||||||
|
|
||||||
int updateInitializedV1(PromptVersion version);
|
|
||||||
|
|
||||||
int retirePublishedVersions(@Param("promptTemplateId") Long promptTemplateId,
|
int retirePublishedVersions(@Param("promptTemplateId") Long promptTemplateId,
|
||||||
@Param("excludeVersionId") Long excludeVersionId);
|
@Param("excludeVersionId") Long excludeVersionId);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,4 @@ public interface IPromptTemplateService
|
|||||||
int publishPromptVersion(Long promptTemplateId, Long promptVersionId, String operator);
|
int publishPromptVersion(Long promptTemplateId, Long promptVersionId, String operator);
|
||||||
|
|
||||||
int rollbackPromptVersion(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.common.utils.StringUtils;
|
||||||
import com.ruoyi.generator.factory.ai.model.ModelGatewayRouter;
|
import com.ruoyi.generator.factory.ai.model.ModelGatewayRouter;
|
||||||
import com.ruoyi.generator.factory.ai.prompt.PromptFingerprintService;
|
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.PromptTemplateRenderer;
|
||||||
import com.ruoyi.generator.factory.ai.prompt.model.PromptTemplate;
|
import com.ruoyi.generator.factory.ai.prompt.model.PromptTemplate;
|
||||||
import com.ruoyi.generator.factory.ai.prompt.model.PromptTemplateDetail;
|
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
|
public class PromptTemplateServiceImpl implements IPromptTemplateService
|
||||||
{
|
{
|
||||||
private static final Pattern PROMPT_CODE = Pattern.compile("^[a-z0-9._-]+$");
|
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 PromptRegistryMapper mapper;
|
||||||
private final PromptFingerprintService fingerprintService;
|
private final PromptFingerprintService fingerprintService;
|
||||||
private final ModelGatewayRouter gatewayRouter;
|
private final ModelGatewayRouter gatewayRouter;
|
||||||
private final BuiltinPromptRegistry builtinPromptRegistry;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PromptTemplateServiceImpl(PromptRegistryMapper mapper,
|
public PromptTemplateServiceImpl(PromptRegistryMapper mapper,
|
||||||
PromptFingerprintService fingerprintService, ModelGatewayRouter gatewayRouter,
|
PromptFingerprintService fingerprintService, ModelGatewayRouter gatewayRouter)
|
||||||
BuiltinPromptRegistry builtinPromptRegistry)
|
|
||||||
{
|
{
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
this.fingerprintService = fingerprintService;
|
this.fingerprintService = fingerprintService;
|
||||||
this.gatewayRouter = gatewayRouter;
|
this.gatewayRouter = gatewayRouter;
|
||||||
this.builtinPromptRegistry = builtinPromptRegistry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -55,7 +48,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
|
|||||||
PromptTemplateDetail detail = new PromptTemplateDetail();
|
PromptTemplateDetail detail = new PromptTemplateDetail();
|
||||||
detail.setTemplate(template);
|
detail.setTemplate(template);
|
||||||
detail.setVersions(versions == null ? Collections.<PromptVersion>emptyList() : versions);
|
detail.setVersions(versions == null ? Collections.<PromptVersion>emptyList() : versions);
|
||||||
detail.setBuiltinRelease(builtinPromptRegistry.resolve(template.getGenerateType()));
|
|
||||||
return detail;
|
return detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +56,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
|
|||||||
public int insertPromptTemplate(PromptTemplate template)
|
public int insertPromptTemplate(PromptTemplate template)
|
||||||
{
|
{
|
||||||
normalizeTemplate(template, true);
|
normalizeTemplate(template, true);
|
||||||
builtinPromptRegistry.resolve(template.getGenerateType());
|
|
||||||
if (mapper.selectPromptTemplateByCode(template.getPromptCode()) != null)
|
if (mapper.selectPromptTemplateByCode(template.getPromptCode()) != null)
|
||||||
{
|
{
|
||||||
throw new ServiceException("Prompt code already exists: " + template.getPromptCode());
|
throw new ServiceException("Prompt code already exists: " + template.getPromptCode());
|
||||||
@@ -148,96 +139,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
|
|||||||
return changePublication(promptTemplateId, promptVersionId, operator, true);
|
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,
|
private int changePublication(Long promptTemplateId, Long promptVersionId,
|
||||||
String operator, boolean rollback)
|
String operator, boolean rollback)
|
||||||
{
|
{
|
||||||
@@ -257,13 +158,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
|
|||||||
throw new ServiceException("Only a draft Prompt version can be published");
|
throw new ServiceException("Only a draft Prompt version can be published");
|
||||||
}
|
}
|
||||||
verifyContentHash(template, version);
|
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());
|
gatewayRouter.resolve(version.getProviderCode());
|
||||||
mapper.retirePublishedVersions(promptTemplateId, promptVersionId);
|
mapper.retirePublishedVersions(promptTemplateId, promptVersionId);
|
||||||
if (mapper.publishPromptVersion(promptVersionId, StringUtils.defaultString(operator)) != 1)
|
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);
|
String aiContent = chatWithUsage(userId, projectId, "flow_config", prompt);
|
||||||
modelAttempts.add(aiContent);
|
modelAttempts.add(aiContent);
|
||||||
SimpleFlowConfig config;
|
SimpleFlowConfig config;
|
||||||
|
AppBlueprintDesign reconciledBlueprint;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
config = parseFlowConfigResponse(aiContent);
|
config = parseFlowConfigResponse(aiContent);
|
||||||
validateGeneratedFlowConfig(project, request, config);
|
reconciledBlueprint = validateGeneratedFlowConfig(project, request, config);
|
||||||
}
|
}
|
||||||
catch (RuntimeException firstFailure)
|
catch (RuntimeException firstFailure)
|
||||||
{
|
{
|
||||||
@@ -197,9 +198,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
aiContent = chatWithUsage(userId, projectId, "flow_config", prompt);
|
aiContent = chatWithUsage(userId, projectId, "flow_config", prompt);
|
||||||
modelAttempts.add(aiContent);
|
modelAttempts.add(aiContent);
|
||||||
config = parseFlowConfigResponse(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.setResponsePayload(aiContent);
|
||||||
generation.setSuccess("1");
|
generation.setSuccess("1");
|
||||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||||
@@ -253,7 +254,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
flowConfig, flowCompilation, true);
|
flowConfig, flowCompilation, true);
|
||||||
}
|
}
|
||||||
persistDatabase(userId, projectId, response);
|
persistDatabase(userId, projectId, response);
|
||||||
markDatabaseGenerated(projectId, project.getDraftRevision(), request, flowConfig);
|
markDatabaseGenerated(projectId, project.getDraftRevision(), request, appBlueprint, flowConfig);
|
||||||
generation.setResponsePayload(aiContent);
|
generation.setResponsePayload(aiContent);
|
||||||
generation.setSuccess("1");
|
generation.setSuccess("1");
|
||||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||||
@@ -920,7 +921,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
return flowConfigSanitizer().sanitize(config);
|
return flowConfigSanitizer().sanitize(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateGeneratedFlowConfig(FrontProject project, GenerateFlowConfigRequest request,
|
private AppBlueprintDesign validateGeneratedFlowConfig(FrontProject project, GenerateFlowConfigRequest request,
|
||||||
SimpleFlowConfig config)
|
SimpleFlowConfig config)
|
||||||
{
|
{
|
||||||
String blueprintJson = firstNonBlank(request == null ? null : request.getAppBlueprint(),
|
String blueprintJson = firstNonBlank(request == null ? null : request.getAppBlueprint(),
|
||||||
@@ -934,6 +935,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
flowConfigContractService().validateDatabase(loadSavedDatabaseDesign(project.getProjectId()),
|
flowConfigContractService().validateDatabase(loadSavedDatabaseDesign(project.getProjectId()),
|
||||||
config, compilation);
|
config, compilation);
|
||||||
}
|
}
|
||||||
|
return blueprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FlowConfigSanitizer flowConfigSanitizer()
|
private FlowConfigSanitizer flowConfigSanitizer()
|
||||||
@@ -946,8 +948,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
String appBlueprintJson = resolveConfirmedAppBlueprint(project, request);
|
String appBlueprintJson = resolveConfirmedAppBlueprint(project, request);
|
||||||
SimpleFlowConfig flowConfig = readFlowConfig(project,
|
SimpleFlowConfig flowConfig = readFlowConfig(project,
|
||||||
request == null ? null : request.getFlowConfig());
|
request == null ? null : request.getFlowConfig());
|
||||||
FlowConfigCompilation compilation = flowConfigContractService().canonicalize(flowConfig,
|
AppBlueprintDesign appBlueprint = parseConfirmedAppBlueprint(appBlueprintJson);
|
||||||
parseConfirmedAppBlueprint(appBlueprintJson));
|
FlowConfigCompilation compilation = flowConfigContractService().canonicalize(flowConfig, appBlueprint);
|
||||||
|
appBlueprintJson = JSON.toJSONString(appBlueprint);
|
||||||
String flowConfigJson = flowConfig == null ? "" : JSON.toJSONString(flowConfig);
|
String flowConfigJson = flowConfig == null ? "" : JSON.toJSONString(flowConfig);
|
||||||
String businessLoopPlanJson = JSON.toJSONString(compilation.getLoopPlan());
|
String businessLoopPlanJson = JSON.toJSONString(compilation.getLoopPlan());
|
||||||
Map<String, Object> context = new LinkedHashMap<String, Object>();
|
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,
|
private void markDatabaseGenerated(Long projectId, Long draftRevision, GenerateDatabaseRequest request,
|
||||||
SimpleFlowConfig flowConfig)
|
AppBlueprintDesign blueprint, SimpleFlowConfig flowConfig)
|
||||||
{
|
{
|
||||||
FrontProject project = new FrontProject();
|
FrontProject project = new FrontProject();
|
||||||
project.setProjectId(projectId);
|
project.setProjectId(projectId);
|
||||||
@@ -4150,6 +4153,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
|
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
|
||||||
project.setProjectDesc(request == null ? null : request.getProjectDesc());
|
project.setProjectDesc(request == null ? null : request.getProjectDesc());
|
||||||
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
|
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
|
||||||
|
if (blueprint != null) project.setAppBlueprint(JSON.toJSONString(blueprint));
|
||||||
if (flowConfig != null) project.setFlowConfig(JSON.toJSONString(flowConfig));
|
if (flowConfig != null) project.setFlowConfig(JSON.toJSONString(flowConfig));
|
||||||
applyStylePreset(project, request == null ? null : request.getStylePreset());
|
applyStylePreset(project, request == null ? null : request.getStylePreset());
|
||||||
if (frontProjectMapper.updateFrontProjectIfRevision(project) == 0)
|
if (frontProjectMapper.updateFrontProjectIfRevision(project) == 0)
|
||||||
@@ -4160,13 +4164,14 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void markFlowConfigGenerated(Long projectId, Long draftRevision, SimpleFlowConfig config,
|
private void markFlowConfigGenerated(Long projectId, Long draftRevision, SimpleFlowConfig config,
|
||||||
GenerateFlowConfigRequest request)
|
AppBlueprintDesign blueprint, GenerateFlowConfigRequest request)
|
||||||
{
|
{
|
||||||
FrontProject project = new FrontProject();
|
FrontProject project = new FrontProject();
|
||||||
project.setProjectId(projectId);
|
project.setProjectId(projectId);
|
||||||
project.setDraftRevision(draftRevision);
|
project.setDraftRevision(draftRevision);
|
||||||
project.setPreviewStatus("0");
|
project.setPreviewStatus("0");
|
||||||
project.setFlowConfig(JSON.toJSONString(config));
|
project.setFlowConfig(JSON.toJSONString(config));
|
||||||
|
project.setAppBlueprint(JSON.toJSONString(blueprint));
|
||||||
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
|
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
|
||||||
project.setProjectDesc(request == null ? null : request.getProjectDesc());
|
project.setProjectDesc(request == null ? null : request.getProjectDesc());
|
||||||
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
|
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
|
||||||
|
|||||||
@@ -24,22 +24,34 @@ public class FlowConfigContractService
|
|||||||
private final FlowConfigCompiler flowCompiler;
|
private final FlowConfigCompiler flowCompiler;
|
||||||
private final FlowConfigDatabaseContractValidator databaseValidator;
|
private final FlowConfigDatabaseContractValidator databaseValidator;
|
||||||
private final BusinessLoopCoverageValidator coverageValidator;
|
private final BusinessLoopCoverageValidator coverageValidator;
|
||||||
|
private final FlowConfigPageBindingReconciler pageBindingReconciler;
|
||||||
|
|
||||||
public FlowConfigContractService()
|
public FlowConfigContractService()
|
||||||
{
|
{
|
||||||
this(new FlowConfigValidator(), new FlowConfigCompiler(),
|
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
|
@Autowired
|
||||||
public FlowConfigContractService(FlowConfigValidator flowValidator, FlowConfigCompiler flowCompiler,
|
public FlowConfigContractService(FlowConfigValidator flowValidator, FlowConfigCompiler flowCompiler,
|
||||||
FlowConfigDatabaseContractValidator databaseValidator,
|
FlowConfigDatabaseContractValidator databaseValidator,
|
||||||
BusinessLoopCoverageValidator coverageValidator)
|
BusinessLoopCoverageValidator coverageValidator,
|
||||||
|
FlowConfigPageBindingReconciler pageBindingReconciler)
|
||||||
{
|
{
|
||||||
this.flowValidator = flowValidator;
|
this.flowValidator = flowValidator;
|
||||||
this.flowCompiler = flowCompiler;
|
this.flowCompiler = flowCompiler;
|
||||||
this.databaseValidator = databaseValidator;
|
this.databaseValidator = databaseValidator;
|
||||||
this.coverageValidator = coverageValidator;
|
this.coverageValidator = coverageValidator;
|
||||||
|
this.pageBindingReconciler = pageBindingReconciler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FlowConfigCompilation compile(SimpleFlowConfig config)
|
public FlowConfigCompilation compile(SimpleFlowConfig config)
|
||||||
@@ -58,8 +70,9 @@ public class FlowConfigContractService
|
|||||||
first.getLoopPlan(), blueprint);
|
first.getLoopPlan(), blueprint);
|
||||||
if (aliases.isEmpty())
|
if (aliases.isEmpty())
|
||||||
{
|
{
|
||||||
|
boolean bindingChanged = pageBindingReconciler.reconcile(config, blueprint);
|
||||||
validatePageBindings(config, blueprint);
|
validatePageBindings(config, blueprint);
|
||||||
return first;
|
return bindingChanged ? compile(config) : first;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
|
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
|
||||||
@@ -83,6 +96,7 @@ public class FlowConfigContractService
|
|||||||
if (metric != null) metric.setTable(rename(metric.getTable(), aliases));
|
if (metric != null) metric.setTable(rename(metric.getTable(), aliases));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pageBindingReconciler.reconcile(config, blueprint);
|
||||||
validatePageBindings(config, blueprint);
|
validatePageBindings(config, blueprint);
|
||||||
return compile(config);
|
return compile(config);
|
||||||
}
|
}
|
||||||
@@ -156,19 +170,25 @@ public class FlowConfigContractService
|
|||||||
|
|
||||||
private void validatePageBindings(SimpleFlowConfig config, AppBlueprintDesign blueprint)
|
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)
|
if (blueprint.getFrontendPages() != null)
|
||||||
{
|
{
|
||||||
for (com.ruoyi.generator.domain.front.dto.AppPageDesign page : blueprint.getFrontendPages())
|
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)
|
if (blueprint.getAdminMenus() != null)
|
||||||
{
|
{
|
||||||
for (com.ruoyi.generator.domain.front.dto.AppMenuDesign menu : blueprint.getAdminMenus())
|
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))
|
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
|
||||||
@@ -183,16 +203,45 @@ public class FlowConfigContractService
|
|||||||
}
|
}
|
||||||
String ownerTable = StringUtils.defaultIfBlank(action.getOwnerTable(), flow.getMainTable());
|
String ownerTable = StringUtils.defaultIfBlank(action.getOwnerTable(), flow.getMainTable());
|
||||||
String bindingTable = StringUtils.defaultIfBlank(action.getButton().getTableName(), ownerTable);
|
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))
|
if (StringUtils.isNotBlank(pageTable) && !StringUtils.equals(pageTable, bindingTable))
|
||||||
{
|
{
|
||||||
throw new ServiceException("flow_config button table does not match page: " + pageCode
|
throw new ServiceException("flow_config button table does not match page: " + pageCode
|
||||||
+ " -> " + bindingTable + ", expected " + pageTable);
|
+ " -> " + 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)
|
private <T> List<T> safe(List<T> items)
|
||||||
{
|
{
|
||||||
return items == null ? java.util.Collections.<T>emptyList() : 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.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.generator.config.DeepSeekProperties;
|
import com.ruoyi.generator.config.DeepSeekProperties;
|
||||||
import com.ruoyi.generator.factory.ai.prompt.BuiltinPromptRegistry;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class HttpDeepSeekClient implements IDeepSeekClient
|
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
|
@Autowired
|
||||||
private DeepSeekProperties deepSeekProperties;
|
private DeepSeekProperties deepSeekProperties;
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private AiUsageRecorder aiUsageRecorder;
|
private AiUsageRecorder aiUsageRecorder;
|
||||||
|
|
||||||
@Override
|
|
||||||
public String chat(String prompt)
|
|
||||||
{
|
|
||||||
return chat(DEFAULT_SYSTEM_PROMPT, prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String chat(String systemPrompt, String prompt)
|
public String chat(String systemPrompt, String prompt)
|
||||||
{
|
{
|
||||||
@@ -86,6 +77,7 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
|||||||
@Override
|
@Override
|
||||||
public String chatModel(String model, String systemPrompt, String prompt)
|
public String chatModel(String model, String systemPrompt, String prompt)
|
||||||
{
|
{
|
||||||
|
requireManagedSystemPrompt(systemPrompt);
|
||||||
if (StringUtils.isBlank(deepSeekProperties.getApiKey()))
|
if (StringUtils.isBlank(deepSeekProperties.getApiKey()))
|
||||||
{
|
{
|
||||||
throw new ServiceException("DeepSeek API Key 未配置");
|
throw new ServiceException("DeepSeek API Key 未配置");
|
||||||
@@ -100,12 +92,6 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
|||||||
return parseContent(response.getBody(), model);
|
return parseContent(response.getBody(), model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void chatStream(String prompt, final StreamHandler handler)
|
|
||||||
{
|
|
||||||
chatStream(DEFAULT_SYSTEM_PROMPT, prompt, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void chatStream(String systemPrompt, String prompt, final StreamHandler handler)
|
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,
|
public void chatStreamModel(String model, String systemPrompt, String prompt,
|
||||||
final StreamHandler handler)
|
final StreamHandler handler)
|
||||||
{
|
{
|
||||||
|
requireManagedSystemPrompt(systemPrompt);
|
||||||
if (StringUtils.isBlank(deepSeekProperties.getApiKey()))
|
if (StringUtils.isBlank(deepSeekProperties.getApiKey()))
|
||||||
{
|
{
|
||||||
throw new ServiceException("DeepSeek API Key 未配置");
|
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)
|
private JSONObject requestBody(String systemPrompt, String prompt, boolean stream)
|
||||||
{
|
{
|
||||||
return requestBody(deepSeekProperties.getModel(), systemPrompt, prompt, 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)
|
private JSONObject requestBody(String model, String systemPrompt, String prompt, boolean stream)
|
||||||
{
|
{
|
||||||
|
requireManagedSystemPrompt(systemPrompt);
|
||||||
JSONObject request = new JSONObject();
|
JSONObject request = new JSONObject();
|
||||||
request.put("model", StringUtils.defaultIfEmpty(model, deepSeekProperties.getModel()));
|
request.put("model", StringUtils.defaultIfEmpty(model, deepSeekProperties.getModel()));
|
||||||
JSONArray messages = new JSONArray();
|
JSONArray messages = new JSONArray();
|
||||||
messages.add(message("system", StringUtils.defaultIfEmpty(systemPrompt, DEFAULT_SYSTEM_PROMPT)));
|
messages.add(message("system", systemPrompt));
|
||||||
messages.add(message("user", prompt));
|
messages.add(message("user", prompt));
|
||||||
request.put("messages", messages);
|
request.put("messages", messages);
|
||||||
request.put("temperature", 0.2);
|
request.put("temperature", 0.2);
|
||||||
@@ -169,6 +152,14 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
|||||||
return request;
|
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)
|
private JSONObject message(String role, String content)
|
||||||
{
|
{
|
||||||
JSONObject message = new JSONObject();
|
JSONObject message = new JSONObject();
|
||||||
|
|||||||
@@ -9,14 +9,10 @@ public interface IDeepSeekClient
|
|||||||
void onContent(String content) throws IOException;
|
void onContent(String content) throws IOException;
|
||||||
}
|
}
|
||||||
|
|
||||||
String chat(String prompt);
|
|
||||||
|
|
||||||
String chat(String systemPrompt, String prompt);
|
String chat(String systemPrompt, String prompt);
|
||||||
|
|
||||||
String chatModel(String model, 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 chatStream(String systemPrompt, String prompt, StreamHandler handler);
|
||||||
|
|
||||||
void chatStreamModel(String model, 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.AiGenerationTask;
|
||||||
import com.ruoyi.generator.domain.front.FrontProject;
|
import com.ruoyi.generator.domain.front.FrontProject;
|
||||||
import com.ruoyi.generator.domain.front.dto.AiGenerationTaskCreateRequest;
|
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.DatabaseDesignResponse;
|
||||||
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationException;
|
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationException;
|
||||||
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationReport;
|
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationReport;
|
||||||
@@ -38,6 +39,8 @@ public class OneClickProjectGenerationServiceImpl implements IOneClickProjectGen
|
|||||||
private FlowConfigDatabaseContractValidator flowConfigDatabaseContractValidator;
|
private FlowConfigDatabaseContractValidator flowConfigDatabaseContractValidator;
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private BusinessRequirementCoverageValidator businessRequirementCoverageValidator;
|
private BusinessRequirementCoverageValidator businessRequirementCoverageValidator;
|
||||||
|
@Autowired(required = false)
|
||||||
|
private FlowConfigPageBindingReconciler pageBindingReconciler;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OneClickProjectGenerationResult generate(AiGenerationTask task, AiGenerationTaskCreateRequest request)
|
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");
|
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());
|
requirementCoverageValidator().validate(requirement(context.getRequest()), context.getFlowConfig());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -204,6 +212,11 @@ public class OneClickProjectGenerationServiceImpl implements IOneClickProjectGen
|
|||||||
? new BusinessRequirementCoverageValidator() : businessRequirementCoverageValidator;
|
? new BusinessRequirementCoverageValidator() : businessRequirementCoverageValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FlowConfigPageBindingReconciler pageBindingReconciler()
|
||||||
|
{
|
||||||
|
return pageBindingReconciler == null ? new FlowConfigPageBindingReconciler() : pageBindingReconciler;
|
||||||
|
}
|
||||||
|
|
||||||
private String requirement(AiGenerationTaskCreateRequest request)
|
private String requirement(AiGenerationTaskCreateRequest request)
|
||||||
{
|
{
|
||||||
if (request == null) return "";
|
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})
|
#{createBy}, sysdate(), #{remark})
|
||||||
</insert>
|
</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 id="retirePublishedVersions">
|
||||||
update factory_prompt_version
|
update factory_prompt_version
|
||||||
set status = 'RETIRED'
|
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.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
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.ai.prompt.model.PromptVersion;
|
||||||
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
|
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
|
||||||
import com.ruoyi.generator.factory.generation.CanonicalFingerprintService;
|
import com.ruoyi.generator.factory.generation.CanonicalFingerprintService;
|
||||||
@@ -17,9 +16,7 @@ import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
|
|||||||
public class DatabasePromptRegistryTest
|
public class DatabasePromptRegistryTest
|
||||||
{
|
{
|
||||||
private PromptRegistryMapper mapper;
|
private PromptRegistryMapper mapper;
|
||||||
private BuiltinPromptRegistry builtin;
|
|
||||||
private PromptFingerprintService fingerprints;
|
private PromptFingerprintService fingerprints;
|
||||||
private PromptRegistryProperties properties;
|
|
||||||
private DatabasePromptRegistry registry;
|
private DatabasePromptRegistry registry;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@@ -28,23 +25,12 @@ public class DatabasePromptRegistryTest
|
|||||||
mapper = mock(PromptRegistryMapper.class);
|
mapper = mock(PromptRegistryMapper.class);
|
||||||
fingerprints = new PromptFingerprintService(
|
fingerprints = new PromptFingerprintService(
|
||||||
new CanonicalFingerprintService(new ProjectSpecJsonCodec()));
|
new CanonicalFingerprintService(new ProjectSpecJsonCodec()));
|
||||||
builtin = new BuiltinPromptRegistry(fingerprints);
|
registry = new DatabasePromptRegistry(mapper, 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resolvesCurrentAndExactDatabaseRelease()
|
public void resolvesCurrentAndExactDatabaseRelease()
|
||||||
{
|
{
|
||||||
properties.setDatabaseEnabled(true);
|
|
||||||
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
|
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
|
||||||
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
|
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
|
||||||
when(mapper.selectExecutableByCodeAndVersion("front.database", "2.0.0"))
|
when(mapper.selectExecutableByCodeAndVersion("front.database", "2.0.0"))
|
||||||
@@ -58,7 +44,6 @@ public class DatabasePromptRegistryTest
|
|||||||
@Test
|
@Test
|
||||||
public void rejectsStoredContentHashDrift()
|
public void rejectsStoredContentHashDrift()
|
||||||
{
|
{
|
||||||
properties.setDatabaseEnabled(true);
|
|
||||||
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
|
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
|
||||||
version.setContentHash("tampered");
|
version.setContentHash("tampered");
|
||||||
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
|
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
|
||||||
@@ -75,32 +60,22 @@ public class DatabasePromptRegistryTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void obsoleteLegacyBuilderReleaseIsRejectedInsteadOfSilentlyFallingBack()
|
public void acceptsContractOwnedByManagedVersion()
|
||||||
{
|
{
|
||||||
properties.setDatabaseEnabled(true);
|
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
|
||||||
PromptVersion version = version("1.0.0", PromptVersion.STATUS_PUBLISHED);
|
version.setUserPromptContract("context-json:database:v2");
|
||||||
version.setUserPromptContract("legacy-builder:database:v1");
|
|
||||||
version.setContentHash(fingerprints.fingerprint(version.getGenerateType(),
|
version.setContentHash(fingerprints.fingerprint(version.getGenerateType(),
|
||||||
version.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
|
version.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
|
||||||
version.getUserPromptContract(), version.getUserPromptTemplate()));
|
version.getUserPromptContract(), version.getUserPromptTemplate()));
|
||||||
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
|
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
|
||||||
|
|
||||||
try
|
assertEquals("context-json:database:v2",
|
||||||
{
|
registry.resolve("database").getUserPromptContract());
|
||||||
registry.resolve("database");
|
|
||||||
fail("Expected incompatible stored contract");
|
|
||||||
}
|
|
||||||
catch (ServiceException e)
|
|
||||||
{
|
|
||||||
assertTrue(e.getMessage().contains("not implemented"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void missingDatabaseReleaseFailsClosedByDefault()
|
public void missingDatabaseReleaseFailsClosedByDefault()
|
||||||
{
|
{
|
||||||
properties.setDatabaseEnabled(true);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
registry.resolve("database");
|
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)
|
private PromptVersion version(String label, String status)
|
||||||
{
|
{
|
||||||
PromptVersion version = new PromptVersion();
|
PromptVersion version = new PromptVersion();
|
||||||
@@ -132,7 +97,7 @@ public class DatabasePromptRegistryTest
|
|||||||
version.setVersion(label);
|
version.setVersion(label);
|
||||||
version.setSystemPrompt("database system v2");
|
version.setSystemPrompt("database system v2");
|
||||||
version.setUserPromptContract("context-json:database:v1");
|
version.setUserPromptContract("context-json:database:v1");
|
||||||
version.setUserPromptTemplate(BuiltinPromptRegistry.DATABASE_USER_PROMPT_TEMPLATE);
|
version.setUserPromptTemplate("{{input}}");
|
||||||
version.setProviderCode("deepseek");
|
version.setProviderCode("deepseek");
|
||||||
version.setModel("deepseek-chat");
|
version.setModel("deepseek-chat");
|
||||||
version.setStatus(status);
|
version.setStatus(status);
|
||||||
|
|||||||
@@ -23,17 +23,15 @@ public class PromptRegistrySchemaScriptTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void mapperKeepsVersionContentImmutableOutsideGuardedBootstrapRepair() throws Exception
|
public void mapperKeepsManagedVersionContentImmutable() throws Exception
|
||||||
{
|
{
|
||||||
String mapper = read(repoRoot().resolve(
|
String mapper = read(repoRoot().resolve(
|
||||||
"ruoyi-generator/src/main/resources/mapper/factory/PromptRegistryMapper.xml"));
|
"ruoyi-generator/src/main/resources/mapper/factory/PromptRegistryMapper.xml"));
|
||||||
assertTrue(mapper.contains("insertPromptVersion"));
|
assertTrue(mapper.contains("insertPromptVersion"));
|
||||||
assertTrue(mapper.contains("publishPromptVersion"));
|
assertTrue(mapper.contains("publishPromptVersion"));
|
||||||
assertTrue(mapper.contains("retirePublishedVersions"));
|
assertTrue(mapper.contains("retirePublishedVersions"));
|
||||||
assertTrue(mapper.contains("updateInitializedV1"));
|
assertFalse(mapper.contains("updateInitializedV1"));
|
||||||
assertTrue(mapper.contains("version_label = 'V1'"));
|
assertFalse(mapper.contains("legacy-builder:"));
|
||||||
assertTrue(mapper.contains("user_prompt_contract like 'legacy-builder:%'"));
|
|
||||||
assertTrue(mapper.contains("remark in ('由当前代码内置 Prompt 初始化', '由当前代码内置业务 Prompt 初始化')"));
|
|
||||||
assertFalse(mapper.contains("updatePromptVersionContent"));
|
assertFalse(mapper.contains("updatePromptVersionContent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +45,7 @@ public class PromptRegistrySchemaScriptTest
|
|||||||
String completeDatabase = read(root.resolve("sql/db.sql"));
|
String completeDatabase = read(root.resolve("sql/db.sql"));
|
||||||
assertTrue(controller.contains("/versions/{promptVersionId}/publish"));
|
assertTrue(controller.contains("/versions/{promptVersionId}/publish"));
|
||||||
assertTrue(controller.contains("/versions/{promptVersionId}/rollback"));
|
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:publish"));
|
||||||
assertTrue(menu.contains("generator:prompt:rollback"));
|
assertTrue(menu.contains("generator:prompt:rollback"));
|
||||||
assertTrue(completeDatabase.contains("'Prompt 中心'"));
|
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());
|
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
|
@Test
|
||||||
public void compilesReservationAndBorrowingAsTwoIndependentFlows()
|
public void compilesReservationAndBorrowingAsTwoIndependentFlows()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import static org.junit.Assert.fail;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.inOrder;
|
import static org.mockito.Mockito.inOrder;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import java.util.Collections;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.InOrder;
|
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.ModelGateway;
|
||||||
import com.ruoyi.generator.factory.ai.model.ModelGatewayRouter;
|
import com.ruoyi.generator.factory.ai.model.ModelGatewayRouter;
|
||||||
import com.ruoyi.generator.factory.ai.prompt.PromptFingerprintService;
|
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.PromptTemplate;
|
||||||
import com.ruoyi.generator.factory.ai.prompt.model.PromptVersion;
|
import com.ruoyi.generator.factory.ai.prompt.model.PromptVersion;
|
||||||
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
|
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
|
||||||
@@ -38,8 +35,7 @@ public class PromptTemplateServiceImplTest
|
|||||||
gatewayRouter = mock(ModelGatewayRouter.class);
|
gatewayRouter = mock(ModelGatewayRouter.class);
|
||||||
fingerprints = new PromptFingerprintService(
|
fingerprints = new PromptFingerprintService(
|
||||||
new CanonicalFingerprintService(new ProjectSpecJsonCodec()));
|
new CanonicalFingerprintService(new ProjectSpecJsonCodec()));
|
||||||
service = new PromptTemplateServiceImpl(mapper, fingerprints, gatewayRouter,
|
service = new PromptTemplateServiceImpl(mapper, fingerprints, gatewayRouter);
|
||||||
new BuiltinPromptRegistry(fingerprints));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -126,7 +122,7 @@ public class PromptTemplateServiceImplTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsUnimplementedUserPromptContractBeforePublication()
|
public void publishesContractDefinedByManagedVersion()
|
||||||
{
|
{
|
||||||
PromptVersion draft = version(202L, PromptVersion.STATUS_DRAFT);
|
PromptVersion draft = version(202L, PromptVersion.STATUS_DRAFT);
|
||||||
draft.setUserPromptContract("context-json:database:v2");
|
draft.setUserPromptContract("context-json:database:v2");
|
||||||
@@ -134,88 +130,11 @@ public class PromptTemplateServiceImplTest
|
|||||||
draft.getVersion(), draft.getSystemPrompt(), draft.getUserPromptContract()));
|
draft.getVersion(), draft.getSystemPrompt(), draft.getUserPromptContract()));
|
||||||
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template());
|
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template());
|
||||||
when(mapper.selectPromptVersionById(202L)).thenReturn(draft);
|
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(gatewayRouter.resolve("deepseek")).thenReturn(mock(ModelGateway.class));
|
||||||
when(mapper.insertPromptVersion(any(PromptVersion.class))).thenAnswer(invocation ->
|
when(mapper.publishPromptVersion(202L, "admin")).thenReturn(1);
|
||||||
{
|
when(mapper.updateCurrentVersion(101L, 202L, "admin")).thenReturn(1);
|
||||||
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);
|
|
||||||
|
|
||||||
assertEquals(1, service.initializeBuiltinV1("admin"));
|
assertEquals(1, service.publishPromptVersion(101L, 202L, "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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PromptTemplate template()
|
private PromptTemplate template()
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import org.junit.Test;
|
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
|
public class AiPromptExternalizationTest
|
||||||
{
|
{
|
||||||
@@ -30,35 +25,19 @@ public class AiPromptExternalizationTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void everyExecutableGenerationTypeOwnsManagedTemplates()
|
public void runtimeRegistryHasNoBuiltinPromptFallback() throws Exception
|
||||||
{
|
{
|
||||||
BuiltinPromptRegistry registry = new BuiltinPromptRegistry(new PromptFingerprintService(
|
Path source = resolve(
|
||||||
new CanonicalFingerprintService(new ProjectSpecJsonCodec())));
|
"ruoyi-generator/src/main/java/com/ruoyi/generator/factory/ai/prompt/DatabasePromptRegistry.java",
|
||||||
String[] types = new String[] { "requirement", "app_blueprint", "flow_config", "database",
|
"src/main/java/com/ruoyi/generator/factory/ai/prompt/DatabasePromptRegistry.java");
|
||||||
"diagram_spec", "paper_outline", "paper_draft", "code_analysis" };
|
String content = new String(Files.readAllBytes(source), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
for (String type : types)
|
assertTrue(content.contains("selectPublishedByGenerateType"));
|
||||||
{
|
assertFalse(content.contains("BuiltinPromptRegistry"));
|
||||||
PromptRelease release = registry.resolve(type);
|
assertFalse(content.contains("PromptRegistryProperties"));
|
||||||
assertTrue(type, release.getSystemPrompt() != null && !release.getSystemPrompt().trim().isEmpty());
|
Path resources = resolve("ruoyi-generator/src/main/resources/factory-prompts",
|
||||||
assertTrue(type, release.getUserPromptTemplate() != null
|
"src/main/resources/factory-prompts");
|
||||||
&& release.getUserPromptTemplate().contains("{{input}}"));
|
assertFalse(Files.exists(resources.resolve("default/system.txt")));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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\""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path resolve(String repositoryPath, String modulePath)
|
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 org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.generator.config.DeepSeekProperties;
|
import com.ruoyi.generator.config.DeepSeekProperties;
|
||||||
|
|
||||||
public class HttpDeepSeekClientTest
|
public class HttpDeepSeekClientTest
|
||||||
@@ -130,6 +131,22 @@ public class HttpDeepSeekClientTest
|
|||||||
assertTrue(request.getJSONObject("stream_options").getBooleanValue("include_usage"));
|
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
|
@Test
|
||||||
public void parseContentRecordsUsageFromNonStreamingResponse() throws Exception
|
public void parseContentRecordsUsageFromNonStreamingResponse() throws Exception
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ export function rollbackPromptVersion(promptTemplateId, promptVersionId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializePromptV1() {
|
|
||||||
return request({ url: '/generator/prompt/initialize-v1', method: 'post' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listPromptModels() {
|
export function listPromptModels() {
|
||||||
return request({ url: '/generator/prompt/models', method: 'get' })
|
return request({ url: '/generator/prompt/models', method: 'get' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,6 @@
|
|||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button v-hasPermi="['generator:prompt:add']" type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增模板</el-button>
|
<el-button v-hasPermi="['generator:prompt:add']" type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增模板</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button v-hasPermi="['generator:prompt:version']" type="primary" plain icon="el-icon-document-add" size="mini" @click="initializeV1">初始化 V1</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button v-hasPermi="['generator:prompt:debug']" type="success" plain icon="el-icon-video-play" size="mini" @click="openDebugLab()">在线调试</el-button>
|
<el-button v-hasPermi="['generator:prompt:debug']" type="success" plain icon="el-icon-video-play" size="mini" @click="openDebugLab()">在线调试</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -44,7 +41,7 @@
|
|||||||
<el-table-column label="当前发布" min-width="130">
|
<el-table-column label="当前发布" min-width="130">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tag v-if="scope.row.currentVersion" size="mini" type="success">{{ scope.row.currentVersion }}</el-tag>
|
<el-tag v-if="scope.row.currentVersion" size="mini" type="success">{{ scope.row.currentVersion }}</el-tag>
|
||||||
<span v-else class="muted">使用内置后备</span>
|
<span v-else class="muted">未发布</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="发布指纹" min-width="170">
|
<el-table-column label="发布指纹" min-width="170">
|
||||||
@@ -281,7 +278,6 @@ import {
|
|||||||
createPromptVersion,
|
createPromptVersion,
|
||||||
publishPromptVersion,
|
publishPromptVersion,
|
||||||
rollbackPromptVersion,
|
rollbackPromptVersion,
|
||||||
initializePromptV1,
|
|
||||||
listPromptModels,
|
listPromptModels,
|
||||||
debugPrompt,
|
debugPrompt,
|
||||||
listPromptDebugRuns,
|
listPromptDebugRuns,
|
||||||
@@ -318,7 +314,7 @@ export default {
|
|||||||
versionDrawerOpen: false,
|
versionDrawerOpen: false,
|
||||||
versionDrawerTitle: 'Prompt 版本',
|
versionDrawerTitle: 'Prompt 版本',
|
||||||
detailLoading: false,
|
detailLoading: false,
|
||||||
detail: { template: null, versions: [], builtinRelease: null },
|
detail: { template: null, versions: [] },
|
||||||
versionDialogOpen: false,
|
versionDialogOpen: false,
|
||||||
versionDialogTitle: '',
|
versionDialogTitle: '',
|
||||||
versionReadOnly: false,
|
versionReadOnly: false,
|
||||||
@@ -428,20 +424,21 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
clearDetail() {
|
clearDetail() {
|
||||||
this.detail = { template: null, versions: [], builtinRelease: null }
|
this.detail = { template: null, versions: [] }
|
||||||
},
|
},
|
||||||
openVersionDialog() {
|
openVersionDialog() {
|
||||||
const template = this.detail.template
|
const template = this.detail.template
|
||||||
const builtin = this.detail.builtinRelease || {}
|
const versions = this.detail.versions || []
|
||||||
|
const managed = versions.find(item => item.promptVersionId === template.currentVersionId) || versions[0] || {}
|
||||||
this.versionReadOnly = false
|
this.versionReadOnly = false
|
||||||
this.versionDialogTitle = '新建 Prompt 版本'
|
this.versionDialogTitle = '新建 Prompt 版本'
|
||||||
this.versionForm = {
|
this.versionForm = {
|
||||||
version: '',
|
version: '',
|
||||||
providerCode: 'deepseek',
|
providerCode: managed.providerCode || 'deepseek',
|
||||||
model: '',
|
model: managed.model || '',
|
||||||
userPromptContract: builtin.userPromptContract || 'legacy-builder:' + template.generateType + ':v1',
|
userPromptContract: managed.userPromptContract || '',
|
||||||
userPromptTemplate: '{{input}}',
|
userPromptTemplate: managed.userPromptTemplate || '{{input}}',
|
||||||
systemPrompt: builtin.systemPrompt || '',
|
systemPrompt: managed.systemPrompt || '',
|
||||||
remark: ''
|
remark: ''
|
||||||
}
|
}
|
||||||
this.versionDialogOpen = true
|
this.versionDialogOpen = true
|
||||||
@@ -485,17 +482,6 @@ export default {
|
|||||||
this.getList()
|
this.getList()
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
},
|
},
|
||||||
initializeV1() {
|
|
||||||
this.$modal.confirm('将当前业务 Prompt 初始化为 V1 并立即发布;同时修复此前系统生成的同质化 V1,不覆盖人工版本。确认继续?').then(() => {
|
|
||||||
return initializePromptV1()
|
|
||||||
}).then(response => {
|
|
||||||
this.$modal.msgSuccess(response.msg || 'V1 初始化完成')
|
|
||||||
this.getList()
|
|
||||||
if (this.versionDrawerOpen && this.detail.template) {
|
|
||||||
this.loadDetail(this.detail.template.promptTemplateId)
|
|
||||||
}
|
|
||||||
}).catch(() => {})
|
|
||||||
},
|
|
||||||
loadModelGateways() {
|
loadModelGateways() {
|
||||||
listPromptModels().then(response => {
|
listPromptModels().then(response => {
|
||||||
this.modelGateways = response.data || []
|
this.modelGateways = response.data || []
|
||||||
|
|||||||
105
RuoYi-Vue/sql/prompt_flow_relation_field_upgrade_20260715.sql
Normal file
105
RuoYi-Vue/sql/prompt_flow_relation_field_upgrade_20260715.sql
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
-- Clarify the managed flow_config effect contract used by AI generation.
|
||||||
|
-- Idempotent: publish a new version only when the current prompt does not
|
||||||
|
-- already explain update_related.relationField.
|
||||||
|
|
||||||
|
DELIMITER $$
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS upgrade_flow_relation_field_prompt_20260715$$
|
||||||
|
|
||||||
|
CREATE PROCEDURE upgrade_flow_relation_field_prompt_20260715()
|
||||||
|
BEGIN
|
||||||
|
DECLARE v_template_id BIGINT;
|
||||||
|
DECLARE v_current_version_id BIGINT;
|
||||||
|
DECLARE v_new_version_id BIGINT;
|
||||||
|
DECLARE v_version_no INT;
|
||||||
|
DECLARE v_version_label VARCHAR(50);
|
||||||
|
DECLARE v_generate_type VARCHAR(50);
|
||||||
|
DECLARE v_prompt_code VARCHAR(100);
|
||||||
|
DECLARE v_system_prompt LONGTEXT;
|
||||||
|
DECLARE v_user_prompt_contract VARCHAR(255);
|
||||||
|
DECLARE v_user_prompt_template LONGTEXT;
|
||||||
|
DECLARE v_target_prompt LONGTEXT;
|
||||||
|
DECLARE v_provider_code VARCHAR(50);
|
||||||
|
DECLARE v_model VARCHAR(100);
|
||||||
|
DECLARE v_pipeline_release_code VARCHAR(100);
|
||||||
|
DECLARE v_content_hash CHAR(64);
|
||||||
|
DECLARE v_old_rule TEXT;
|
||||||
|
DECLARE v_new_rule TEXT;
|
||||||
|
|
||||||
|
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||||
|
BEGIN
|
||||||
|
ROLLBACK;
|
||||||
|
RESIGNAL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
SELECT t.prompt_template_id, t.current_version_id, t.generate_type, t.prompt_code,
|
||||||
|
v.system_prompt, v.user_prompt_contract, v.user_prompt_template,
|
||||||
|
v.provider_code, v.model, v.pipeline_release_code
|
||||||
|
INTO v_template_id, v_current_version_id, v_generate_type, v_prompt_code,
|
||||||
|
v_system_prompt, v_user_prompt_contract, v_user_prompt_template,
|
||||||
|
v_provider_code, v_model, v_pipeline_release_code
|
||||||
|
FROM factory_prompt_template t
|
||||||
|
JOIN factory_prompt_version v ON v.prompt_version_id = t.current_version_id
|
||||||
|
WHERE t.prompt_code = 'front.flow_config'
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
SET v_old_rule = '6. decrease/increase 必须给 table、relationField、field、amount;decrease 还要给 min。';
|
||||||
|
SET v_new_rule = '6. decrease、increase、update_related 必须给 table 和 relationField;relationField 是 ownerTable 上保存目标表主键的字段。update_related 更新当前记录自身(effect.table 等于 ownerTable)时,relationField 必须填写 ownerTable 主键请求字段,通常为 id。decrease/increase 还必须给 field、amount,decrease 还要给 min;只有 create_record 可以省略 relationField。';
|
||||||
|
|
||||||
|
IF LOCATE('只有 create_record 可以省略 relationField', v_system_prompt) = 0 THEN
|
||||||
|
IF LOCATE(v_old_rule, v_system_prompt) > 0 THEN
|
||||||
|
SET v_target_prompt = REPLACE(v_system_prompt, v_old_rule, v_new_rule);
|
||||||
|
ELSE
|
||||||
|
SET v_target_prompt = CONCAT(v_system_prompt, CHAR(10), v_new_rule);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT COALESCE(MAX(version_no), 0) + 1
|
||||||
|
INTO v_version_no
|
||||||
|
FROM factory_prompt_version
|
||||||
|
WHERE prompt_template_id = v_template_id
|
||||||
|
FOR UPDATE;
|
||||||
|
SET v_version_label = CONCAT('V', v_version_no);
|
||||||
|
SET v_content_hash = SHA2(CONCAT(
|
||||||
|
'{"generateType":', JSON_QUOTE(v_generate_type),
|
||||||
|
',"promptCode":', JSON_QUOTE(v_prompt_code),
|
||||||
|
',"systemPrompt":', JSON_QUOTE(v_target_prompt),
|
||||||
|
',"userPromptContract":', JSON_QUOTE(v_user_prompt_contract),
|
||||||
|
',"userPromptTemplate":', JSON_QUOTE(v_user_prompt_template),
|
||||||
|
',"version":', JSON_QUOTE(v_version_label), '}'), 256);
|
||||||
|
|
||||||
|
INSERT INTO factory_prompt_version
|
||||||
|
(prompt_template_id, version_no, version_label, system_prompt,
|
||||||
|
user_prompt_contract, user_prompt_template, provider_code, model,
|
||||||
|
content_hash, status, parent_version_id, pipeline_release_code,
|
||||||
|
published_by, published_at, create_by, create_time, remark)
|
||||||
|
VALUES
|
||||||
|
(v_template_id, v_version_no, v_version_label, v_target_prompt,
|
||||||
|
v_user_prompt_contract, v_user_prompt_template, v_provider_code, v_model,
|
||||||
|
v_content_hash, 'DRAFT', v_current_version_id, v_pipeline_release_code,
|
||||||
|
'', NULL, 'system-upgrade', NOW(),
|
||||||
|
'Require relationField for update_related effects');
|
||||||
|
SET v_new_version_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
UPDATE factory_prompt_version
|
||||||
|
SET status = 'RETIRED'
|
||||||
|
WHERE prompt_template_id = v_template_id
|
||||||
|
AND status = 'PUBLISHED'
|
||||||
|
AND prompt_version_id <> v_new_version_id;
|
||||||
|
UPDATE factory_prompt_version
|
||||||
|
SET status = 'PUBLISHED', published_by = 'system-upgrade', published_at = NOW()
|
||||||
|
WHERE prompt_version_id = v_new_version_id AND status = 'DRAFT';
|
||||||
|
UPDATE factory_prompt_template
|
||||||
|
SET current_version_id = v_new_version_id,
|
||||||
|
update_by = 'system-upgrade', update_time = NOW()
|
||||||
|
WHERE prompt_template_id = v_template_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
END$$
|
||||||
|
|
||||||
|
CALL upgrade_flow_relation_field_prompt_20260715()$$
|
||||||
|
DROP PROCEDURE upgrade_flow_relation_field_prompt_20260715$$
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
-- Upgrade the managed Prompt releases used by app blueprint and flow generation.
|
||||||
|
-- Idempotent: a template is only versioned when its current published content
|
||||||
|
-- does not already contain the resource-detail binding rule.
|
||||||
|
|
||||||
|
DELIMITER $$
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS upgrade_resource_detail_binding_prompts_20260715$$
|
||||||
|
|
||||||
|
CREATE PROCEDURE upgrade_resource_detail_binding_prompts_20260715()
|
||||||
|
BEGIN
|
||||||
|
DECLARE v_template_id BIGINT;
|
||||||
|
DECLARE v_current_version_id BIGINT;
|
||||||
|
DECLARE v_new_version_id BIGINT;
|
||||||
|
DECLARE v_version_no INT;
|
||||||
|
DECLARE v_version_label VARCHAR(50);
|
||||||
|
DECLARE v_generate_type VARCHAR(50);
|
||||||
|
DECLARE v_prompt_code VARCHAR(100);
|
||||||
|
DECLARE v_system_prompt LONGTEXT;
|
||||||
|
DECLARE v_user_prompt_contract VARCHAR(255);
|
||||||
|
DECLARE v_user_prompt_template LONGTEXT;
|
||||||
|
DECLARE v_target_prompt LONGTEXT;
|
||||||
|
DECLARE v_provider_code VARCHAR(50);
|
||||||
|
DECLARE v_model VARCHAR(100);
|
||||||
|
DECLARE v_pipeline_release_code VARCHAR(100);
|
||||||
|
DECLARE v_content_hash CHAR(64);
|
||||||
|
DECLARE v_anchor TEXT;
|
||||||
|
DECLARE v_rule TEXT;
|
||||||
|
|
||||||
|
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||||
|
BEGIN
|
||||||
|
ROLLBACK;
|
||||||
|
RESIGNAL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
SELECT t.prompt_template_id, t.current_version_id, t.generate_type, t.prompt_code,
|
||||||
|
v.system_prompt, v.user_prompt_contract, v.user_prompt_template,
|
||||||
|
v.provider_code, v.model, v.pipeline_release_code
|
||||||
|
INTO v_template_id, v_current_version_id, v_generate_type, v_prompt_code,
|
||||||
|
v_system_prompt, v_user_prompt_contract, v_user_prompt_template,
|
||||||
|
v_provider_code, v_model, v_pipeline_release_code
|
||||||
|
FROM factory_prompt_template t
|
||||||
|
JOIN factory_prompt_version v ON v.prompt_version_id = t.current_version_id
|
||||||
|
WHERE t.prompt_code = 'front.app_blueprint'
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
SET v_rule = '需求中只要存在用户针对单条资源记录发起并创建业务记录的动作(例如预约、申请、借用、报名、下单),就必须为资源列表创建同 tableName 的 detail 子页面;parentPageCode 指向资源列表页,path 使用列表路径加 /:id。此类动作后续必须落在详情页主操作区,不能依赖列表行操作。';
|
||||||
|
IF LOCATE('同 tableName 的 detail 子页面', v_user_prompt_template) = 0 THEN
|
||||||
|
SET v_anchor = 'frontendMenus 顶层菜单应指向 pageCode;详情页作为子页面,不创建顶层详情菜单。';
|
||||||
|
IF LOCATE(v_anchor, v_user_prompt_template) > 0 THEN
|
||||||
|
SET v_target_prompt = REPLACE(v_user_prompt_template, v_anchor,
|
||||||
|
CONCAT(v_anchor, CHAR(10), v_rule));
|
||||||
|
ELSE
|
||||||
|
SET v_target_prompt = CONCAT(v_user_prompt_template, CHAR(10), v_rule);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT COALESCE(MAX(version_no), 0) + 1
|
||||||
|
INTO v_version_no
|
||||||
|
FROM factory_prompt_version
|
||||||
|
WHERE prompt_template_id = v_template_id
|
||||||
|
FOR UPDATE;
|
||||||
|
SET v_version_label = CONCAT('V', v_version_no);
|
||||||
|
SET v_content_hash = SHA2(CONCAT(
|
||||||
|
'{"generateType":', JSON_QUOTE(v_generate_type),
|
||||||
|
',"promptCode":', JSON_QUOTE(v_prompt_code),
|
||||||
|
',"systemPrompt":', JSON_QUOTE(v_system_prompt),
|
||||||
|
',"userPromptContract":', JSON_QUOTE(v_user_prompt_contract),
|
||||||
|
',"userPromptTemplate":', JSON_QUOTE(v_target_prompt),
|
||||||
|
',"version":', JSON_QUOTE(v_version_label), '}'), 256);
|
||||||
|
|
||||||
|
INSERT INTO factory_prompt_version
|
||||||
|
(prompt_template_id, version_no, version_label, system_prompt,
|
||||||
|
user_prompt_contract, user_prompt_template, provider_code, model,
|
||||||
|
content_hash, status, parent_version_id, pipeline_release_code,
|
||||||
|
published_by, published_at, create_by, create_time, remark)
|
||||||
|
VALUES
|
||||||
|
(v_template_id, v_version_no, v_version_label, v_system_prompt,
|
||||||
|
v_user_prompt_contract, v_target_prompt, v_provider_code, v_model,
|
||||||
|
v_content_hash, 'DRAFT', v_current_version_id, v_pipeline_release_code,
|
||||||
|
'', NULL, 'system-upgrade', NOW(),
|
||||||
|
'Resource initiating actions require a same-table detail page');
|
||||||
|
SET v_new_version_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
UPDATE factory_prompt_version
|
||||||
|
SET status = 'RETIRED'
|
||||||
|
WHERE prompt_template_id = v_template_id
|
||||||
|
AND status = 'PUBLISHED'
|
||||||
|
AND prompt_version_id <> v_new_version_id;
|
||||||
|
UPDATE factory_prompt_version
|
||||||
|
SET status = 'PUBLISHED', published_by = 'system-upgrade', published_at = NOW()
|
||||||
|
WHERE prompt_version_id = v_new_version_id AND status = 'DRAFT';
|
||||||
|
UPDATE factory_prompt_template
|
||||||
|
SET current_version_id = v_new_version_id,
|
||||||
|
update_by = 'system-upgrade', update_time = NOW()
|
||||||
|
WHERE prompt_template_id = v_template_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT t.prompt_template_id, t.current_version_id, t.generate_type, t.prompt_code,
|
||||||
|
v.system_prompt, v.user_prompt_contract, v.user_prompt_template,
|
||||||
|
v.provider_code, v.model, v.pipeline_release_code
|
||||||
|
INTO v_template_id, v_current_version_id, v_generate_type, v_prompt_code,
|
||||||
|
v_system_prompt, v_user_prompt_contract, v_user_prompt_template,
|
||||||
|
v_provider_code, v_model, v_pipeline_release_code
|
||||||
|
FROM factory_prompt_template t
|
||||||
|
JOIN factory_prompt_version v ON v.prompt_version_id = t.current_version_id
|
||||||
|
WHERE t.prompt_code = 'front.flow_config'
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
SET v_rule = '12. 用户针对单条资源记录发起并创建主业务记录的动作(ownerTable 不等于 mainTable、无 from/to、create_record 到 mainTable)必须绑定同 ownerTable 的 detail 页面和 detail.primaryActions,例如预约、申请、借用、报名、下单。还书、取消、审核等已有业务记录上的状态流转动作仍可绑定业务记录列表的 list.rowActions。禁止把资源发起动作放在资源列表的 list.rowActions。';
|
||||||
|
IF LOCATE('禁止把资源发起动作放在资源列表的 list.rowActions', v_system_prompt) = 0 THEN
|
||||||
|
SET v_target_prompt = CONCAT(v_system_prompt, CHAR(10), v_rule);
|
||||||
|
|
||||||
|
SELECT COALESCE(MAX(version_no), 0) + 1
|
||||||
|
INTO v_version_no
|
||||||
|
FROM factory_prompt_version
|
||||||
|
WHERE prompt_template_id = v_template_id
|
||||||
|
FOR UPDATE;
|
||||||
|
SET v_version_label = CONCAT('V', v_version_no);
|
||||||
|
SET v_content_hash = SHA2(CONCAT(
|
||||||
|
'{"generateType":', JSON_QUOTE(v_generate_type),
|
||||||
|
',"promptCode":', JSON_QUOTE(v_prompt_code),
|
||||||
|
',"systemPrompt":', JSON_QUOTE(v_target_prompt),
|
||||||
|
',"userPromptContract":', JSON_QUOTE(v_user_prompt_contract),
|
||||||
|
',"userPromptTemplate":', JSON_QUOTE(v_user_prompt_template),
|
||||||
|
',"version":', JSON_QUOTE(v_version_label), '}'), 256);
|
||||||
|
|
||||||
|
INSERT INTO factory_prompt_version
|
||||||
|
(prompt_template_id, version_no, version_label, system_prompt,
|
||||||
|
user_prompt_contract, user_prompt_template, provider_code, model,
|
||||||
|
content_hash, status, parent_version_id, pipeline_release_code,
|
||||||
|
published_by, published_at, create_by, create_time, remark)
|
||||||
|
VALUES
|
||||||
|
(v_template_id, v_version_no, v_version_label, v_target_prompt,
|
||||||
|
v_user_prompt_contract, v_user_prompt_template, v_provider_code, v_model,
|
||||||
|
v_content_hash, 'DRAFT', v_current_version_id, v_pipeline_release_code,
|
||||||
|
'', NULL, 'system-upgrade', NOW(),
|
||||||
|
'Resource initiating actions bind to detail primary actions');
|
||||||
|
SET v_new_version_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
UPDATE factory_prompt_version
|
||||||
|
SET status = 'RETIRED'
|
||||||
|
WHERE prompt_template_id = v_template_id
|
||||||
|
AND status = 'PUBLISHED'
|
||||||
|
AND prompt_version_id <> v_new_version_id;
|
||||||
|
UPDATE factory_prompt_version
|
||||||
|
SET status = 'PUBLISHED', published_by = 'system-upgrade', published_at = NOW()
|
||||||
|
WHERE prompt_version_id = v_new_version_id AND status = 'DRAFT';
|
||||||
|
UPDATE factory_prompt_template
|
||||||
|
SET current_version_id = v_new_version_id,
|
||||||
|
update_by = 'system-upgrade', update_time = NOW()
|
||||||
|
WHERE prompt_template_id = v_template_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
END$$
|
||||||
|
|
||||||
|
CALL upgrade_resource_detail_binding_prompts_20260715()$$
|
||||||
|
DROP PROCEDURE upgrade_resource_detail_binding_prompts_20260715$$
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
13
RuoYi-Vue/tmp-logs/logback-local.xml
Normal file
13
RuoYi-Vue/tmp-logs/logback-local.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="console" />
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
1
RuoYi-Vue/tmp-logs/metric-empty-current-backend.pid
Normal file
1
RuoYi-Vue/tmp-logs/metric-empty-current-backend.pid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
20464
|
||||||
1
RuoYi-Vue/tmp-logs/metric-empty-fixed-backend.pid
Normal file
1
RuoYi-Vue/tmp-logs/metric-empty-fixed-backend.pid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
33372
|
||||||
5
RuoYi-Vue/tmp-logs/qing-ui-preview/babel.config.js
Normal file
5
RuoYi-Vue/tmp-logs/qing-ui-preview/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
"@vue/cli-plugin-babel/preset"
|
||||||
|
]
|
||||||
|
}
|
||||||
24
RuoYi-Vue/tmp-logs/qing-ui-preview/package.json
Normal file
24
RuoYi-Vue/tmp-logs/qing-ui-preview/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "product-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vue-cli-service serve --host 0.0.0.0",
|
||||||
|
"serve": "vue-cli-service serve --host 0.0.0.0",
|
||||||
|
"build": "vue-cli-service build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"echarts": "5.4.0",
|
||||||
|
"element-ui": "^2.15.14",
|
||||||
|
"quill": "^1.3.7",
|
||||||
|
"vue": "^2.6.14",
|
||||||
|
"vue-quill-editor": "^3.0.6",
|
||||||
|
"vue-router": "^3.6.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-service": "^4.5.19",
|
||||||
|
"vue-template-compiler": "^2.6.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
RuoYi-Vue/tmp-logs/qing-ui-preview/public/index.html
Normal file
13
RuoYi-Vue/tmp-logs/qing-ui-preview/public/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>演示商品</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>请启用 JavaScript 后访问。</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
543
RuoYi-Vue/tmp-logs/qing-ui-preview/src/App.vue
Normal file
543
RuoYi-Vue/tmp-logs/qing-ui-preview/src/App.vue
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
<template>
|
||||||
|
<div class="portal-shell">
|
||||||
|
<template v-if="!isLoginRoute">
|
||||||
|
<header class="top-nav">
|
||||||
|
<router-link class="brand" to="/demo/product">
|
||||||
|
<span class="brand-logo" aria-hidden="true"><i></i></span>
|
||||||
|
<span>演示商品</span>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<nav class="portal-nav" aria-label="主导航">
|
||||||
|
<div class="portal-nav-group">
|
||||||
|
<router-link class="portal-nav-parent" to="/demo/product" exact>首页</router-link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="user-area">
|
||||||
|
<template v-if="currentUser">
|
||||||
|
<button class="user-chip" type="button">
|
||||||
|
<span class="avatar">{{ userInitial }}</span>
|
||||||
|
<span>{{ currentUserName }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="logout-link" type="button" @click="logout">退出</button>
|
||||||
|
</template>
|
||||||
|
<button v-else class="login-link" type="button" @click="goLogin">登录</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section v-if="isHomeRoute" class="hero-banner" aria-label="项目横幅">
|
||||||
|
<div class="hero-pattern" aria-hidden="true"></div>
|
||||||
|
<div class="hero-content">
|
||||||
|
<span class="hero-eyebrow">WELCOME</span>
|
||||||
|
<h1>演示商品</h1>
|
||||||
|
<p>发现内容,连接服务,轻松完成每一次访问。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<main :class="['portal-main', { 'portal-main-login': isLoginRoute }]">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "App",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentUser: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoginRoute() {
|
||||||
|
return this.$route.path === "/login"
|
||||||
|
},
|
||||||
|
isHomeRoute() {
|
||||||
|
return this.$route.path === "/demo/product"
|
||||||
|
},
|
||||||
|
currentUserName() {
|
||||||
|
return this.currentUser && (this.currentUser.displayName || this.currentUser.account || this.currentUser.nickname || this.currentUser.username || "User")
|
||||||
|
},
|
||||||
|
userInitial() {
|
||||||
|
const name = this.currentUserName || "U"
|
||||||
|
return name.slice(0, 1).toUpperCase()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadUser()
|
||||||
|
window.addEventListener("portal-user-change", this.loadUser)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener("portal-user-change", this.loadUser)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
canShowMenu(visibleRoles) {
|
||||||
|
if (!Array.isArray(visibleRoles) || visibleRoles.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const roleCode = this.currentUser && this.currentUser.roleCode
|
||||||
|
return Boolean(roleCode) && visibleRoles.indexOf(roleCode) !== -1
|
||||||
|
},
|
||||||
|
isNavGroupActive(paths) {
|
||||||
|
return Array.isArray(paths) && paths.indexOf(this.$route.path) !== -1
|
||||||
|
},
|
||||||
|
loadUser() {
|
||||||
|
const raw = localStorage.getItem("portal-user")
|
||||||
|
if (!raw) {
|
||||||
|
this.currentUser = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.currentUser = JSON.parse(raw)
|
||||||
|
} catch (error) {
|
||||||
|
localStorage.removeItem("portal-user")
|
||||||
|
this.currentUser = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goLogin() {
|
||||||
|
this.$router.push({
|
||||||
|
path: "/login",
|
||||||
|
query: { redirect: this.$route.fullPath }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
logout() {
|
||||||
|
this.request.post("/auth/logout").catch(() => {})
|
||||||
|
localStorage.removeItem("portal-token")
|
||||||
|
localStorage.removeItem("portal-user")
|
||||||
|
window.dispatchEvent(new Event("portal-user-change"))
|
||||||
|
this.$router.push("/login")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--portal-primary: #167d83;
|
||||||
|
--portal-primary-strong: #11656a;
|
||||||
|
--portal-primary-soft: #eaf7f7;
|
||||||
|
--portal-accent: #2f72d6;
|
||||||
|
--portal-text: #172033;
|
||||||
|
--portal-text-secondary: #5f6b7a;
|
||||||
|
--portal-border: #e3e9f1;
|
||||||
|
--portal-surface: #ffffff;
|
||||||
|
--portal-background: #f5f7fb;
|
||||||
|
--portal-radius-sm: 8px;
|
||||||
|
--portal-radius-md: 12px;
|
||||||
|
--portal-radius-lg: 18px;
|
||||||
|
--portal-shadow-sm: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
--portal-shadow-md: 0 18px 44px rgba(15, 23, 42, 0.12);
|
||||||
|
--portal-content-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--portal-background);
|
||||||
|
color: var(--portal-text);
|
||||||
|
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible,
|
||||||
|
a:focus-visible {
|
||||||
|
outline: 3px solid rgba(47, 114, 214, 0.28);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--portal-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, auto) minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
min-height: 72px;
|
||||||
|
padding: 0 32px;
|
||||||
|
color: var(--portal-text);
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
border-bottom: 1px solid var(--portal-border);
|
||||||
|
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand,
|
||||||
|
.portal-nav,
|
||||||
|
.user-area,
|
||||||
|
.user-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand span:last-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex: 0 0 38px;
|
||||||
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 11px;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, var(--portal-primary), var(--portal-accent));
|
||||||
|
box-shadow: 0 7px 18px rgba(22, 125, 131, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo::before,
|
||||||
|
.brand-logo::after,
|
||||||
|
.brand-logo i {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
transform: rotate(-35deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo::before {
|
||||||
|
width: 20px;
|
||||||
|
height: 7px;
|
||||||
|
top: 9px;
|
||||||
|
left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo i {
|
||||||
|
width: 15px;
|
||||||
|
height: 6px;
|
||||||
|
top: 17px;
|
||||||
|
left: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo::after {
|
||||||
|
width: 10px;
|
||||||
|
height: 5px;
|
||||||
|
top: 25px;
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-group {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-parent,
|
||||||
|
.portal-nav-toggle,
|
||||||
|
.portal-nav-child {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-parent {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: var(--portal-radius-sm);
|
||||||
|
color: var(--portal-text);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-parent:hover,
|
||||||
|
.portal-nav-parent.router-link-active,
|
||||||
|
.portal-nav-group.active .portal-nav-parent {
|
||||||
|
color: var(--portal-primary-strong);
|
||||||
|
background: var(--portal-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-directory {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-toggle {
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin-left: -8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-group:hover .portal-nav-toggle,
|
||||||
|
.portal-nav-group:focus-within .portal-nav-toggle,
|
||||||
|
.portal-nav-group.active .portal-nav-toggle {
|
||||||
|
color: var(--portal-primary-strong);
|
||||||
|
background: var(--portal-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% - 8px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 30;
|
||||||
|
display: none;
|
||||||
|
min-width: 168px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--portal-border);
|
||||||
|
border-radius: var(--portal-radius-md);
|
||||||
|
background: var(--portal-surface);
|
||||||
|
box-shadow: var(--portal-shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-group:hover .portal-nav-dropdown,
|
||||||
|
.portal-nav-group:focus-within .portal-nav-dropdown {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-child {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-child:hover,
|
||||||
|
.portal-nav-child.router-link-active {
|
||||||
|
color: var(--portal-primary-strong);
|
||||||
|
background: var(--portal-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-area {
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-chip,
|
||||||
|
.login-link,
|
||||||
|
.logout-link {
|
||||||
|
border: 1px solid var(--portal-border);
|
||||||
|
color: var(--portal-text-secondary);
|
||||||
|
background: var(--portal-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-chip {
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 10px 0 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: inline-grid;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #bfe4e5;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--portal-primary-strong);
|
||||||
|
background: var(--portal-primary-soft);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-link,
|
||||||
|
.login-link {
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: var(--portal-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-link:hover,
|
||||||
|
.login-link:hover {
|
||||||
|
color: var(--portal-primary-strong);
|
||||||
|
background: var(--portal-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-banner {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
min-height: 228px;
|
||||||
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
linear-gradient(132deg, rgba(20, 124, 131, 0.97), rgba(47, 114, 214, 0.91)),
|
||||||
|
var(--portal-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-pattern {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.58;
|
||||||
|
background:
|
||||||
|
linear-gradient(42deg, transparent 0 18%, rgba(255, 255, 255, 0.18) 18% 27%, transparent 27% 100%),
|
||||||
|
linear-gradient(132deg, transparent 0 48%, rgba(246, 186, 51, 0.32) 48% 55%, transparent 55% 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(var(--portal-content-width), calc(100% - 48px));
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-eyebrow {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.78);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-banner h1 {
|
||||||
|
max-width: 780px;
|
||||||
|
margin: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: clamp(34px, 4vw, 48px);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-shadow: 0 12px 28px rgba(15, 23, 42, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-banner p {
|
||||||
|
max-width: 620px;
|
||||||
|
margin: 14px 0 0;
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-main {
|
||||||
|
width: min(var(--portal-content-width), calc(100% - 48px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 0 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-main-login {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.top-nav {
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 10px;
|
||||||
|
height: auto;
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-content: flex-start;
|
||||||
|
order: 3;
|
||||||
|
gap: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-group {
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-parent {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-nav-dropdown {
|
||||||
|
position: fixed;
|
||||||
|
top: 104px;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-banner {
|
||||||
|
min-height: 176px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
width: min(100% - 32px, var(--portal-content-width));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-banner p {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-main {
|
||||||
|
width: min(calc(100% - 28px), var(--portal-content-width));
|
||||||
|
padding: 22px 0 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.brand {
|
||||||
|
max-width: calc(100vw - 150px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand span:last-child {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-area {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-chip > span:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-banner {
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-eyebrow {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
RuoYi-Vue/tmp-logs/qing-ui-preview/src/main.js
Normal file
21
RuoYi-Vue/tmp-logs/qing-ui-preview/src/main.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import Vue from "vue"
|
||||||
|
import ElementUI from "element-ui"
|
||||||
|
import VueQuillEditor from "vue-quill-editor"
|
||||||
|
import "element-ui/lib/theme-chalk/index.css"
|
||||||
|
import "quill/dist/quill.core.css"
|
||||||
|
import "quill/dist/quill.snow.css"
|
||||||
|
import App from "./App.vue"
|
||||||
|
import router from "./router"
|
||||||
|
import request from "./utils/request"
|
||||||
|
import dict from "./utils/dict"
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
Vue.use(ElementUI)
|
||||||
|
Vue.use(VueQuillEditor)
|
||||||
|
Vue.prototype.request = request
|
||||||
|
Vue.prototype._dict = dict
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount("#app")
|
||||||
50
RuoYi-Vue/tmp-logs/qing-ui-preview/src/router/index.js
Normal file
50
RuoYi-Vue/tmp-logs/qing-ui-preview/src/router/index.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import Vue from "vue"
|
||||||
|
import Router from "vue-router"
|
||||||
|
import Login from "@/views/login/index.vue"
|
||||||
|
import Product from "@/views/demo/product/index.vue"
|
||||||
|
|
||||||
|
Vue.use(Router)
|
||||||
|
|
||||||
|
const router = new Router({
|
||||||
|
mode: "hash",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
redirect: "/demo/product"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "Login",
|
||||||
|
component: Login
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/demo/product",
|
||||||
|
name: "Product",
|
||||||
|
component: Product,
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/secondary",
|
||||||
|
name: "Secondary",
|
||||||
|
component: Product,
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const requiresAuth = to.matched.some(record => record.meta && record.meta.requiresAuth)
|
||||||
|
const hasToken = Boolean(localStorage.getItem("portal-token"))
|
||||||
|
|
||||||
|
if (requiresAuth && !hasToken) {
|
||||||
|
next({
|
||||||
|
path: "/login",
|
||||||
|
query: { redirect: to.fullPath }
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
1
RuoYi-Vue/tmp-logs/qing-ui-preview/src/style.css
Normal file
1
RuoYi-Vue/tmp-logs/qing-ui-preview/src/style.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
body { margin: 0; }
|
||||||
32
RuoYi-Vue/tmp-logs/qing-ui-preview/src/utils/dict.js
Normal file
32
RuoYi-Vue/tmp-logs/qing-ui-preview/src/utils/dict.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const dictData = {}
|
||||||
|
|
||||||
|
const emptyDict = []
|
||||||
|
|
||||||
|
function normalizeOptions(options) {
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
return emptyDict
|
||||||
|
}
|
||||||
|
return options.map((option) => {
|
||||||
|
const label = option.label || option.name || option.value
|
||||||
|
const value = option.value || label
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
label,
|
||||||
|
name: option.name || label,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getDictDataByType(type) {
|
||||||
|
return normalizeOptions(dictData[type])
|
||||||
|
},
|
||||||
|
getDictLabel(type, value) {
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
const matched = normalizeOptions(dictData[type]).find((option) => String(option.value) === String(value))
|
||||||
|
return matched ? matched.label : value
|
||||||
|
}
|
||||||
|
}
|
||||||
6
RuoYi-Vue/tmp-logs/qing-ui-preview/src/utils/request.js
Normal file
6
RuoYi-Vue/tmp-logs/qing-ui-preview/src/utils/request.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const page = { data: { records: [], total: 0 } }
|
||||||
|
export default {
|
||||||
|
get() { return Promise.resolve(page) },
|
||||||
|
post(path) { return Promise.resolve(path === '/auth/login' ? { code: '200', data: { token: 'preview-token', user: { displayName: 'Preview User' } } } : { code: '200' }) },
|
||||||
|
delete() { return Promise.resolve({ code: '200' }) }
|
||||||
|
}
|
||||||
1122
RuoYi-Vue/tmp-logs/qing-ui-preview/src/views/demo/product/index.vue
Normal file
1122
RuoYi-Vue/tmp-logs/qing-ui-preview/src/views/demo/product/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
291
RuoYi-Vue/tmp-logs/qing-ui-preview/src/views/login/index.vue
Normal file
291
RuoYi-Vue/tmp-logs/qing-ui-preview/src/views/login/index.vue
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
<template>
|
||||||
|
<section class="portal-login">
|
||||||
|
<div class="login-visual" aria-hidden="true">
|
||||||
|
<div class="login-brand">
|
||||||
|
<span class="login-logo"></span>
|
||||||
|
<strong>演示商品</strong>
|
||||||
|
</div>
|
||||||
|
<h1>欢迎使用</h1>
|
||||||
|
<p>登录后继续访问项目功能与个人服务。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-panel">
|
||||||
|
<div class="login-heading">
|
||||||
|
<span>欢迎回来</span>
|
||||||
|
<h2>账号登录</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="roleOptions.length === 0"
|
||||||
|
title="当前没有可登录的前台角色,请联系管理员检查系统角色配置。"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
class="login-alert"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-form :model="form" label-position="top" class="login-form" @submit.native.prevent>
|
||||||
|
<el-form-item label="账号">
|
||||||
|
<el-input v-model="form.account" placeholder="请输入账号" prefix-icon="el-icon-user" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码">
|
||||||
|
<el-input v-model="form.password" type="password" placeholder="请输入密码" prefix-icon="el-icon-lock" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-button type="primary" class="login-button" :loading="loading" :disabled="roleOptions.length === 0" @click="handleLogin">登录</el-button>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const roleOptions = [
|
||||||
|
]
|
||||||
|
|
||||||
|
const defaultRole = roleOptions.length > 0 ? roleOptions[0] : null
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PortalLogin",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
roleOptions,
|
||||||
|
form: {
|
||||||
|
roleCode: defaultRole ? defaultRole.value : "",
|
||||||
|
account: defaultRole ? defaultRole.sampleAccount : "",
|
||||||
|
password: "123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"form.roleCode"(roleCode) {
|
||||||
|
const role = this.roleOptions.find(item => item.value === roleCode)
|
||||||
|
if (role && (!this.form.account || this.roleOptions.some(item => item.sampleAccount === this.form.account))) {
|
||||||
|
this.form.account = role.sampleAccount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleLogin() {
|
||||||
|
if (!this.form.roleCode) {
|
||||||
|
this.$message.warning("请选择登录角色")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.form.account || !this.form.password) {
|
||||||
|
this.$message.warning("请输入账号和密码")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.request.post("/auth/login", this.form)
|
||||||
|
.then(res => {
|
||||||
|
if (!res || res.code !== "200" || !res.data || !res.data.token) {
|
||||||
|
throw new Error((res && res.msg) || "登录失败")
|
||||||
|
}
|
||||||
|
localStorage.setItem("portal-token", res.data.token)
|
||||||
|
localStorage.setItem("portal-user", JSON.stringify(res.data.user || {
|
||||||
|
account: this.form.account,
|
||||||
|
displayName: this.form.account,
|
||||||
|
roleCode: this.form.roleCode
|
||||||
|
}))
|
||||||
|
window.dispatchEvent(new Event("portal-user-change"))
|
||||||
|
this.$message.success("登录成功")
|
||||||
|
const redirect = this.$route.query.redirect || "/demo/product"
|
||||||
|
this.$router.push(redirect)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$message.error(error.message || "登录失败")
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.portal-login {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(430px, 36vw);
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
linear-gradient(132deg, rgba(20, 124, 131, 0.97), rgba(47, 114, 214, 0.91)),
|
||||||
|
var(--portal-primary, #167d83);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-visual {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 72px clamp(48px, 8vw, 128px);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-visual::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
opacity: 0.58;
|
||||||
|
background:
|
||||||
|
linear-gradient(42deg, transparent 0 18%, rgba(255, 255, 255, 0.18) 18% 27%, transparent 27% 100%),
|
||||||
|
linear-gradient(132deg, transparent 0 48%, rgba(246, 186, 51, 0.32) 48% 55%, transparent 55% 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand,
|
||||||
|
.login-visual h1,
|
||||||
|
.login-visual p {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 9px 19px 9px 19px;
|
||||||
|
background: linear-gradient(135deg, #6de5e8 0 38%, #f6c85b 38% 66%, #ffffff 66%);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-visual h1,
|
||||||
|
.login-visual p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-visual h1 {
|
||||||
|
max-width: 680px;
|
||||||
|
font-size: clamp(42px, 5vw, 64px);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.18;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-visual p {
|
||||||
|
max-width: 520px;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: clamp(40px, 5vw, 64px);
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: -18px 0 48px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-heading span {
|
||||||
|
color: var(--portal-primary, #167d83);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-heading h2 {
|
||||||
|
margin: 8px 0 28px;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-alert {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-select,
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form >>> .el-input__inner,
|
||||||
|
.login-form >>> .el-select .el-input__inner {
|
||||||
|
height: 44px;
|
||||||
|
border-color: #dfe5ec;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form >>> .el-input__inner:focus {
|
||||||
|
border-color: var(--portal-primary, #167d83);
|
||||||
|
box-shadow: 0 0 0 3px rgba(22, 125, 131, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
height: 44px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-color: var(--portal-primary, #167d83);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--portal-primary, #167d83);
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 10px 22px rgba(22, 125, 131, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover,
|
||||||
|
.login-button:focus {
|
||||||
|
border-color: var(--portal-primary-strong, #11656a);
|
||||||
|
background: var(--portal-primary-strong, #11656a);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 880px) {
|
||||||
|
.portal-login {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-visual {
|
||||||
|
min-height: 220px;
|
||||||
|
padding: 34px 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-visual h1 {
|
||||||
|
font-size: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel {
|
||||||
|
padding: 34px 24px 44px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.login-visual {
|
||||||
|
min-height: 184px;
|
||||||
|
padding: 26px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-visual h1 {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-visual p {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
RuoYi-Vue/tmp-logs/qing-ui-preview/vue.config.js
Normal file
18
RuoYi-Vue/tmp-logs/qing-ui-preview/vue.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const port = process.env.PORT || 8081
|
||||||
|
const apiBaseUrl = process.env.VUE_APP_API_BASE_URL || "http://localhost:8080"
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
transpileDependencies: [],
|
||||||
|
devServer: {
|
||||||
|
port,
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: apiBaseUrl,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/api": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user