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:
@@ -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
|
||||
}
|
||||
+31
@@ -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)
|
||||
Reference in New Issue
Block a user