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
+23 -1
View File
@@ -1,13 +1,14 @@
from fastapi import Depends, FastAPI, HTTPException
from app.config import get_settings
from app.models import LeaveDraftSuggestionRequest, LeaveDraftSuggestionResponse, LeaveProgressAnswerRequest, LeaveProgressAnswerResponse
from app.models import LeaveDraftSuggestionRequest, LeaveDraftSuggestionResponse, LeaveProgressAnswerRequest, LeaveProgressAnswerResponse, DesignerSuggestionRequest, DesignerSuggestionResponse
from app.qwen import (
QwenConfigurationError,
QwenSuggestionGateway,
QwenUpstreamError,
SuggestionGateway,
ProgressGateway,
DesignerGateway,
)
app = FastAPI(title="AIOA AI Service", version="0.1.0")
@@ -21,6 +22,10 @@ def get_progress_gateway() -> ProgressGateway:
return QwenSuggestionGateway(get_settings())
def get_designer_gateway() -> DesignerGateway:
return QwenSuggestionGateway(get_settings())
@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "UP"}
@@ -55,3 +60,20 @@ async def answer_leave_progress(
except QwenUpstreamError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return LeaveProgressAnswerResponse(answer=answer, model=get_settings().qwen_model)
@app.post("/v1/designer/suggest", response_model=DesignerSuggestionResponse)
async def suggest_design(
request: DesignerSuggestionRequest,
gateway: DesignerGateway = Depends(get_designer_gateway),
) -> DesignerSuggestionResponse:
try:
suggestion = await gateway.suggest_design(request)
except QwenConfigurationError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
except QwenUpstreamError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return DesignerSuggestionResponse(
suggestion=suggestion,
model=get_settings().qwen_model,
)
+78
View File
@@ -65,3 +65,81 @@ class LeaveProgressAnswerRequest(BaseModel):
class LeaveProgressAnswerResponse(BaseModel):
answer: str = Field(min_length=1, max_length=2000)
model: str
# ── AI 对话式设计助手(多阶段交互) ──────────────────────────────────
class DesignerStage(StrEnum):
UNDERSTANDING = "UNDERSTANDING"
CLARIFYING = "CLARIFYING"
GENERATING = "GENERATING"
VALIDATING = "VALIDATING"
CONFIRMING = "CONFIRMING"
class DesignerFieldType(StrEnum):
TEXT = "text"
TEXT_AREA = "textArea"
SELECT = "select"
DATE_TIME = "dateTime"
NUMBER = "number"
class DesignerFormField(BaseModel):
key: str = Field(min_length=1, max_length=64)
label: str = Field(min_length=1, max_length=100)
control: DesignerFieldType
required: bool = False
placeholder: str | None = Field(default=None, max_length=200)
options: list[str] | None = Field(default=None, max_length=20)
helperText: str | None = Field(default=None, max_length=200)
class DesignerProcessStep(BaseModel):
name: str = Field(min_length=1, max_length=100)
assigneeVariable: str = Field(min_length=1, max_length=64)
class DesignerProcessSuggestion(BaseModel):
mode: str = Field(default="SERIAL", max_length=20)
steps: list[DesignerProcessStep] = Field(default_factory=list, max_length=10)
conditionThresholdDays: float | None = None
class SchemaValidationIssue(BaseModel):
field: str = Field(min_length=1, max_length=100)
issue: str = Field(min_length=1, max_length=300)
severity: str = Field(default="WARNING", max_length=20)
class DesignerSuggestion(BaseModel):
stage: DesignerStage = DesignerStage.UNDERSTANDING
formTitle: str | None = Field(default=None, max_length=100)
formKey: str | None = Field(default=None, max_length=64)
fields: list[DesignerFormField] = Field(default_factory=list, max_length=30)
process: DesignerProcessSuggestion | None = None
summary: str = Field(default="", max_length=500)
understanding: str = Field(default="", max_length=800)
assumptions: list[str] = Field(default_factory=list, max_length=10)
needsClarification: list[str] = Field(default_factory=list, max_length=10)
validationIssues: list[SchemaValidationIssue] = Field(default_factory=list, max_length=20)
schemaReady: bool = False
class ChatTurn(BaseModel):
role: str = Field(min_length=1, max_length=20)
content: str = Field(min_length=1, max_length=4000)
class DesignerSuggestionRequest(BaseModel):
message: str = Field(min_length=1, max_length=2000)
history: list[ChatTurn] = Field(default_factory=list, max_length=20)
currentSchema: str | None = Field(default=None, max_length=8000)
timezone: str = Field(default="Asia/Shanghai", min_length=1, max_length=64)
class DesignerSuggestionResponse(BaseModel):
suggestion: DesignerSuggestion
model: str
requiresUserConfirmation: bool = True
+98 -1
View File
@@ -5,7 +5,14 @@ from typing import Protocol
import httpx
from app.config import Settings
from app.models import LeaveDraftSuggestion, LeaveDraftSuggestionRequest, LeaveProgressAnswerRequest
from app.models import (
LeaveDraftSuggestion,
LeaveDraftSuggestionRequest,
LeaveProgressAnswerRequest,
DesignerSuggestionRequest,
DesignerSuggestion,
ChatTurn,
)
class SuggestionGateway(Protocol):
@@ -16,6 +23,10 @@ class ProgressGateway(Protocol):
async def answer_progress(self, request: LeaveProgressAnswerRequest) -> str: ...
class DesignerGateway(Protocol):
async def suggest_design(self, request: DesignerSuggestionRequest) -> DesignerSuggestion: ...
class QwenSuggestionGateway:
def __init__(self, settings: Settings) -> None:
self.settings = settings
@@ -82,6 +93,42 @@ class QwenSuggestionGateway:
except (KeyError, IndexError, TypeError, ValueError, json.JSONDecodeError) as exc:
raise QwenUpstreamError("Qwen returned an invalid progress answer") from exc
async def suggest_design(self, request: DesignerSuggestionRequest) -> DesignerSuggestion:
if not self.settings.qwen_api_key:
raise QwenConfigurationError("QWEN_API_KEY is not configured")
user_content = json.dumps(
{
"message": request.message,
"history": [t.model_dump() for t in request.history],
"currentSchema": request.currentSchema,
"timezone": request.timezone,
},
ensure_ascii=False,
)
messages: list[dict[str, str]] = [{"role": "system", "content": DESIGNER_SYSTEM_PROMPT}]
for turn in request.history:
messages.append({"role": turn.role, "content": turn.content})
messages.append({"role": "user", "content": user_content})
payload = {
"model": self.settings.qwen_model,
"temperature": 0.2,
"response_format": {"type": "json_object"},
"messages": messages,
}
async with httpx.AsyncClient(timeout=self.settings.request_timeout_seconds) as client:
response = await client.post(
f"{self.settings.qwen_base_url}/chat/completions",
headers={"Authorization": f"Bearer {self.settings.qwen_api_key}"},
json=payload,
)
if response.status_code >= 400:
raise QwenUpstreamError(f"Qwen returned HTTP {response.status_code}")
try:
content = response.json()["choices"][0]["message"]["content"]
return DesignerSuggestion.model_validate_json(content)
except (KeyError, IndexError, TypeError, ValueError) as exc:
raise QwenUpstreamError("Qwen returned an invalid designer response") from exc
SYSTEM_PROMPT = """
你是企业 OA 请假表单解析器。只把用户自然语言转换为 JSON 建议值,不执行任何业务动作。
@@ -97,6 +144,56 @@ PROGRESS_SYSTEM_PROMPT = """
不得猜测审批人、原因、流程变量或预计完成时间,不得给出批准、驳回、撤回、提交等写操作指令,不得使用 Markdown。
""".strip()
DESIGNER_SYSTEM_PROMPT = """
你是企业 OA 表单与流程设计助手,通过多阶段对话帮助用户生成正确的表单和审批流程 Schema。
## 工作流程(5 个阶段)
1. UNDERSTANDING(理解):分析用户需求,复述你的理解,判断信息是否充分。
- 如果信息不足需要澄清 → 进入 CLARIFYING
- 如果信息充分 → 直接进入 GENERATING
2. CLARIFYING(澄清):向用户提出具体问题,每次最多 3 个问题。
- 用户回答后重新评估,信息充分则进入 GENERATING
3. GENERATING(生成):根据理解生成完整的表单字段和审批流程 Schema。
- 生成后自动进入 VALIDATING
4. VALIDATING(校验):自检生成的 Schema 是否正确完整。
- 检查项:字段 key 唯一、必填字段合理、控件类型匹配、审批步骤 ≥ 2、assigneeVariable 不重复(PARALLEL/CONDITIONAL)、select 有 options
- 有问题则修复后重新校验,无问题则进入 CONFIRMING
5. CONFIRMING(确认):展示最终 Schema 摘要,请用户确认或调整。
- 用户确认 → schemaReady = true
- 用户要求调整 → 回到 GENERATING
## 输出格式
输出必须是 JSON 对象,包含以下字段:
- stage:当前阶段(UNDERSTANDING/CLARIFYING/GENERATING/VALIDATING/CONFIRMING
- understanding:你对用户需求的理解复述(中文)
- formTitle:表单中文标题
- formKey:英文 kebab-case 标识符
- fields:数组,每项含 key(英文 camelCase)、label(中文)、controltext/textArea/select/dateTime/number)、required、placeholder、options(仅 select)、helperText
- process:对象,含 modeSERIAL/PARALLEL/CONDITIONAL)、steps(数组,每项含 name 和 assigneeVariable)、conditionThresholdDays(仅 CONDITIONAL
- summary:一句话总结
- assumptions:你做出的假设
- needsClarification:需要用户回答的问题(CLARIFYING 阶段使用)
- validationIssues:校验发现的问题数组,每项含 field、issue、severityERROR/WARNING
- schemaReadySchema 是否已确认可用(仅 CONFIRMING 阶段用户确认后为 true
## 约束
- assigneeVariable 只能是:approverId(部门主管)、oaAdministratorIdOA 管理员)、hrReviewerIdHR 复核人)
- 字段 key 用英文 camelCase 且唯一,label 用中文
- 审批流程至少 2 个步骤
- PARALLEL 和 CONDITIONAL 模式下 assigneeVariable 不可重复
- CONDITIONAL 模式需设置 conditionThresholdDays
- 不得输出权限、租户、隐藏字段等敏感信息
- 不得使用 Markdown
- 如果用户提供了 currentSchema,说明用户已在图形界面修改过,你需要基于当前 Schema 进行调整而非重新生成
""".strip()
class QwenConfigurationError(RuntimeError):
pass