From 08d5a45c804e750804df8c4b1714d8b53c9e04a6 Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Mon, 18 May 2026 16:45:17 +0800 Subject: [PATCH] x --- src/api/routes.py | 10 +- src/services/llm_service.py | 154 +++++++++++ src/services/processing_service.py | 10 +- static/style.css | 151 ---------- static/vue-app.js | 428 +++++++++++------------------ 5 files changed, 340 insertions(+), 413 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index 4781629..62372ae 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -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") diff --git a/src/services/llm_service.py b/src/services/llm_service.py index 862d972..032d024 100644 --- a/src/services/llm_service.py +++ b/src/services/llm_service.py @@ -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( + "\n" + f"{payload}\n" + "" + ) + 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", {}) diff --git a/src/services/processing_service.py b/src/services/processing_service.py index 6791955..6936078 100644 --- a/src/services/processing_service.py +++ b/src/services/processing_service.py @@ -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. 完成处理 diff --git a/static/style.css b/static/style.css index 3a017ba..97993a9 100644 --- a/static/style.css +++ b/static/style.css @@ -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 { diff --git a/static/vue-app.js b/static/vue-app.js index d66f174..404342d 100644 --- a/static/vue-app.js +++ b/static/vue-app.js @@ -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(/\s*([\s\S]*?)\s*/); + 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); - return { - id: `slider-${index}`, - type: 'slider', - index, - item, - title: `滑块 #${index + 1}`, - tags, - risks - }; - }); - 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 stripEmbeddedSideActionAi = (report) => String(report || '') + .replace(/\s*[\s\S]*?\s*/, '') + .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 { + 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 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, '&') .replace(/ { - 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,73 +2303,86 @@ const ResultView = { -