# ============================================================ # 行业工艺路线结构化模板库(moduleId: knowledge-routing-templates, 可重生 ✅) # machining_patterns 的文本知识 → 结构化模板(SQLite routing_templates 表): # - RAG 回答「怎么加工」(文本),模板库供向导「直接生成路线」(结构化) # - 知识文档与模板互相引用:template.asset_title ↔ 知识资产标题 # 面向整个机加工行业:车/铣/钻镗/磨/钣金/焊接/装配/城轨装配。 # ============================================================ from __future__ import annotations from typing import Any from sqlalchemy import select from server.auth.context import get_identity # 内置模板定义:steps = (seq, 工序码, 工序名, 设备类型, 工时下限, 上限, setup, changeover) _BUILTIN: list[dict[str, Any]] = [ { "code": "TPL-TURNING", "name": "车削类零件(轴/套/盘)", "category": "车削", "keywords": "车削,轴,套,盘,销,外圆,螺纹,回转体", "description": "回转体零件典型路线:下料→车端面→粗车→半精车→切槽/螺纹→精车→去毛刺→终检", "asset_title": "车削工艺路线模式", "steps": [ (10, "OP-BLANK", "下料", "锯床/剪板机", 2, 5, 10, 5), (20, "OP-FACE", "车端面打中心孔", "卧式车床", 3, 8, 15, 10), (30, "OP-ROUGH-TURN", "粗车外圆", "数控车床", 5, 15, 20, 15), (40, "OP-SEMI-TURN", "半精车", "数控车床", 4, 10, 15, 10), (50, "OP-GROOVE", "切槽/车螺纹", "数控车床", 3, 8, 15, 10), (60, "OP-FINISH-TURN", "精车", "数控车床", 4, 12, 20, 15), (70, "OP-DEBURR", "去毛刺", "钳工台", 2, 5, 5, 0), (80, "OP-INSPECT", "终检", "检验台", 3, 6, 5, 0), ], }, { "code": "TPL-MILLING", "name": "铣削类零件(箱体/支架/平板)", "category": "铣削", "keywords": "铣削,箱体,支架,平板,型腔,平面,壳体", "description": "箱体支架类典型路线:毛坯→粗铣基准面→粗铣各面→半精→精铣→钻孔攻丝→去毛刺→终检", "asset_title": "铣削工艺路线模式", "steps": [ (10, "OP-BLANK", "下料/毛坯", "锯床", 2, 5, 10, 5), (20, "OP-ROUGH-MILL-REF", "粗铣基准面", "立式加工中心", 8, 20, 25, 15), (30, "OP-ROUGH-MILL", "粗铣各面/槽", "立式加工中心", 10, 30, 20, 15), (40, "OP-SEMI-MILL", "半精铣", "立式加工中心", 8, 20, 15, 10), (50, "OP-FINISH-MILL", "精铣", "立式加工中心", 8, 25, 20, 15), (60, "OP-DRILL-TAP", "钻孔攻丝", "钻攻中心", 5, 15, 15, 10), (70, "OP-DEBURR", "去毛刺", "钳工台", 3, 6, 5, 0), (80, "OP-INSPECT", "终检", "三坐标/检验台", 5, 10, 5, 0), ], }, { "code": "TPL-HOLE", "name": "孔系加工(钻/镗/铰)", "category": "钻镗", "keywords": "钻孔,镗孔,铰孔,攻丝,孔系,深孔", "description": "孔系典型路线:钻中心孔→钻→扩→铰/镗→锪窝→攻丝;高精度孔粗镗→半精镗→精镗", "asset_title": "钻镗孔系工艺模式", "steps": [ (10, "OP-CENTER", "钻中心孔", "钻床", 1, 3, 10, 5), (20, "OP-DRILL", "钻孔", "摇臂钻/加工中心", 2, 8, 10, 8), (30, "OP-REAM-BORE", "铰孔/镗孔", "镗床/加工中心", 3, 10, 15, 10), (40, "OP-TAP", "攻丝", "钻攻中心", 1, 4, 10, 5), (50, "OP-INSPECT", "孔位检验", "检验台", 2, 5, 5, 0), ], }, { "code": "TPL-GRINDING", "name": "磨削精加工", "category": "磨削", "keywords": "磨削,精磨,外圆磨,平面磨,淬火,超精", "description": "热处理后精加工:校直→粗磨→半精磨→精磨→(超精/研磨)→检验;磨削常为瓶颈", "asset_title": "磨削与超精工艺模式", "steps": [ (10, "OP-STRAIGHTEN", "校直", "校直机", 2, 5, 10, 5), (20, "OP-ROUGH-GRIND", "粗磨", "外圆磨床", 5, 12, 25, 20), (30, "OP-SEMI-GRIND", "半精磨", "外圆磨床", 4, 10, 20, 15), (40, "OP-FINISH-GRIND", "精磨", "外圆磨床", 5, 15, 25, 20), (50, "OP-INSPECT", "形位检验", "圆度仪/检验台", 3, 8, 5, 0), ], }, { "code": "TPL-SHEETMETAL", "name": "钣金冲压", "category": "钣金", "keywords": "钣金,冲压,折弯,冲裁,拉伸,板料", "description": "板料典型路线:开料→冲裁→折弯→整形→去毛刺→表面处理", "asset_title": "钣金冲压工艺模式", "steps": [ (10, "OP-SHEAR", "开料", "剪板机", 1, 3, 10, 5), (20, "OP-PUNCH", "冲裁/冲孔", "冲床", 0.5, 2, 20, 15), (30, "OP-BEND", "折弯", "折弯机", 1, 4, 15, 10), (40, "OP-REFORM", "整形", "整形模/压机", 1, 3, 10, 8), (50, "OP-DEBURR", "去毛刺", "打磨台", 1, 3, 5, 0), (60, "OP-SURFACE", "表面处理", "喷涂/电镀线", 3, 10, 15, 0), ], }, { "code": "TPL-WELDING", "name": "焊接结构件", "category": "焊接", "keywords": "焊接,结构件,坡口,组对,探伤,焊缝", "description": "下料→坡口→组对点固→焊接→矫正→探伤→机加接口→涂装;先焊后机加保证接口精度", "asset_title": "焊接结构件工艺模式", "steps": [ (10, "OP-CUT", "下料/坡口", "等离子切割/铣边机", 3, 8, 15, 10), (20, "OP-FIT", "组对点固", "组对平台", 5, 15, 20, 10), (30, "OP-WELD", "焊接", "焊接工位/机器人", 10, 40, 25, 20), (40, "OP-CORRECT", "矫正", "火焰/机械矫正", 3, 10, 10, 5), (50, "OP-NDT", "探伤", "UT/MT 探伤", 5, 15, 10, 0), (60, "OP-MACHINE-IF", "机加接口", "加工中心", 8, 20, 25, 15), ], }, { "code": "TPL-ASSEMBLY", "name": "机械装配(部装→总装)", "category": "装配", "keywords": "装配,部装,总装,机电,调试,测试", "description": "零部件齐套→部装→分总成→总装→电气接线→功能测试→终检→包装;部装常为瓶颈", "asset_title": "机械装配工艺模式", "steps": [ (10, "OP-KIT", "齐套检验", "备料区", 5, 10, 5, 0), (20, "OP-SUBASSY", "部装", "装配台", 30, 60, 15, 15), (30, "OP-ASSY-1", "总装一段", "装配台", 20, 45, 10, 10), (40, "OP-ASSY-2", "总装二段", "装配台", 20, 45, 10, 10), (50, "OP-WIRE", "电气接线", "接线工位", 10, 30, 10, 5), (60, "OP-TEST", "功能测试", "测试台", 10, 25, 15, 5), (70, "OP-FINAL", "终检包装", "检验台", 5, 15, 5, 0), ], }, { "code": "TPL-RAIL-ASSY", "name": "城轨机构装配(部装锚瓶颈)", "category": "装配", "keywords": "城轨,机构,部装,总装,干线,门机构", "description": "现场完整生产路线形态:部装(瓶颈,≈45分/件)→ 总装1…N 串联(≈30分/件)→ 检验交付;" "BOM 几乎全部消耗在首道部装", "asset_title": "城轨机构装配路线模板", "steps": [ (10, "OP-SUBASSY", "机构部装", "共享装配工位", 40, 50, 15, 15), (20, "OP-ASSY-1", "总装一", "共享装配工位", 25, 35, 10, 10), (30, "OP-ASSY-2", "总装二", "共享装配工位", 25, 35, 10, 10), (40, "OP-ASSY-3", "总装三", "共享装配工位", 25, 35, 10, 10), (50, "OP-INSPECT", "检验交付", "检验台", 10, 20, 5, 0), ], }, ] def seed_templates() -> int: """内置模板落库(幂等);返回模板总数。资产互链:按标题解析 assetId。""" from server.db.database import get_session from server.db.models import RoutingTemplate, RoutingTemplateStep # 标题 → assetId(知识资产互链) asset_by_title: dict[str, str] = {} try: from server.knowledge.assets import get_knowledge for a in get_knowledge().assets: asset_by_title[a.get("title") or ""] = a.get("assetId") or "" except Exception: pass with get_session() as session: existing = {t.code for t in session.execute(select(RoutingTemplate).where( RoutingTemplate.tenant_uuid == "platform", RoutingTemplate.deleted == 0, )).scalars()} for tpl in _BUILTIN: if tpl["code"] in existing: continue row = RoutingTemplate( tenant_uuid="platform", code=tpl["code"], name=tpl["name"], industry="machining", category=tpl["category"], keywords=tpl["keywords"], description=tpl["description"], asset_id=asset_by_title.get(tpl.get("asset_title") or "", ""), source="builtin", ) session.add(row) session.flush() for (seq, code, name, eq, low, high, setup, chg) in tpl["steps"]: session.add(RoutingTemplateStep( tenant_uuid="platform", template_id=row.id, seq=seq, operation_code=code, operation_name=name, equipment_type=eq, std_min_low=float(low), std_min_high=float(high), setup_min=float(setup), changeover_min=float(chg), )) session.commit() return len(session.execute(select(RoutingTemplate).where( RoutingTemplate.tenant_uuid == "platform", RoutingTemplate.deleted == 0, )).scalars().all()) def list_templates() -> list[dict[str, Any]]: """模板清单(含步骤)。""" from server.db.database import get_session from server.db.models import RoutingTemplate, RoutingTemplateStep identity = get_identity() seed_templates() with get_session() as session: out = [] for t in session.execute(select(RoutingTemplate).where( RoutingTemplate.tenant_uuid.in_(("platform", identity.tenant_uuid)), RoutingTemplate.deleted == 0, ).order_by(RoutingTemplate.id)).scalars(): steps = session.execute( select(RoutingTemplateStep).where( RoutingTemplateStep.tenant_uuid == t.tenant_uuid, RoutingTemplateStep.template_id == t.id, RoutingTemplateStep.deleted == 0, ) .order_by(RoutingTemplateStep.seq)).scalars().all() out.append(_template_dict(t, steps)) return out def _template_dict(t, steps) -> dict[str, Any]: return { "code": t.code, "name": t.name, "industry": t.industry, "category": t.category, "keywords": t.keywords.split(","), "description": t.description, "assetId": t.asset_id, "source": t.source, "steps": [{ "seq": s.seq, "operationCode": s.operation_code, "operationName": s.operation_name, "equipmentType": s.equipment_type, "stdMinLow": s.std_min_low, "stdMinHigh": s.std_min_high, "stdMinDefault": round((s.std_min_low + s.std_min_high) / 2, 1), "setupMin": s.setup_min, "changeoverMin": s.changeover_min, } for s in steps], } def get_template(code: str) -> dict[str, Any] | None: return next((t for t in list_templates() if t["code"] == code), None) def recommend_templates(text: str, top_k: int = 3) -> list[dict[str, Any]]: """按产品名/描述关键词推荐模板(缺路线时向导用)。""" text = (text or "").strip() scored: list[tuple[float, dict]] = [] for t in list_templates(): score = 0.0 for kw in t["keywords"]: if kw and kw in text: score += 2.0 if t["category"] in text: score += 1.5 for step in t["steps"]: if step["operationName"] and step["operationName"] in text: score += 0.5 if score > 0: scored.append((score, t)) scored.sort(key=lambda x: -x[0]) hits = [dict(t, matchScore=s) for s, t in scored[:top_k]] if not hits: # 无关键词命中 → 用 RAG 检索工艺模式知识反查模板 try: from server.knowledge.assets import get_knowledge from server.knowledge.retrieval import hybrid_search rag_hits = hybrid_search(get_knowledge().iter_search_units(), f"{text} 工艺路线", top_k=3) asset_ids = {h.get("assetId") for h in rag_hits} hits = [dict(t, matchScore=0.5) for t in list_templates() if t.get("assetId") in asset_ids][:top_k] except Exception: hits = [] return hits def apply_template_to_product(world: dict[str, Any], template_code: str, product_code: str, product_name: str = "") -> dict[str, Any]: """把模板实例化为产品柔性工艺路线(工时取区间中值,来源=模板)。 调用方负责 P2 确认与落盘;返回摘要。 """ tpl = get_template(template_code) if not tpl: raise ValueError(f"未找到工艺模板:{template_code}") pn = product_name or product_code # 已有路线 → 先清掉该产品旧步骤(模板重放) world["flexRoutings"] = [r for r in world.get("flexRoutings") or [] if r.get("productCode") != product_code] ops = {o["code"]: o for o in world.get("flexOperations") or []} equipment = world.get("flexEquipment") or [] added_ops = 0 for step in tpl["steps"]: code = step["operationCode"] if code not in ops: world.setdefault("flexOperations", []).append({ "code": code, "name": step["operationName"], "isBottleneck": False, "changeoverMin": step["changeoverMin"], }) ops[code] = world["flexOperations"][-1] added_ops += 1 # 无能力设备时给运行中设备补该能力(能力池共享;标推断) if not any(code in (e.get("capabilities") or []) for e in equipment): for e in equipment: if e.get("status") == "RUNNING": e.setdefault("capabilities", []).append(code) e.setdefault("opStdTime", {})[code] = step["stdMinDefault"] world.setdefault("flexRoutings", []).append({ "productCode": product_code, "productName": pn, "seq": step["seq"], "operationCode": code, "requireMold": False, "stdTimePerUnit": step["stdMinDefault"], "stdTimeSource": "模板", }) # 班组资格覆盖:模板工序并入通用班组(无则新建),避免 NO_TEAM 卡死 tpl_ops = [s["operationCode"] for s in tpl["steps"]] teams = world.get("flexTeams") or [] generic = next((tm for tm in teams if tm.get("code") == "T-TPL"), None) if generic is None and teams is not None: generic = {"code": "T-TPL", "name": "模板工序班组(推断)", "memberCount": 4, "supportOps": [], "skillLevel": "L2"} world.setdefault("flexTeams", []).append(generic) if generic is not None: for code in tpl_ops: if code not in generic["supportOps"]: generic["supportOps"].append(code) # 产品主数据缺失则补 if not any(m.get("code") == product_code for m in world.get("flexMaterials") or []): world.setdefault("flexMaterials", []).append({ "code": product_code, "name": pn, "type": "FINISHED_PRODUCT", "unit": "件", "stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0, }) return {"template": tpl["code"], "templateName": tpl["name"], "productCode": product_code, "steps": len(tpl["steps"]), "addedOperations": added_ops, "assetId": tpl.get("assetId") or ""}