Add async AI generation tasks
This commit is contained in:
@@ -76,6 +76,49 @@ export function generateBusinessBlueprint(projectId, data) {
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function createAiTask(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/ai-tasks`,
|
||||
method: 'post',
|
||||
data
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function getAiTask(projectId, taskId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/ai-tasks/${taskId}`,
|
||||
method: 'get'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function retryAiTask(projectId, taskId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/ai-tasks/${taskId}/retry`,
|
||||
method: 'post'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function cancelAiTask(projectId, taskId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/ai-tasks/${taskId}/cancel`,
|
||||
method: 'post'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function getAiQuota() {
|
||||
return request({
|
||||
url: '/front/project/ai-quota',
|
||||
method: 'get'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function listGenerations(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/generations`,
|
||||
method: 'get'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function getDatabase(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/database`,
|
||||
|
||||
@@ -105,6 +105,31 @@
|
||||
预览项目
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeAiTask || aiQuota" class="ai-task-strip">
|
||||
<div v-if="activeAiTask" class="ai-task-card">
|
||||
<div class="ai-task-card-header">
|
||||
<strong>{{ taskTypeLabel(activeAiTask.generateType) }}</strong>
|
||||
<el-tag size="small" effect="plain">{{ taskStatusLabel(activeAiTask.status) }}</el-tag>
|
||||
</div>
|
||||
<el-progress :percentage="Number(activeAiTask.progress || 0)" :stroke-width="8" />
|
||||
<p class="muted">{{ activeAiTask.currentStep || '-' }}</p>
|
||||
<p v-if="activeAiTask.errorMessage" class="task-error">{{ activeAiTask.errorMessage }}</p>
|
||||
<el-button
|
||||
v-if="activeAiTask.status === 'FAILED' || activeAiTask.status === 'RETRY_WAITING'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleRetryActiveTask"
|
||||
>
|
||||
重试
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="aiQuota" class="ai-quota-card">
|
||||
<span>今日 {{ aiQuota.dailyTaskUsed || 0 }}/{{ aiQuota.dailyTaskLimit || 0 }}</span>
|
||||
<span>运行中 {{ aiQuota.runningCount || 0 }}/{{ aiQuota.runningLimit || 0 }}</span>
|
||||
<span>本月成本 {{ aiQuota.monthlyCostUsedCents || 0 }}/{{ aiQuota.monthlyCostLimitCents || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -220,14 +245,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { DataAnalysis, View } from '@element-plus/icons-vue'
|
||||
import AppModuleDiagramView from '@/components/AppModuleDiagramView.vue'
|
||||
import DatabaseDesigner from '@/components/DatabaseDesigner.vue'
|
||||
import ErDiagramView from '@/components/ErDiagramView.vue'
|
||||
import { createProject, generateAppBlueprint, generateBusinessBlueprint, generateDatabase, generateProject, getDatabase, getProject, listAvailableModules, saveDatabase, updateProject } from '@/api/project'
|
||||
import { createAiTask, createProject, generateProject, getAiQuota, getAiTask, getDatabase, getProject, listAvailableModules, retryAiTask, saveDatabase, updateProject } from '@/api/project'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -247,6 +272,9 @@ const appBlueprint = ref(emptyAppBlueprint())
|
||||
const appBlueprintJson = ref(formatJson(emptyAppBlueprint()))
|
||||
const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram(), businessActions: [] })
|
||||
const availableModules = ref([])
|
||||
const activeAiTask = ref(null)
|
||||
const aiQuota = ref(null)
|
||||
let stopAiPolling = false
|
||||
const projectForm = reactive({
|
||||
projectName: '',
|
||||
keyword: String(route.query.keyword || ''),
|
||||
@@ -332,6 +360,51 @@ function normalizeBusinessActions(payload) {
|
||||
return Array.isArray(data) ? data : []
|
||||
}
|
||||
|
||||
function parseTaskResult(task) {
|
||||
if (!task?.resultPayload) return null
|
||||
try {
|
||||
return JSON.parse(task.resultPayload)
|
||||
} catch (error) {
|
||||
return task.resultPayload
|
||||
}
|
||||
}
|
||||
|
||||
function taskTypeLabel(type) {
|
||||
const labels = {
|
||||
app_blueprint: '应用蓝图',
|
||||
database: '数据库',
|
||||
business_blueprint: '业务蓝图'
|
||||
}
|
||||
return labels[type] || type || 'AI 生成'
|
||||
}
|
||||
|
||||
function taskStatusLabel(status) {
|
||||
const labels = {
|
||||
QUEUED: '排队中',
|
||||
RUNNING: '生成中',
|
||||
RETRY_WAITING: '等待重试',
|
||||
SUCCEEDED: '已完成',
|
||||
FAILED: '失败',
|
||||
CANCELED: '已取消'
|
||||
}
|
||||
return labels[status] || status || '-'
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function aiTaskPayload(generateType) {
|
||||
return {
|
||||
generateType,
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
industryTemplate: projectForm.industryTemplate,
|
||||
extraRequirements: projectForm.keyword,
|
||||
appBlueprint: appBlueprintJson.value
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProjectId(value) {
|
||||
const raw = Array.isArray(value) ? value[0] : value
|
||||
const id = Number(raw)
|
||||
@@ -533,6 +606,57 @@ function validateRequirementInput() {
|
||||
return true
|
||||
}
|
||||
|
||||
async function refreshAiQuota() {
|
||||
try {
|
||||
aiQuota.value = await getAiQuota()
|
||||
} catch (error) {
|
||||
aiQuota.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function applyAiTaskResult(task) {
|
||||
const payload = parseTaskResult(task)
|
||||
if (task.generateType === 'app_blueprint') {
|
||||
appBlueprint.value = normalizeAppBlueprint(payload)
|
||||
syncAppBlueprintJson()
|
||||
return
|
||||
}
|
||||
if (task.generateType === 'database') {
|
||||
database.value = normalizeDatabase(payload)
|
||||
return
|
||||
}
|
||||
if (task.generateType === 'business_blueprint') {
|
||||
database.value = {
|
||||
...database.value,
|
||||
businessActions: normalizeBusinessActions(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForAiTask(id, initialTask) {
|
||||
let task = initialTask
|
||||
activeAiTask.value = task
|
||||
while (!stopAiPolling && task && !['SUCCEEDED', 'FAILED', 'CANCELED'].includes(task.status)) {
|
||||
await delay(2000)
|
||||
task = await getAiTask(id, task.taskId)
|
||||
activeAiTask.value = task
|
||||
}
|
||||
|
||||
if (task?.status === 'SUCCEEDED') {
|
||||
applyAiTaskResult(task)
|
||||
await refreshAiQuota()
|
||||
return task
|
||||
}
|
||||
|
||||
throw new Error(task?.errorMessage || 'AI 生成任务未完成')
|
||||
}
|
||||
|
||||
async function startAiTask(generateType) {
|
||||
const id = await ensureProject()
|
||||
const task = await createAiTask(id, aiTaskPayload(generateType))
|
||||
return waitForAiTask(id, task)
|
||||
}
|
||||
|
||||
async function handleGenerateAppBlueprint() {
|
||||
if (!validateRequirementInput()) {
|
||||
return
|
||||
@@ -541,15 +665,7 @@ async function handleGenerateAppBlueprint() {
|
||||
generatingAppBlueprint.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const id = await ensureProject()
|
||||
const result = await generateAppBlueprint(id, {
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
industryTemplate: projectForm.industryTemplate,
|
||||
extraRequirements: projectForm.keyword
|
||||
})
|
||||
appBlueprint.value = normalizeAppBlueprint(result)
|
||||
syncAppBlueprintJson()
|
||||
await startAiTask('app_blueprint')
|
||||
ElMessage.success('应用蓝图已生成')
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '生成应用蓝图失败,请稍后重试。'
|
||||
@@ -581,15 +697,7 @@ async function handleGenerateDatabase() {
|
||||
generating.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const id = await ensureProject()
|
||||
const result = await generateDatabase(id, {
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
industryTemplate: projectForm.industryTemplate,
|
||||
extraRequirements: projectForm.keyword,
|
||||
appBlueprint: appBlueprintJson.value
|
||||
})
|
||||
database.value = normalizeDatabase(result)
|
||||
await startAiTask('database')
|
||||
|
||||
if (!database.value.tables.length) {
|
||||
errorMessage.value = '后端暂未返回表结构,请稍后重试或手动添加表。'
|
||||
@@ -618,16 +726,7 @@ async function handleGenerateBusinessBlueprint() {
|
||||
try {
|
||||
const id = await ensureProject()
|
||||
await saveDatabase(id, database.value)
|
||||
const result = await generateBusinessBlueprint(id, {
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
industryTemplate: projectForm.industryTemplate,
|
||||
extraRequirements: projectForm.keyword
|
||||
})
|
||||
database.value = {
|
||||
...database.value,
|
||||
businessActions: normalizeBusinessActions(result)
|
||||
}
|
||||
await waitForAiTask(id, await createAiTask(id, aiTaskPayload('business_blueprint')))
|
||||
ElMessage.success('业务蓝图已生成')
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '生成业务蓝图失败,请确认表结构已保存后重试。'
|
||||
@@ -636,6 +735,27 @@ async function handleGenerateBusinessBlueprint() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRetryActiveTask() {
|
||||
if (!projectId.value || !activeAiTask.value?.taskId) return
|
||||
|
||||
errorMessage.value = ''
|
||||
const type = activeAiTask.value.generateType
|
||||
if (type === 'app_blueprint') generatingAppBlueprint.value = true
|
||||
if (type === 'database') generating.value = true
|
||||
if (type === 'business_blueprint') generatingBusinessBlueprint.value = true
|
||||
try {
|
||||
const task = await retryAiTask(projectId.value, activeAiTask.value.taskId)
|
||||
await waitForAiTask(projectId.value, task)
|
||||
ElMessage.success(`${taskTypeLabel(type)}已生成`)
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || 'AI 生成任务重试失败'
|
||||
} finally {
|
||||
if (type === 'app_blueprint') generatingAppBlueprint.value = false
|
||||
if (type === 'database') generating.value = false
|
||||
if (type === 'business_blueprint') generatingBusinessBlueprint.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveDatabase() {
|
||||
if (!projectId.value) return
|
||||
|
||||
@@ -674,11 +794,16 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
refreshAiQuota()
|
||||
loadModuleOptions()
|
||||
if (projectId.value) {
|
||||
loadProjectDraft(projectId.value)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAiPolling = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -818,6 +943,34 @@ onMounted(() => {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.ai-task-strip {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.ai-task-card {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ai-task-card-header,
|
||||
.ai-quota-card {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.task-error {
|
||||
color: #c2410c;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.panel-header {
|
||||
align-items: stretch;
|
||||
|
||||
Reference in New Issue
Block a user