feat: LLM多阶段交互式Schema生成 + UI布局优化

- AI Service: 多阶段对话模型(理解→澄清→生成→校验→确认),支持对话历史和当前Schema上下文
- Backend: 适配多阶段AI模型,Gateway/Service/Controller传递history和currentSchema
- Frontend: AiDesignerChat组件重写,支持阶段徽章、快速回复、Schema校验展示
- Frontend: 表单/流程设计器布局优化,AI助手和Schema预览作为独立列
- Frontend: 流程设计器元数据和模式选择器合并为一行
- Keycloak: 自定义登录主题(中文化)
- 修复CSS媒体查询括号平衡问题
This commit is contained in:
selfrelease
2026-07-19 09:36:42 +08:00
parent a2238bc853
commit 57c8a26147
19 changed files with 853 additions and 14 deletions
@@ -0,0 +1,43 @@
package com.all8ai.aioa.ai.api
import com.all8ai.aioa.ai.application.AiDesignerService
import com.all8ai.aioa.ai.domain.ChatTurn
import com.all8ai.aioa.ai.domain.DesignerSuggestionResult
import com.all8ai.aioa.identity.application.CurrentUserService
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/v1/ai/designer-suggestions")
class AiDesignerController(
private val currentUserService: CurrentUserService,
private val service: AiDesignerService,
) {
@PostMapping
fun suggest(
@AuthenticationPrincipal jwt: Jwt,
@Valid @RequestBody request: AiDesignerSuggestionRequest,
): DesignerSuggestionResult = service.suggest(
currentUserService.get(jwt.subject, jwt.getClaimAsString("tenant_id")),
request.message,
request.history?.map { ChatTurn(it.role, it.content) } ?: emptyList(),
request.currentSchema,
request.timezone,
)
}
data class AiDesignerSuggestionRequest(
@field:NotBlank @field:Size(max = 2000) val message: String,
val history: List<ChatTurnDto>? = null,
@field:Size(max = 8000) val currentSchema: String? = null,
@field:NotBlank @field:Size(max = 64) val timezone: String = "Asia/Shanghai",
)
data class ChatTurnDto(val role: String, val content: String)
@@ -0,0 +1,47 @@
package com.all8ai.aioa.ai.application
import com.all8ai.aioa.ai.domain.AiDesignerGateway
import com.all8ai.aioa.ai.domain.ChatTurn
import com.all8ai.aioa.ai.domain.DesignerSuggestionResult
import com.all8ai.aioa.audit.application.AuditService
import com.all8ai.aioa.identity.domain.CurrentUser
import com.all8ai.aioa.shared.web.ApiException
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import com.all8ai.aioa.shared.security.ToolPermission
import com.all8ai.aioa.shared.security.requirePermission
@Service
class AiDesignerService(
private val gateway: AiDesignerGateway,
private val auditService: AuditService,
) {
fun suggest(actor: CurrentUser, message: String, history: List<ChatTurn>, currentSchema: String?, timezone: String): DesignerSuggestionResult {
actor.requirePermission(ToolPermission.PROCESS_CONFIGURATION_MANAGE_TENANT)
val normalized = message.trim()
if (normalized.isEmpty() || normalized.length > 2000) {
throw ApiException(HttpStatus.BAD_REQUEST, "AI_PROMPT_INVALID", "描述长度必须为 1 到 2000 个字符")
}
if (timezone.isBlank() || timezone.length > 64) {
throw ApiException(HttpStatus.BAD_REQUEST, "TIMEZONE_INVALID", "时区无效")
}
val result = gateway.suggest(normalized, history, currentSchema?.takeIf { it.isNotBlank() }, timezone)
if (!result.requiresUserConfirmation) {
throw ApiException(HttpStatus.BAD_GATEWAY, "AI_CONFIRMATION_REQUIRED", "AI 建议必须要求用户确认")
}
auditService.recordSuccess(
actor,
"AI_DESIGNER_SUGGESTED",
"AI_SUGGESTION",
"designer",
null,
mapOf(
"model" to result.model,
"promptLength" to normalized.length,
"fieldCount" to result.suggestion.fields.size,
"hasProcess" to (result.suggestion.process != null),
),
)
return result
}
}
@@ -0,0 +1,57 @@
package com.all8ai.aioa.ai.domain
data class DesignerFormField(
val key: String,
val label: String,
val control: String,
val required: Boolean = false,
val placeholder: String? = null,
val options: List<String>? = null,
val helperText: String? = null,
)
data class DesignerProcessStep(
val name: String,
val assigneeVariable: String,
)
data class DesignerProcessSuggestion(
val mode: String = "SERIAL",
val steps: List<DesignerProcessStep> = emptyList(),
val conditionThresholdDays: Double? = null,
)
data class SchemaValidationIssue(
val field: String,
val issue: String,
val severity: String = "WARNING",
)
data class DesignerSuggestion(
val stage: String = "UNDERSTANDING",
val formTitle: String? = null,
val formKey: String? = null,
val fields: List<DesignerFormField> = emptyList(),
val process: DesignerProcessSuggestion? = null,
val summary: String = "",
val understanding: String = "",
val assumptions: List<String> = emptyList(),
val needsClarification: List<String> = emptyList(),
val validationIssues: List<SchemaValidationIssue> = emptyList(),
val schemaReady: Boolean = false,
)
data class DesignerSuggestionResult(
val suggestion: DesignerSuggestion,
val model: String,
val requiresUserConfirmation: Boolean = true,
)
data class ChatTurn(
val role: String,
val content: String,
)
fun interface AiDesignerGateway {
fun suggest(message: String, history: List<ChatTurn>, currentSchema: String?, timezone: String): DesignerSuggestionResult
}
@@ -0,0 +1,31 @@
package com.all8ai.aioa.ai.infrastructure
import com.all8ai.aioa.ai.domain.AiDesignerGateway
import com.all8ai.aioa.ai.domain.ChatTurn
import com.all8ai.aioa.ai.domain.DesignerSuggestionResult
import com.all8ai.aioa.shared.web.ApiException
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient
import org.springframework.web.client.RestClientException
@Component
class HttpAiDesignerGateway(@Value("\${aioa.ai-service.url}") aiServiceUrl: String) : AiDesignerGateway {
private val client = RestClient.builder().baseUrl(aiServiceUrl).build()
override fun suggest(message: String, history: List<ChatTurn>, currentSchema: String?, timezone: String): DesignerSuggestionResult = try {
client.post()
.uri("/v1/designer/suggest")
.body(DesignerRequest(message, history, currentSchema, timezone))
.retrieve()
.body(DesignerSuggestionResult::class.java)
?: throw ApiException(HttpStatus.BAD_GATEWAY, "AI_RESPONSE_EMPTY", "AI 服务未返回设计建议")
} catch (e: ApiException) {
throw e
} catch (_: RestClientException) {
throw ApiException(HttpStatus.BAD_GATEWAY, "AI_SERVICE_UNAVAILABLE", "AI 服务暂时不可用")
}
}
private data class DesignerRequest(val message: String, val history: List<ChatTurn>, val currentSchema: String?, val timezone: String)