This commit is contained in:
2026-05-18 16:45:17 +08:00
parent a2ab973ce7
commit 08d5a45c80
5 changed files with 340 additions and 413 deletions
+9 -1
View File
@@ -1119,7 +1119,15 @@ async def process_file_core(
# 9.8 LLM 增强分析
llm_report = None
if analysis_result:
llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json)
side_action_ai = await llm_service.generate_side_action_analysis(
analysis_result, detailed_cavity_json
)
design_report = await llm_service.generate_design_report(
analysis_result, detailed_cavity_json
)
llm_report = llm_service.compose_llm_report(
design_report, side_action_ai
)
# 10. 完成处理
await storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
+154
View File
@@ -64,6 +64,53 @@ _DESIGN_REPORT_USER = """请根据以下模具分析数据生成评审报告:
## 原始设计建议
{recommendations}"""
_SIDE_ACTION_ANALYSIS_SYSTEM = """你是一位资深注塑模具结构工程师。
请根据提供的 STP 分析结果,判断当前产品是否需要倒扣/抽芯机构,并输出标准化结论。
输出要求:
1. 严格输出 JSON,不要输出其他内容
2. 只允许基于已给数据判断,数据不足时必须标记为 manual_review
3. 结论面向工程评审,避免坐标、面索引、底层算法术语堆砌
4. 建议必须标准化、简洁、可执行
JSON 格式:
{
"status": "required|not_required|manual_review",
"confidence": 0.0,
"conclusion": "一句中文结论",
"mechanism_recommendation": "slider|lifter|mixed|none|manual_review",
"summary": "一段 40-80 字中文摘要",
"reasons": ["原因1", "原因2"],
"standard_advice": ["建议1", "建议2"],
"manual_review_items": ["复核项1", "复核项2"]
}"""
_SIDE_ACTION_ANALYSIS_USER = """请分析当前注塑件是否需要倒扣/抽芯机构:
## 产品信息
- 文件:{filename}
- 材料:{material}
- 边界框:{bbox}
## 特征检测
{features}
## 最优方案
{best_scheme}
## 规则分析结果
{side_actions}
## DFM 风险
{dfm_violations}
判断要求:
1. 如果规则结果明确显示无倒扣,可输出 not_required
2. 如果存在外侧倒扣,优先考虑 slider
3. 如果存在内侧倒扣,优先考虑 lifter
4. 如果内外侧倒扣同时存在,可输出 mixed
5. 如果数据不够支撑明确判断,输出 manual_review"""
_PARTING_SYSTEM = """你是一位注塑模具分模专家。
根据产品几何特征和多个候选分模方向的评分数据,推荐最优分模方向。
@@ -134,6 +181,56 @@ class LLMService:
logger.warning("LLM 设计报告生成失败(不影响主流程): %s", e)
return None
async def generate_side_action_analysis(
self,
analysis_result: Dict[str, Any],
detailed_cavity_json: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
"""生成倒扣/抽芯 AI 标准化分析"""
if not self._enabled:
return None
try:
prompt = self._build_side_action_prompt(analysis_result, detailed_cavity_json)
response = await self._chat(
_SIDE_ACTION_ANALYSIS_SYSTEM,
prompt,
min(self._max_tokens, 1200),
expect_json=True,
)
if not response:
return None
result = self._parse_json_response(response)
if result:
logger.info(
"LLM 倒扣/抽芯分析生成成功: status=%s confidence=%s",
result.get("status"),
result.get("confidence"),
)
return result
except Exception as e:
logger.warning("LLM 倒扣/抽芯分析失败(不影响主流程): %s", e)
return None
@staticmethod
def compose_llm_report(
design_report: Optional[str],
side_action_analysis: Optional[Dict[str, Any]],
) -> Optional[str]:
"""将结构化倒扣分析打包进既有 llm_report 字段,避免改动外部协议。"""
sections: List[str] = []
if side_action_analysis:
payload = json.dumps(side_action_analysis, ensure_ascii=False)
sections.append(
"<!--SIDE_ACTION_AI_BEGIN-->\n"
f"{payload}\n"
"<!--SIDE_ACTION_AI_END-->"
)
if design_report and design_report.strip():
sections.append(design_report.strip())
merged = "\n\n".join(sections).strip()
return merged or None
async def recommend_parting_direction(
self,
geometry_data: Dict[str, Any],
@@ -199,6 +296,63 @@ class LLMService:
recommendations=json.dumps(analysis_result.get("design_recommendations", []), ensure_ascii=False, indent=2) or "无",
)
def _build_side_action_prompt(self, analysis_result, detailed_cavity_json) -> str:
features = json.dumps(
analysis_result.get("detected_features", []),
ensure_ascii=False,
indent=2,
)
if len(features) > 2500:
features = features[:2500] + "\n... (已截断)"
best_scheme = {}
if detailed_cavity_json:
candidate_schemes = detailed_cavity_json.get("candidate_schemes", [])
best_scheme_id = detailed_cavity_json.get("best_scheme_id")
if candidate_schemes:
best_scheme = candidate_schemes[0]
if best_scheme_id:
for scheme in candidate_schemes:
if scheme.get("scheme_id") == best_scheme_id:
best_scheme = scheme
break
cavity_data = best_scheme.get("cavity_data", {}) if isinstance(best_scheme, dict) else {}
metadata = cavity_data.get("metadata", {}) if isinstance(cavity_data, dict) else {}
side_actions = (
best_scheme.get("side_actions")
or cavity_data.get("side_actions")
or {}
)
best_scheme_view = {
"scheme_id": best_scheme.get("scheme_id"),
"title": best_scheme.get("title"),
"score": best_scheme.get("score"),
"parting_axis": best_scheme.get("parting", {}).get("axis"),
"mold_structure_type": best_scheme.get("mold_structure_type"),
"undercut_regions_count": len(best_scheme.get("undercut_regions", []) or []),
}
side_actions_view = {
"summary": side_actions.get("summary", {}),
"recommendations": side_actions.get("recommendations", []),
"slider_count": len(side_actions.get("slider_mechanisms", []) or []),
"lifter_count": len(side_actions.get("lifter_mechanisms", []) or []),
}
dfm_violations = best_scheme.get("dfm_violations", []) if isinstance(best_scheme, dict) else []
return _SIDE_ACTION_ANALYSIS_USER.format(
filename=metadata.get("file_name", "unknown.stp"),
material=metadata.get("selected_material", "ABS"),
bbox=json.dumps(
analysis_result.get("geometry_data", {}).get("bounding_box", {}),
ensure_ascii=False,
),
features=features or "无特征检测数据",
best_scheme=json.dumps(best_scheme_view, ensure_ascii=False, indent=2),
side_actions=json.dumps(side_actions_view, ensure_ascii=False, indent=2),
dfm_violations=json.dumps(dfm_violations[:6], ensure_ascii=False, indent=2),
)
def _build_parting_prompt(self, geometry_data, candidate_schemes, material, cavity_count) -> str:
bbox = geometry_data.get("bounding_box", {})
axis_normal_stats = geometry_data.get("axis_normal_stats", {})
+9 -1
View File
@@ -291,7 +291,15 @@ class ProcessingService:
llm_report = None
stage_started = time.perf_counter()
if analysis_result:
llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json)
side_action_ai = await llm_service.generate_side_action_analysis(
analysis_result, detailed_cavity_json
)
design_report = await llm_service.generate_design_report(
analysis_result, detailed_cavity_json
)
llm_report = llm_service.compose_llm_report(
design_report, side_action_ai
)
stage_timings["generate_llm_report"] = round(time.perf_counter() - stage_started, 3)
# 10. 完成处理
-151
View File
@@ -1358,123 +1358,6 @@ optgroup {
background: linear-gradient(180deg, var(--bg-primary) 0%, var(--primary-50) 100%);
}
.mechanism-list {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.side-action-list {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.side-action-row {
width: 100%;
text-align: left;
border: 1px solid var(--border-light);
border-radius: var(--radius-xl);
padding: var(--space-4);
background: var(--bg-primary);
cursor: pointer;
display: flex;
flex-direction: column;
gap: var(--space-3);
transition: all var(--duration-fast) var(--ease-default);
}
.side-action-row:hover {
border-color: var(--primary-200);
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
.side-action-row-main {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.side-action-row-title {
font-size: var(--text-base);
font-weight: var(--font-semibold);
color: var(--text-primary);
}
.chip-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.chip {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
background: var(--bg-tertiary);
border: 1px solid var(--border-light);
font-size: var(--text-xs);
font-weight: var(--font-medium);
color: var(--text-secondary);
}
.chip-risk {
background: #fff7ed;
border-color: #fde68a;
color: #9a3412;
}
.drawer-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.35);
z-index: 2000;
}
.drawer-panel {
position: fixed;
top: 0;
right: 0;
height: 100%;
width: min(520px, 92vw);
background: var(--bg-primary);
border-left: 1px solid var(--border-light);
box-shadow: var(--shadow-lg);
z-index: 2001;
display: flex;
flex-direction: column;
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-5);
border-bottom: 1px solid var(--border-light);
}
.drawer-header h3 {
margin: 0;
font-size: var(--text-lg);
}
.drawer-body {
padding: var(--space-5);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.mechanism-card {
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
padding: var(--space-4);
background: var(--bg-primary);
}
.mechanism-title {
font-size: var(--text-base);
font-weight: var(--font-semibold);
@@ -1482,37 +1365,6 @@ optgroup {
margin-bottom: var(--space-3);
}
.mechanism-subtitle {
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--text-secondary);
margin-bottom: var(--space-2);
}
.mechanism-notes {
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1px solid var(--border-light);
}
.mechanism-actions {
margin-top: auto;
display: flex;
justify-content: flex-end;
}
.mechanism-note-list {
margin: 0;
padding-left: 1.1rem;
color: var(--text-secondary);
font-size: var(--text-sm);
line-height: 1.7;
}
.mechanism-note-list li + li {
margin-top: var(--space-2);
}
.result-priority-grid {
margin-bottom: var(--space-6);
}
@@ -1580,9 +1432,6 @@ optgroup {
.info-list {
grid-template-columns: 1fr;
}
.drawer-panel {
width: 92vw;
}
}
.info-item {
+160 -252
View File
@@ -1425,7 +1425,6 @@ const ResultView = {
error: '',
selectedSchemeId: null,
previewStatus: 'idle',
activeMechanismDetail: null,
camLoading: false,
camError: '',
camPlan: null,
@@ -1516,102 +1515,97 @@ const ResultView = {
|| state.task?.side_actions
|| null;
});
const sideActionSummary = computed(() => selectedSideActions.value?.summary || {});
const sliderMechanisms = computed(() => selectedSideActions.value?.slider_mechanisms || []);
const lifterMechanisms = computed(() => selectedSideActions.value?.lifter_mechanisms || []);
const sideActionRecommendations = computed(() => selectedSideActions.value?.recommendations || []);
const inferSliderDirectionKey = (vector) => {
if (!Array.isArray(vector) || vector.length < 3) return null;
const values = vector.map(v => Number(v) || 0);
const abs = values.map(v => Math.abs(v));
const max = Math.max(...abs);
if (!max) return null;
const idx = abs.findIndex(v => v === max);
const axis = idx === 0 ? 'X' : idx === 1 ? 'Y' : 'Z';
const sign = values[idx] >= 0 ? '+' : '-';
return `${axis}${sign}`;
};
const demoldingDigest = computed(() => {
const sliders = sliderMechanisms.value.length;
const lifters = lifterMechanisms.value.length;
const total = sliders + lifters;
const hasHydraulic = Boolean(sideActionSummary.value?.has_hydraulic);
const complexity = sideActionSummary.value?.complexity || (total >= 6 ? 'very_complex' : total >= 4 ? 'complex' : total >= 2 ? 'moderate' : total === 1 ? 'simple' : 'none');
const directionKeys = new Set(
sliderMechanisms.value
.map(item => inferSliderDirectionKey(item.slide_direction))
.filter(Boolean)
);
const directionLabel = directionKeys.size <= 1 ? '单向' : '多向';
const riskLevel = hasHydraulic || total >= 5 ? '高' : total >= 3 ? '中' : total >= 1 ? '低' : '无';
const actions = (sideActionRecommendations.value || []).slice(0, 2);
return {
sliders,
lifters,
total,
hasHydraulic,
complexity,
directionLabel,
riskLevel,
actions
};
});
const normalizeActuationLabel = (value) => {
const text = String(value || '').toLowerCase();
if (!text) return 'N/A';
if (text.includes('hydraulic')) return '液压';
if (text.includes('mechanical')) return '机械';
if (text.includes('spring')) return '弹簧';
return value;
};
const buildMechanismTags = (type, item = {}, totalCount = 0, hasHydraulic = false) => {
const tags = [];
const risks = [];
if (type === 'slider') {
if (item.slide_stroke != null) tags.push(`行程 ${Number(item.slide_stroke).toFixed(1)}mm`);
if (item.slide_angle != null) tags.push(`角度 ${Number(item.slide_angle).toFixed(1)}°`);
if (item.actuation) tags.push(`驱动 ${normalizeActuationLabel(item.actuation)}`);
if (item.slide_stroke != null && Number(item.slide_stroke) > 60) risks.push('行程偏大');
if (item.slide_angle != null && Number(item.slide_angle) > 25) risks.push('角度偏大');
} else if (type === 'lifter') {
if (item.lifter_angle != null) tags.push(`角度 ${Number(item.lifter_angle).toFixed(1)}°`);
if (item.lifter_stroke != null) tags.push(`行程 ${Number(item.lifter_stroke).toFixed(1)}mm`);
if (item.return_mechanism) tags.push(`回位 ${item.return_mechanism}`);
if (item.lifter_stroke != null && Number(item.lifter_stroke) > 60) risks.push('行程偏大');
if (item.lifter_angle != null && Number(item.lifter_angle) > 20) risks.push('角度偏大');
const parseEmbeddedSideActionAi = (report) => {
const source = String(report || '');
const match = source.match(/<!--SIDE_ACTION_AI_BEGIN-->\s*([\s\S]*?)\s*<!--SIDE_ACTION_AI_END-->/);
if (!match) return null;
try {
return JSON.parse(match[1]);
} catch (error) {
console.warn('AI 倒扣分析解析失败:', error);
return null;
}
if (totalCount >= 5) risks.push('数量偏多');
if (hasHydraulic) risks.push('需液压评审');
return { tags, risks: risks.slice(0, 3) };
};
const mechanismRows = computed(() => {
const totalCount = demoldingDigest.value.total;
const hasHydraulic = demoldingDigest.value.hasHydraulic;
const sliders = sliderMechanisms.value.map((item, index) => {
const { tags, risks } = buildMechanismTags('slider', item, totalCount, hasHydraulic);
const stripEmbeddedSideActionAi = (report) => String(report || '')
.replace(/<!--SIDE_ACTION_AI_BEGIN-->\s*[\s\S]*?\s*<!--SIDE_ACTION_AI_END-->/, '')
.trim();
const normalizeSideActionAiAdvice = (raw, source = 'ai') => {
if (!raw || typeof raw !== 'object') return null;
const status = ['required', 'not_required', 'manual_review'].includes(raw.status) ? raw.status : 'manual_review';
const mechanism = ['slider', 'lifter', 'mixed', 'none', 'manual_review'].includes(raw.mechanism_recommendation)
? raw.mechanism_recommendation
: 'manual_review';
const confidence = Number(raw.confidence);
const statusLabelMap = {
required: '需要倒扣/抽芯',
not_required: '无需倒扣/抽芯',
manual_review: '需人工确认'
};
const mechanismLabelMap = {
slider: '优先滑块',
lifter: '优先斜顶',
mixed: '滑块 + 斜顶',
none: '无需侧向机构',
manual_review: '人工评审'
};
return {
id: `slider-${index}`,
type: 'slider',
index,
item,
title: `滑块 #${index + 1}`,
tags,
risks
source,
status,
statusLabel: statusLabelMap[status],
mechanismRecommendation: mechanism,
mechanismLabel: mechanismLabelMap[mechanism],
confidence: Number.isFinite(confidence) ? Math.max(0, Math.min(confidence, 1)) : null,
conclusion: raw.conclusion || statusLabelMap[status],
summary: raw.summary || '',
reasons: Array.isArray(raw.reasons) ? raw.reasons.filter(Boolean).slice(0, 5) : [],
standardAdvice: Array.isArray(raw.standard_advice) ? raw.standard_advice.filter(Boolean).slice(0, 5) : [],
manualReviewItems: Array.isArray(raw.manual_review_items) ? raw.manual_review_items.filter(Boolean).slice(0, 4) : []
};
});
const lifters = lifterMechanisms.value.map((item, index) => {
const { tags, risks } = buildMechanismTags('lifter', item, totalCount, hasHydraulic);
return {
id: `lifter-${index}`,
type: 'lifter',
index,
item,
title: `斜顶 #${index + 1}`,
tags,
risks
};
});
return [...sliders, ...lifters];
const fallbackSideActionAiAdvice = computed(() => {
const sideActions = selectedSideActions.value;
if (!sideActions || typeof sideActions !== 'object') return null;
const summary = sideActions.summary || {};
const sliderCount = (sideActions.slider_mechanisms || []).length;
const lifterCount = (sideActions.lifter_mechanisms || []).length;
const totalCount = sliderCount + lifterCount;
const mechanismRecommendation = sliderCount && lifterCount
? 'mixed'
: sliderCount
? 'slider'
: lifterCount
? 'lifter'
: 'none';
const status = totalCount > 0
? 'required'
: summary.total_undercut_faces === 0
? 'not_required'
: 'manual_review';
return normalizeSideActionAiAdvice({
status,
confidence: 0.55,
conclusion: status === 'required'
? '规则分析判断当前产品需要倒扣/抽芯机构'
: status === 'not_required'
? '规则分析未发现必须采用倒扣/抽芯机构的特征'
: '当前规则结果不足以完成稳定判断,建议人工复核',
mechanism_recommendation: mechanismRecommendation === 'none' ? 'none' : mechanismRecommendation,
summary: status === 'required'
? `检测到 ${sliderCount} 个滑块需求、${lifterCount} 个斜顶需求,建议先按标准机构路线做结构评审。`
: status === 'not_required'
? '当前方案未发现明显倒扣特征,可优先按常规模具结构推进。'
: '现有数据可用于初筛,但不足以替代工程师对复杂倒扣的最终确认。',
reasons: [
totalCount > 0 ? `规则分析识别出 ${totalCount} 处侧向机构需求` : '规则分析未识别出明确侧向机构需求',
summary.complexity ? `当前复杂度判定为 ${summary.complexity}` : '当前复杂度信息不足',
summary.has_hydraulic ? '规则分析提示可能涉及液压抽芯' : '未发现液压抽芯硬性提示'
].filter(Boolean),
standard_advice: (sideActions.recommendations || []).slice(0, 4),
manual_review_items: [
'结合 3D 预览复核倒扣是否可通过改产品取消',
'确认侧向机构与顶出、冷却、分型面是否存在干涉'
]
}, 'rules');
});
const dfmLevelSummary = computed(() => {
const base = { critical: 0, high: 0, medium: 0, low: 0 };
@@ -1625,7 +1619,15 @@ const ResultView = {
});
return base;
});
const llmReportHtml = computed(() => renderMarkdownToHtml(state.task?.llm_report || ''));
const sideActionAiAdvice = computed(() => {
return normalizeSideActionAiAdvice(
parseEmbeddedSideActionAi(state.task?.llm_report || ''),
'ai'
) || fallbackSideActionAiAdvice.value;
});
const cleanedLlmReport = computed(() => stripEmbeddedSideActionAi(state.task?.llm_report || ''));
const hasVisibleLlmReport = computed(() => Boolean(cleanedLlmReport.value));
const llmReportHtml = computed(() => renderMarkdownToHtml(cleanedLlmReport.value));
const stageTimingEntries = computed(() => {
const timings = state.task?.stage_timings || {};
return Object.entries(timings)
@@ -1656,7 +1658,7 @@ const ResultView = {
{ id: 'candidate-schemes', label: '方案' },
{ id: 'preview-3d', label: '预览' },
{ id: 'engineering-summary', label: '工程摘要' },
{ id: 'demolding-risk', label: '脱模风险' },
{ id: 'ai-side-action', label: 'AI倒扣分析' },
{ id: 'dfm-check', label: 'DFM' },
{ id: 'export-cam', label: '导出/CAM' },
{ id: 'llm-report', label: '设计报告' }
@@ -1718,20 +1720,6 @@ const ResultView = {
return stageNameMap[name] || name;
};
const formatTiming = (value) => `${Number(value || 0).toFixed(3)} s`;
const formatVector = (vector) => {
if (!Array.isArray(vector)) return 'N/A';
return `[${vector.map(v => Number(v).toFixed(2)).join(', ')}]`;
};
const formatMechanismSize = (size) => {
if (!size || typeof size !== 'object') return 'N/A';
const keys = Object.keys(size);
return keys.map(key => `${key}:${Number(size[key]).toFixed(1)} mm`).join(' / ');
};
const formatMechanismType = (type) => {
if (type === 'slider') return '滑块';
if (type === 'lifter') return '斜顶';
return type || '机构';
};
const escapeHtml = (value) => String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
@@ -1821,16 +1809,6 @@ const ResultView = {
closeCode();
return html.filter(Boolean).join('\n');
};
const openMechanismDetail = (type, index, item) => {
state.activeMechanismDetail = {
type,
index,
item,
};
};
const closeMechanismDetail = () => {
state.activeMechanismDetail = null;
};
const exportCAD = async (format) => {
try {
@@ -1925,13 +1903,8 @@ const ResultView = {
selectedHtmlFile,
selectedDfmViolations,
selectedInjectionSystem,
selectedSideActions,
sideActionSummary,
sliderMechanisms,
lifterMechanisms,
sideActionRecommendations,
demoldingDigest,
mechanismRows,
sideActionAiAdvice,
hasVisibleLlmReport,
dfmLevelSummary,
llmReportHtml,
stageTimingEntries,
@@ -1940,11 +1913,6 @@ const ResultView = {
getUndercutCount,
formatStageName,
formatTiming,
formatVector,
formatMechanismSize,
formatMechanismType,
openMechanismDetail,
closeMechanismDetail,
primaryRecommendation,
sortedRecommendations,
selectedDfmCount,
@@ -2335,72 +2303,85 @@ const ResultView = {
</div>
</div>
<div id="demolding-risk" v-if="selectedSideActions && demoldingDigest.total" class="viewer-section">
<h3>脱模风险摘要</h3>
<div class="result-grid">
<div class="result-card">
<h4>结论</h4>
<div id="ai-side-action" class="viewer-section">
<div class="summary-header">
<h3>AI 倒扣与抽芯分析</h3>
<span
v-if="sideActionAiAdvice"
:class="[
'badge',
sideActionAiAdvice.status === 'required'
? 'badge-error'
: sideActionAiAdvice.status === 'not_required'
? 'badge-success'
: 'badge-warning'
]"
>
{{ sideActionAiAdvice.statusLabel }}
</span>
<span v-else class="badge badge-warning">未生成</span>
</div>
<div v-if="sideActionAiAdvice" class="result-grid">
<div class="result-card result-card-highlight">
<h4>AI 结论</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">侧向机构</span>
<span class="info-value">滑块 {{ demoldingDigest.sliders }} / 斜顶 {{ demoldingDigest.lifters }}</span>
<span class="info-label">判断结果</span>
<span class="info-value">{{ sideActionAiAdvice.conclusion }}</span>
</div>
<div class="info-item">
<span class="info-label">抽芯方向</span>
<span class="info-value">{{ demoldingDigest.directionLabel }}</span>
<span class="info-label">推荐方案</span>
<span class="info-value">{{ sideActionAiAdvice.mechanismLabel }}</span>
</div>
<div class="info-item" v-if="sideActionAiAdvice.confidence != null">
<span class="info-label">置信度</span>
<span class="info-value">{{ Math.round(sideActionAiAdvice.confidence * 100) }}%</span>
</div>
<div class="info-item">
<span class="info-label">复杂度</span>
<span class="info-value">{{ demoldingDigest.complexity }}</span>
</div>
<div class="info-item">
<span class="info-label">液压抽芯</span>
<span class="info-value">{{ demoldingDigest.hasHydraulic ? '需要' : '暂不需要' }}</span>
<span class="info-label">分析来源</span>
<span class="info-value">{{ sideActionAiAdvice.source === 'ai' ? 'AI 模型' : '规则回退' }}</span>
</div>
</div>
<div class="inline-note">建议先用上方 3D 预览确认倒扣区域的空间位置,再查看侧向机构清单。</div>
<div class="inline-note">{{ sideActionAiAdvice.summary || '当前任务未返回更多摘要。' }}</div>
</div>
<div class="result-card">
<h4>风险与下一步</h4>
<div class="recommendations-list">
<div class="recommendation-item high" v-if="demoldingDigest.riskLevel === '高'">
<div class="recommendation-text">侧向机构数量或液压抽芯提示较多,建议优先做结构优化与干涉评审,再进入制造准备。</div>
</div>
<div class="recommendation-item medium" v-else-if="demoldingDigest.riskLevel === '中'">
<div class="recommendation-text">侧向机构数量适中,建议结合 3D 预览复核抽芯方向与行程空间。</div>
</div>
<div class="recommendation-item low" v-else>
<div class="recommendation-text">侧向机构较少,可进入 DFM 复核与导出准备。</div>
<h4>判断依据</h4>
<div class="recommendations-list" v-if="sideActionAiAdvice.reasons.length">
<div class="recommendation-item medium" v-for="(reason, idx) in sideActionAiAdvice.reasons" :key="'ai-reason-' + idx">
<div class="recommendation-text">{{ reason }}</div>
</div>
</div>
<div class="recommendations-list" v-if="demoldingDigest.actions.length">
<div class="recommendation-item medium" v-for="(rec, idx) in demoldingDigest.actions" :key="'demolding-action-' + idx">
<div class="recommendation-text">{{ rec }}</div>
<div class="inline-note" v-else>当前任务未返回明确的判断依据。</div>
</div>
<div class="result-card">
<h4>标准化建议</h4>
<div class="recommendations-list" v-if="sideActionAiAdvice.standardAdvice.length">
<div class="recommendation-item low" v-for="(advice, idx) in sideActionAiAdvice.standardAdvice" :key="'ai-advice-' + idx">
<div class="recommendation-text">{{ advice }}</div>
</div>
</div>
<div class="inline-note" v-else>当前方案未返回额外侧向机构建议。</div>
<div class="inline-note" v-else>当前任务未返回额外标准建议。</div>
</div>
<div class="result-card">
<h4>人工复核项</h4>
<div class="recommendations-list" v-if="sideActionAiAdvice.manualReviewItems.length">
<div class="recommendation-item medium" v-for="(item, idx) in sideActionAiAdvice.manualReviewItems" :key="'ai-review-' + idx">
<div class="recommendation-text">{{ item }}</div>
</div>
</div>
<div class="inline-note" v-else>当前任务无需额外人工复核项。</div>
</div>
</div>
<details id="side-action-list" v-if="mechanismRows.length" class="viewer-section diagnostic-panel compact-panel">
<summary>侧向机构清单(滑块 {{ demoldingDigest.sliders }} / 斜顶 {{ demoldingDigest.lifters }})</summary>
<div class="diagnostic-panel-body">
<div class="side-action-list">
<button class="side-action-row" v-for="row in mechanismRows" :key="row.id" @click="openMechanismDetail(row.type, row.index, row.item)">
<div class="side-action-row-main">
<div class="side-action-row-title">{{ row.title }}</div>
<div class="chip-row">
<span class="chip" v-for="(tag, tagIdx) in row.tags" :key="row.id + '-tag-' + tagIdx">{{ tag }}</span>
<div v-else class="inline-alert inline-alert-warning">
<div class="inline-alert-title">AI 结论未生成</div>
<div class="inline-alert-message">当前任务未生成倒扣与抽芯 AI 分析,请检查模型配置或重新运行分析。</div>
</div>
</div>
<div class="chip-row chip-row-risk" v-if="row.risks.length">
<span class="chip chip-risk" v-for="(tag, tagIdx) in row.risks" :key="row.id + '-risk-' + tagIdx">{{ tag }}</span>
</div>
</button>
</div>
</div>
</details>
<div id="dfm-check" class="viewer-section">
<h3>DFM 检查</h3>
@@ -2433,7 +2414,7 @@ const ResultView = {
<div class="recommendation-text">当前方案存在较高工艺风险,建议优先调整壁厚、倒扣或分型方式后再进入制造准备。</div>
</div>
<div class="recommendation-item medium" v-else>
<div class="recommendation-text">当前方案未发现显著高风险项,可结合 3D 预览与脱模风险摘要继续做工程评审。</div>
<div class="recommendation-text">当前方案未发现显著高风险项,可结合 3D 预览与 AI 倒扣分析继续做工程评审。</div>
</div>
</div>
</div>
@@ -2449,7 +2430,7 @@ const ResultView = {
<span class="priority-badge" :class="(item.level || 'medium').toLowerCase()">{{ getPriorityText((item.level || 'medium').toLowerCase()) }}</span>
</div>
<div class="dfm-card-message">{{ item.message || 'N/A' }}</div>
<div class="dfm-card-hint">建议结合上方 3D 预览与侧向机构清单定位具体风险区域。</div>
<div class="dfm-card-hint">建议结合上方 3D 预览与 AI 倒扣分析定位具体风险区域。</div>
</div>
</div>
</div>
@@ -2567,85 +2548,12 @@ const ResultView = {
</template>
</div>
<div id="llm-report" class="viewer-section" v-if="state.task.llm_report">
<div id="llm-report" class="viewer-section" v-if="hasVisibleLlmReport">
<h3>LLM 设计报告</h3>
<div class="result-card">
<div class="markdown-preview" v-html="llmReportHtml"></div>
</div>
</div>
<div v-if="state.activeMechanismDetail" class="drawer-overlay" @click.self="closeMechanismDetail">
<div class="drawer-panel">
<div class="drawer-header">
<h3>{{ formatMechanismType(state.activeMechanismDetail.type) }} {{ state.activeMechanismDetail.index + 1 }}</h3>
<button class="modal-close" @click="closeMechanismDetail">&times;</button>
</div>
<div class="drawer-body">
<div class="result-card">
<div class="info-list">
<div class="info-item">
<span class="info-label">机构类型</span>
<span class="info-value">{{ formatMechanismType(state.activeMechanismDetail.type) }}</span>
</div>
<div class="info-item">
<span class="info-label">机构位置</span>
<span class="info-value">{{ formatVector(state.activeMechanismDetail.item.location) }}</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'slider'">
<span class="info-label">抽芯方向</span>
<span class="info-value">{{ formatVector(state.activeMechanismDetail.item.slide_direction) }}</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'slider'">
<span class="info-label">抽芯行程</span>
<span class="info-value">{{ state.activeMechanismDetail.item.slide_stroke || 'N/A' }} mm</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'slider'">
<span class="info-label">抽芯角度</span>
<span class="info-value">{{ state.activeMechanismDetail.item.slide_angle || 'N/A' }}°</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'slider'">
<span class="info-label">驱动方式</span>
<span class="info-value">{{ state.activeMechanismDetail.item.actuation || 'N/A' }}</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'slider'">
<span class="info-label">导滑方式</span>
<span class="info-value">{{ state.activeMechanismDetail.item.guide_type || 'N/A' }}</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'slider'">
<span class="info-label">锁紧方式</span>
<span class="info-value">{{ state.activeMechanismDetail.item.locking_mechanism || 'N/A' }}</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'lifter'">
<span class="info-label">斜顶角度</span>
<span class="info-value">{{ state.activeMechanismDetail.item.lifter_angle || 'N/A' }}°</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'lifter'">
<span class="info-label">斜顶行程</span>
<span class="info-value">{{ state.activeMechanismDetail.item.lifter_stroke || 'N/A' }} mm</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'lifter'">
<span class="info-label">导向方式</span>
<span class="info-value">{{ state.activeMechanismDetail.item.guide_type || 'N/A' }}</span>
</div>
<div class="info-item" v-if="state.activeMechanismDetail.type === 'lifter'">
<span class="info-label">回位方式</span>
<span class="info-value">{{ state.activeMechanismDetail.item.return_mechanism || 'N/A' }}</span>
</div>
<div class="info-item">
<span class="info-label">机构尺寸</span>
<span class="info-value">{{ formatMechanismSize(state.activeMechanismDetail.item.block_size) }}</span>
</div>
</div>
</div>
<div class="result-card" v-if="state.activeMechanismDetail.item.manufacturing_notes?.length">
<h4>注意事项</h4>
<ul class="mechanism-note-list">
<li v-for="(note, noteIdx) in state.activeMechanismDetail.item.manufacturing_notes" :key="'active-note-' + noteIdx">{{ note }}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
`