2026-07-21 11:05:57 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 意图识别两级管线(moduleId: core-intent, 可重生 ✅)
|
|
|
|
|
|
# plan.md §9.6:一级规则快路(<10ms,POC 已验证)→ 二级 LLM 语义解析(JSON 强校验)
|
|
|
|
|
|
# 低置信拒识;LLM 不可用/输出非法 → 自动降级为 unknown(引导用户换说法)
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
from __future__ import annotations # 前向类型引用
|
|
|
|
|
|
|
|
|
|
|
|
import re # 正则快路
|
|
|
|
|
|
from typing import Any # 类型标注
|
|
|
|
|
|
|
|
|
|
|
|
from pydantic import ValidationError # 契约校验异常
|
|
|
|
|
|
|
|
|
|
|
|
from server.agent_core.providers import get_provider # LLM Provider
|
|
|
|
|
|
from server.contracts import IntentResult # 意图结果契约
|
|
|
|
|
|
|
|
|
|
|
|
# 世界状态类型别名(快路需要查产线表做聚焦解析)
|
|
|
|
|
|
World = dict[str, Any]
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- LLM 系统提示词(意图枚举 + 槽位说明 + 输出 Schema) ----------------
|
|
|
|
|
|
_LLM_SYSTEM = """你是工厂排产系统的意图解析器。把用户的中文口令解析为 JSON(只输出 JSON,不要多余文字)。
|
|
|
|
|
|
输出格式:{"intent": "...", "params": {...}, "confidence": 0.0~1.0}
|
|
|
|
|
|
可选 intent 及其 params:
|
2026-07-23 13:38:43 +08:00
|
|
|
|
- schedule.run 试排/排产/重排。params: strategy ∈ [DELIVERY_FIRST交期优先, CAPACITY_BALANCE产能均衡, CHANGEOVER_MIN换型最小化, CAMPAIGN战役合并, COST_FIRST成本最优(换型代理), FIFO先进先出, COMPREHENSIVE综合](默认COMPREHENSIVE), engine ∈ [RULE规则, CP约束规划(OR-Tools), HYBRID混合(RULE热启动+CP改良), GA遗传(暂RULE代跑)];未点名引擎时用排产参数 defaultEngine
|
2026-07-21 11:05:57 +08:00
|
|
|
|
- schedule.publish 发布/下发当前排产版本。params: {}
|
2026-07-23 13:38:43 +08:00
|
|
|
|
- campaign.preview 战役/批次合并预览。params: windowDays=交期窗口天数(默认7)- order.upsert 新建或修改销售订单。params: orderNo=订单号(仅修改时给,如 SO20260716001), customerName=客户名, customerLevel ∈ [VIP,A,B,C], productCode=成品编码或名称, quantity=数量, deliveryDate=交期(YYYY-MM-DD), priority=1~9, isRush=是否加急
|
2026-07-21 11:05:57 +08:00
|
|
|
|
- order.cancel 取消销售订单。params: orderNo=订单号(如 SO20260716001)
|
|
|
|
|
|
- order.complete 完成销售订单。params: orderNo=订单号(如 SO20260716001)
|
2026-07-23 13:38:43 +08:00
|
|
|
|
- order.delete 删除销售订单(物理移除)。params: orderNo=订单号
|
|
|
|
|
|
- order.clear 一键清空全部订单与采购委外建议。params: {}
|
|
|
|
|
|
- order.submit 提交订单审核。params: orderNo=订单号
|
|
|
|
|
|
- order.approve 批准订单(可批量)。params: orderNo 或 orderIds
|
|
|
|
|
|
- order.reject 驳回订单。params: orderNo, reviewNote=原因(可选)
|
|
|
|
|
|
- rush.evaluate 紧急插单影响快评(固定轨沙盒)。params: orderNo=已有订单号(可选), customerName, customerLevel, productCode, quantity, deliveryDate, priority, strategy
|
|
|
|
|
|
- rush.apply 采用上次插单快评结果(写主干+草稿排产)。params: {} 或与 evaluate 相同字段
|
|
|
|
|
|
- forecast.query 查看预测/长周期订单。params: {}
|
|
|
|
|
|
- forecast.upsert 新建或编辑预测订单。params: productCode, quantity, bucket∈[DAY,WEEK,MONTH], periodStart, dueDate, confidence, status
|
|
|
|
|
|
- forecast.delete 删除预测订单。params: forecastNo
|
|
|
|
|
|
- forecast.convert 预测转正为销售订单。params: forecastNo
|
|
|
|
|
|
- plan.buckets 时间分桶计划/日周月粗能力。params: mode∈[DAY,WEEK,MONTH,HYBRID], capacityMode∈[FINITE有限,INFINITE无限], horizonDays, includeForecast
|
|
|
|
|
|
- plan.rccp 有限/无限产能粗评估对照。params: mode, horizonDays, includeForecast
|
|
|
|
|
|
- plan.feasibility 交期可行性分析(能否按期/缺口)。params: horizonDays, includeForecast
|
|
|
|
|
|
- plan.inventory 库存投影/库存曲线。params: materialCode, materialType∈[FINISHED_PRODUCT,RAW_MATERIAL], bucket∈[DAY,WEEK], horizonDays, includeForecast
|
|
|
|
|
|
- plan.leveling 产能平衡/削峰建议。params: mode∈[DAY,WEEK,MONTH,HYBRID], targetLoad, horizonDays, includeForecast
|
|
|
|
|
|
- plan.supply 产供方向决策(加班/扩线/外协)。params: mode, targetLoad, horizonDays, includeForecast
|
2026-07-21 11:05:57 +08:00
|
|
|
|
- order.decompose 订单分解/展开BOM/生成采购委外建议。params: orderNo=订单号(可选,缺省=全部可排产订单)
|
2026-07-23 13:38:43 +08:00
|
|
|
|
- mrp.release 下达/确认下达采购或委外建议单(草稿转正式)。params: orderNo=订单号(可选), kind ∈ [all全部, purchase采购, outsource委外](默认all)
|
|
|
|
|
|
- plan.trace 追溯/钉扎/看订单依据/从哪排的/BOM库存负荷。params: orderNo=订单号(可选,FO-/SO-), track ∈ [fixed, flex, auto]
|
|
|
|
|
|
- master.material.upsert 新建物料主数据。params: code=编码, name=名称, type ∈ [FINISHED_PRODUCT成品, SEMI_FINISHED半成品, RAW_MATERIAL原料], unit=单位, stock=库存, procurementLeadTime=采购前置期天数
|
|
|
|
|
|
- master.changeover.upsert 换型矩阵写入。params: fromFamily, toFamily, setupMinutes, note?; delete=true+id 删除
|
|
|
|
|
|
- changeover.query 查看换型矩阵。params: 无
|
|
|
|
|
|
- master.clear 一键清理主数据(清空)。params: scope ∈ [resource资源, process工艺模型, calendar日历维保, flex柔性资源, all全部]
|
|
|
|
|
|
- master.query 查订单/物料/工序/BOM/工艺路线/主数据概览/柔性资源/委外/采购建议。params: entity ∈ [order, material, operation, bom, routing, overview, flex, mrp], code=订单号或料号(可选), orderRef=last, aspect ∈ [outsource, purchase, all], query=原口令
|
|
|
|
|
|
- params.update 更新排产参数/客户等级权重。params: vipWeight/aWeight/bWeight/cWeight(可选数字), resetDefaults=是否恢复默认
|
|
|
|
|
|
- params.query 查看当前排产参数/客户等级权重。params: {}
|
|
|
|
|
|
- constraint.profile.query 查看约束配置。params: {}
|
|
|
|
|
|
- constraint.profile.save 更新约束配置。params: constraintId, enabled, kind∈[hard,soft], resetDefaults
|
|
|
|
|
|
- data.import 批量导入订单或物料(自然语言多行/CSV)。params: kind ∈ [orders, materials], text=原始导入正文
|
|
|
|
|
|
- flex.site.load 加载现场完整生产路线/替换柔性演示数据。params: routePath=xlsx路径(可选), dataDir=源表目录(可选), includeSiblings=是否带同源其它单
|
2026-07-28 02:12:46 +08:00
|
|
|
|
- flex.schedule 柔性排产/能力池排产/虚拟产线/多品种小批量/走全流程排产/滚动排产/把这个订单排产/计划排产/用外部算法试排。params: sortMode ∈ [ASC正排, DESC倒排, BOTTLENECK瓶颈锚](默认BOTTLENECK), window ∈ [realtime实时60m, short短窗2h, mid中窗2d, long长窗7d, full全量](可选), orderNo=订单号(可选), orderRef=last(指代刚查过的订单), engine=EXTERNAL(外部算法), skillId=skill_id(可选), useExternal=true(可选)
|
2026-07-23 13:38:43 +08:00
|
|
|
|
- flex.reschedule 短窗重排/日窗重排/局部重排/分级重排。params: level ∈ [L2短窗, L3日窗, L4全局]
|
|
|
|
|
|
- flex.swap L1局部换机/备机接手。params: equipmentCode(如 PRESS-01), woId(可选), markFault(默认true)
|
|
|
|
|
|
- flex.capacity 瓶颈产能/产能池评估/各工序能做多少。params: {}
|
|
|
|
|
|
- flex.simulate_due 询问某产品多少数量何时能交(交期承诺)。params: productCode ∈ [HV-HARNESS高压线束, PDU-UNIT PDU配电单元], quantity=数量
|
|
|
|
|
|
- flex.compare 三模式对比/正排倒排瓶颈锚对比。params: {}
|
|
|
|
|
|
- flex.rush 柔性紧急插单。params: productCode, quantity, dueDate(可选), priority(默认1)
|
|
|
|
|
|
- flex.fault 设备故障/停机/恢复。params: equipmentCode, status ∈ [MAINTENANCE故障, RUNNING恢复]
|
|
|
|
|
|
- flex.resource.patch 改设备区域状态或模具寿命锁定。params: kind ∈ [equipment, mold], code, status/zone/lifeUsed
|
|
|
|
|
|
- conflict.list 查看冲突/冲突中心/有哪些冲突。params: scope ∈ [flex, fixed, all]
|
|
|
|
|
|
- flex.conflict.resolve 解决冲突/修复冲突。params: conflictId, action(可选)
|
|
|
|
|
|
- sap.status SAP连接/集成状态。params: {}
|
|
|
|
|
|
- sap.sync.inbound 从SAP拉单/同步SAP订单/同步库存。params: {}
|
|
|
|
|
|
- sap.sync.outbound 回写SAP/完工回写。params: {}
|
|
|
|
|
|
- mes.status MES连接/车间系统状态。params: {}
|
|
|
|
|
|
- mes.dispatch 下发MES/下发车间。params: track ∈ [flex柔性, fixed固定]
|
|
|
|
|
|
- mes.report 报工/模拟报工。params: woId=工单号(可选), track, progressPct, finish
|
2026-07-21 11:05:57 +08:00
|
|
|
|
- scenario.compare 多方案/多策略对比试排(沙盒,不影响当前方案)。params: {}
|
2026-07-23 13:38:43 +08:00
|
|
|
|
- scenario.sensitivity 主控参数敏感性分析 Tornado(沙盒)。params: strategy=策略模板(可选)
|
|
|
|
|
|
- sop.compile 把SOP编译成约束规则包预览。params: query=SOP标题或关键词(如换线)
|
|
|
|
|
|
- sop.apply 应用SOP规则包(P2)。params: query=同上 或 pack=编译结果对象
|
2026-07-21 11:05:57 +08:00
|
|
|
|
- checkpoint.create 存档/建检查点/打快照。params: label=快照名(可选)
|
|
|
|
|
|
- checkpoint.rollback 回滚/回到某个检查点。params: pairId=检查点ID(可选,缺省=最近一个)
|
|
|
|
|
|
- knowledge.query 查知识库/问规定/问SOP/问策略原理(如"换线有什么规定")。params: query=原始问题
|
2026-07-28 02:12:46 +08:00
|
|
|
|
- knowledge.import 导入知识文档(PDF/docx/md)。params: path=本地文件路径, kind ∈ [sop,process,algorithm,case](可选), title=标题(可选)
|
|
|
|
|
|
- schedule.wizard 引导式排产向导(我要排产/帮我排产/怎么排产/不会排产)。params: {}
|
|
|
|
|
|
- readiness.query 数据齐备度检查/工时维护情况(哪些工时没维护、排产数据够不够)。params: {}
|
|
|
|
|
|
- data.analyze 分析项目/文件/当前数据内容,整理盘点并列出排产缺口。params: query=原口令
|
|
|
|
|
|
- folder.analyze 解析工程目录里的 Excel(读内容/样例/能否开排)。params: {}
|
|
|
|
|
|
- folder.schedule 根据工程目录数据导入并试排。params: {}
|
|
|
|
|
|
- assistant.reply 通用排产问答(怎么用/你是谁/解释现状/开放问题)。params: query=原口令
|
|
|
|
|
|
- flex.time.update 更新单件工时(如"把部装工时改成50分钟")。params: operationCode=工序编码或名称, productCode=产品编码(可选), stdMin=分钟数
|
|
|
|
|
|
- skill.list 查看已接入外部算法。params: {}
|
|
|
|
|
|
- skill.health 算法 skill 健康检查。params: skillId(可选)
|
|
|
|
|
|
- skill.register 登记算法 skill。params: skill_id, endpoint, name(可选), track ∈ [flex,fixed], auth(可选)
|
|
|
|
|
|
- skill.enable 启用/停用算法 skill。params: skillId, enabled=true|false
|
2026-07-23 13:38:43 +08:00
|
|
|
|
- report.generate 生成报告。params: reportType ∈ [daily日报, version-diff版本对比, plan排产方案], orderNo=订单号(可选), orderRef=last(可选)
|
|
|
|
|
|
- viewport.mode 切换视图。params: mode ∈ [gantt甘特图, load负荷热力, util资源利用率, due交期承诺看板, flex柔性甘特, pool产能池, kpi KPI仪表盘, compare方案对比表]
|
2026-07-21 11:05:57 +08:00
|
|
|
|
- viewport.filter 过滤订单。params: type ∈ [vip, customer]; type=customer 时给 name=客户名
|
|
|
|
|
|
- viewport.focus 聚焦某条产线。params: lineCode 如 "L001"(产线A=L001 产线B=L002 产线C=L003)
|
|
|
|
|
|
- viewport.highlight 高亮风险。params: what ∈ [overdue超期, conflict冲突]
|
|
|
|
|
|
- viewport.timescale 时间粒度。params: scale ∈ [day, hour]
|
|
|
|
|
|
- viewport.reset 重置视图。params: {}
|
2026-07-23 13:38:43 +08:00
|
|
|
|
- query.kpi 查询指标/KPI/利用率/情况/打开仪表盘。params: {}
|
|
|
|
|
|
- guidance.next 下一步建议/该做什么/空态引导。params: {}
|
2026-07-21 11:05:57 +08:00
|
|
|
|
- data.reset 清空数据重新初始化。params: {}
|
|
|
|
|
|
- help 询问能做什么。params: {}
|
|
|
|
|
|
- unknown 无法归类。confidence 给低值。
|
|
|
|
|
|
只输出一个 JSON 对象。confidence 表示你对解析的把握。"""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 13:38:43 +08:00
|
|
|
|
_LEVEL_MAP = {"VIP": "VIP", "甲": "A", "乙": "B", "丙": "C", "A": "A", "B": "B", "C": "C"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_due_date(t: str) -> str | None:
|
|
|
|
|
|
"""从口令里抽交期:支持 YYYY-MM-DD / YYYY/MM/DD / M月D日 / M-D(缺省当年)。"""
|
|
|
|
|
|
import datetime as _dt
|
|
|
|
|
|
m = re.search(r"(20\d{2})[-/.](\d{1,2})[-/.](\d{1,2})", t)
|
|
|
|
|
|
if m:
|
|
|
|
|
|
y, mo, d = int(m.group(1)), int(m.group(2)), int(m.group(3))
|
|
|
|
|
|
else:
|
|
|
|
|
|
m2 = re.search(r"(\d{1,2})\s*月\s*(\d{1,2})\s*[日号]?", t)
|
|
|
|
|
|
if not m2:
|
|
|
|
|
|
return None
|
|
|
|
|
|
y, mo, d = _dt.date.today().year, int(m2.group(1)), int(m2.group(2))
|
|
|
|
|
|
try:
|
|
|
|
|
|
return _dt.date(y, mo, d).strftime("%Y-%m-%d")
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _order_upsert_slots(t: str, world: World) -> dict:
|
|
|
|
|
|
"""尽力从口令抽取建/改单槽位(缺失项留空,由 handler 追问或用现单兜底)。"""
|
|
|
|
|
|
params: dict[str, Any] = {}
|
|
|
|
|
|
m_no = re.search(r"(SO\d{6,})", t, re.I) # 编辑目标订单号
|
|
|
|
|
|
if m_no:
|
|
|
|
|
|
params["orderNo"] = m_no.group(1).upper()
|
|
|
|
|
|
m_cust = re.search(r"客户\s*[::]?\s*([^\s,,。::]{1,16})", t) \
|
|
|
|
|
|
or re.search(r"(?:给|向|为)\s*([^\s,,。]{1,16}?)\s*(?:新建|新增|下|加|开|录)", t)
|
|
|
|
|
|
if m_cust:
|
|
|
|
|
|
params["customerName"] = m_cust.group(1).strip()
|
|
|
|
|
|
prod = None # 成品解析:先名称/编码扫描
|
|
|
|
|
|
for m in world.get("materials", []):
|
|
|
|
|
|
if m.get("type") == "FINISHED_PRODUCT" and (m["name"] in t or m["code"].lower() in t.lower()):
|
|
|
|
|
|
prod = m
|
|
|
|
|
|
break
|
|
|
|
|
|
if prod:
|
|
|
|
|
|
params["productCode"] = prod["code"]
|
|
|
|
|
|
m_qty = re.search(r"数量\s*[::]?\s*(\d{1,6})", t) \
|
|
|
|
|
|
or re.search(r"(\d{1,6})\s*(?:套|个|件|台|只|pcs)", t, re.I)
|
|
|
|
|
|
if m_qty:
|
|
|
|
|
|
params["quantity"] = int(m_qty.group(1))
|
|
|
|
|
|
due = _parse_due_date(t)
|
|
|
|
|
|
if due:
|
|
|
|
|
|
params["deliveryDate"] = due
|
|
|
|
|
|
m_lvl = re.search(r"\b(VIP)\b|([甲乙丙])类?客户|等级\s*([ABC])", t, re.I)
|
|
|
|
|
|
if m_lvl:
|
|
|
|
|
|
key = (m_lvl.group(1) or m_lvl.group(2) or m_lvl.group(3) or "").upper()
|
|
|
|
|
|
if key in _LEVEL_MAP:
|
|
|
|
|
|
params["customerLevel"] = _LEVEL_MAP[key]
|
|
|
|
|
|
if re.search(r"加急|插单|紧急|急单", t):
|
|
|
|
|
|
params["isRush"] = True
|
|
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 02:12:46 +08:00
|
|
|
|
def _work_dir_has_tables() -> bool:
|
|
|
|
|
|
"""任一工程项目目录下已有 Excel/CSV → 开排优先走目录导入。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
import os
|
|
|
|
|
|
from server.state.projects import get_project_store
|
|
|
|
|
|
snap = get_project_store().snapshot(include_messages=False)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
for p in snap.get("projects") or []:
|
|
|
|
|
|
if not isinstance(p, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
work = str(p.get("workDir") or "").strip()
|
|
|
|
|
|
if not work or not os.path.isdir(work):
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
names = os.listdir(work)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if any(
|
|
|
|
|
|
(
|
|
|
|
|
|
re.search(r"\.(xlsx|xlsm|csv|txt|sql)$", n, re.I)
|
|
|
|
|
|
and not n.startswith("~$") and not n.startswith(".")
|
|
|
|
|
|
)
|
|
|
|
|
|
for n in names
|
|
|
|
|
|
):
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_imperative_schedule(text: str) -> bool:
|
|
|
|
|
|
"""用户明确要求「现在就排」,不是问概念/向导。"""
|
|
|
|
|
|
t = (text or "").strip()
|
|
|
|
|
|
if re.search(r"什么叫|什么是|啥叫|怎么排|如何排|会不会|能不能排产吗|向导", t):
|
|
|
|
|
|
return False
|
|
|
|
|
|
return bool(re.search(
|
2026-08-11 00:54:05 +08:00
|
|
|
|
r"^(给我|帮我|为我|请|麻烦).{0,6}(模拟|试)?排(产|程)?(一下|一版)?[!!。.~~]*$"
|
|
|
|
|
|
r"|^(我要你|你给我|你帮我|要你).{0,6}(模拟|试)?排(产|程)?(一下|一版)?[!!。.~~]*$"
|
|
|
|
|
|
r"|^排产(吧|啊|呀|啦|!|!)?(一下|一版)?$"
|
|
|
|
|
|
r"|^(开始|马上|赶紧|直接).{0,4}(给我|帮我)?(模拟|试)?排(产|程)?(一下|一版)?[!!。.~~]*$"
|
|
|
|
|
|
r"|^(排一下|开排|开干(排产)?|去排产|模拟排产(一下)?|(试|模拟)排(一版|一下)?)[!!。.~~]*$"
|
|
|
|
|
|
r"|我要你排产|给我排产|你给我排产|帮我排产吧|模拟排产一下|帮我模拟排产|给我模拟排产",
|
2026-07-28 02:12:46 +08:00
|
|
|
|
t,
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 11:05:57 +08:00
|
|
|
|
# ---------------- 一级:规则快路(移植 POC parseNL,P0 纯解析) ----------------
|
|
|
|
|
|
def parse_fast(text: str, world: World) -> IntentResult | None:
|
|
|
|
|
|
"""正则词表快路:命中返回高置信意图;未命中返回 None(交给 LLM)。"""
|
|
|
|
|
|
t = text.strip() # 去除首尾空白
|
|
|
|
|
|
|
|
|
|
|
|
# 快路辅助:构造高置信结果(来源标注 RULE_FAST)
|
|
|
|
|
|
def hit(intent: str, params: dict | None = None) -> IntentResult:
|
|
|
|
|
|
return IntentResult(intent=intent, params=params or {}, confidence=0.95, source="RULE_FAST")
|
|
|
|
|
|
|
|
|
|
|
|
if re.search(r"重置数据|清空数据|重新初始化", t): # 数据重置
|
|
|
|
|
|
return hit("data.reset")
|
2026-08-11 00:54:05 +08:00
|
|
|
|
# 工程图纸:仅命中明确的 DXF/图纸解析、预览或主数据映射表达。
|
|
|
|
|
|
drawing_path = re.search(r"([A-Za-z]:[\\/][^\n\r\"']+?\.dxf)\b", t, re.I)
|
|
|
|
|
|
if re.search(r"(?:DXF|图纸)", t, re.I):
|
|
|
|
|
|
params = {"query": t}
|
|
|
|
|
|
if drawing_path:
|
|
|
|
|
|
params["path"] = drawing_path.group(1)
|
|
|
|
|
|
if re.search(r"预览|可视化|打开|查看", t):
|
|
|
|
|
|
params["drawingAction"] = "preview"
|
|
|
|
|
|
return hit("data.analyze", params)
|
|
|
|
|
|
if re.search(r"映射|转成|生成|写入|入库|物料|BOM|工艺", t, re.I):
|
|
|
|
|
|
params["drawingAction"] = "stage"
|
|
|
|
|
|
return hit("data.analyze", params)
|
|
|
|
|
|
if re.search(r"解析|分析|识别|读取|扫描", t):
|
|
|
|
|
|
params["drawingAction"] = "analyze"
|
|
|
|
|
|
return hit("data.analyze", params)
|
|
|
|
|
|
# 带真实表格/SQL 路径的口令优先按数据处理,不能被句尾「知识库」误吞。
|
|
|
|
|
|
has_data_path = bool(re.search(r"[A-Za-z]:[\\/].*\.(xlsx|xlsm|csv|txt|sql)\b", t, re.I))
|
|
|
|
|
|
if has_data_path and re.search(r"排产|试排|开排", t):
|
|
|
|
|
|
return hit("folder.schedule", {"query": t})
|
|
|
|
|
|
if has_data_path and re.search(r"解析|分析|读取|识别|导入|入库|主数据|知识库", t):
|
|
|
|
|
|
return hit("data.analyze", {"query": t})
|
2026-07-28 02:12:46 +08:00
|
|
|
|
# 工程目录:深度解析 / 按目录数据排产(靠前)
|
|
|
|
|
|
if re.search(
|
|
|
|
|
|
r"根据.{0,16}排产|用.{0,8}(目录|文件夹|这些数据).{0,8}排产"
|
|
|
|
|
|
r"|以上数据|这些(数据|文件|表格).{0,12}排|按(上面|这些|目录).{0,8}排"
|
|
|
|
|
|
r"|导入.{0,6}(目录|文件夹).{0,6}(并)?(试)?排",
|
|
|
|
|
|
t,
|
|
|
|
|
|
):
|
|
|
|
|
|
return hit("folder.schedule", {"query": t})
|
|
|
|
|
|
if re.search(
|
|
|
|
|
|
r"文件夹|工程目录|工作目录|"
|
|
|
|
|
|
r"项目(里|里面|内)(的)?(文件|数据|资料)|项目(文件夹|目录)|"
|
|
|
|
|
|
r"解析.{0,8}(文件夹|目录)|"
|
|
|
|
|
|
r"读取.{0,6}(文件夹|目录)|分析.{0,6}(文件夹|目录)|再解析.*目录|重新解析.*目录",
|
|
|
|
|
|
t, re.I,
|
|
|
|
|
|
):
|
|
|
|
|
|
return hit("folder.analyze", {"query": t})
|
|
|
|
|
|
|
|
|
|
|
|
# —— 明确「开排」祈使句:直接干,禁止再甩四选一菜单 ——
|
|
|
|
|
|
# 有工程目录表格 → 按目录导入试排;否则直接柔性排一版
|
|
|
|
|
|
if _is_imperative_schedule(t):
|
|
|
|
|
|
if _work_dir_has_tables():
|
|
|
|
|
|
return hit("folder.schedule", {"query": t})
|
|
|
|
|
|
return hit("flex.schedule", {"sortMode": "BOTTLENECK", "query": t})
|
|
|
|
|
|
# 小白引导(怎么排 / 不会排)才进向导;「我要排产」保留向导兼容
|
|
|
|
|
|
if re.search(
|
|
|
|
|
|
r"怎么排产|排产向导|带我排(一版|产)?|教我排|一步步排|"
|
|
|
|
|
|
r"不(会|懂)排产|第一次排产|从头排产|^我要排产$",
|
|
|
|
|
|
t,
|
|
|
|
|
|
):
|
|
|
|
|
|
return hit("schedule.wizard")
|
|
|
|
|
|
if re.search(r"直接排(一版|产)?|赶紧排|马上排|帮我排一下", t):
|
|
|
|
|
|
return hit("flex.schedule", {"sortMode": "BOTTLENECK"})
|
|
|
|
|
|
|
|
|
|
|
|
# 问候 / 几点了 / 概念 / 能力 → 通用助理(什么都能问)
|
|
|
|
|
|
if re.search(r"^(你好|您好|嗨|hi|hello|在吗)[\s!!.。??]*$", t, re.I) \
|
|
|
|
|
|
or re.search(r"(现在|当前)?(几点|几时了)|几点了|今天(几号|日期|星期几)|现在是几号", t) \
|
|
|
|
|
|
or re.search(r"你是谁|你能做什么|怎么用|如何使用|介绍一下(你自己|系统|助手)?|你会什么", t) \
|
|
|
|
|
|
or re.search(r"(什么叫|什么是|啥叫|解释).{0,8}排产|排产.{0,4}(什么意思|是什么|啥意思)|你会排产|会不会排产", t) \
|
|
|
|
|
|
or re.search(r"讲个笑话|来个笑话|算一下|\d+\s+[+\-*/×÷]\s+\d+", t): # 须空格,勿匹配交期 2026-08
|
|
|
|
|
|
return hit("assistant.reply", {"query": t})
|
|
|
|
|
|
# 闲聊 / 发牢骚(勿用「带我/教我」抢排产口令)
|
|
|
|
|
|
if re.search(
|
|
|
|
|
|
r"天气|下雨|吃饭|午饭|晚饭|累了|无聊|哈哈|呵呵|讲个笑话|"
|
|
|
|
|
|
r"上班好烦|不想干|摸鱼|咋整|怎么办啊",
|
|
|
|
|
|
t, re.I,
|
|
|
|
|
|
) and not re.search(r"排产|排程|试排|排一版", t):
|
|
|
|
|
|
return hit("assistant.reply", {"query": t})
|
|
|
|
|
|
# 开放式项目/现状问答(先于过宽的 KPI「怎么样」)
|
|
|
|
|
|
if re.search(
|
|
|
|
|
|
r"(现在|当前|项目).{0,6}(怎么样|什么情况|进度|状态|有什么数据)"
|
|
|
|
|
|
r"|帮我看看|看看(现在|当前|项目)|解释一下|总结一下|现状"
|
|
|
|
|
|
r"|不知道(该|怎么)|该怎么(办|做|开始)|从哪(里)?开始"
|
|
|
|
|
|
r"|能不能排|随便问问|跟你聊聊",
|
|
|
|
|
|
t,
|
|
|
|
|
|
):
|
|
|
|
|
|
return hit("assistant.reply", {"query": t})
|
|
|
|
|
|
# 分析项目/文件/数据(靠前:避免落到 unknown + 能力清单)
|
|
|
|
|
|
if re.search(
|
|
|
|
|
|
r"【文件解析】|我已提供主数据材料"
|
|
|
|
|
|
r"|分析.{0,16}(文件|数据|项目|excel|表格|内容|附件)"
|
|
|
|
|
|
r"|(这个|该|此).{0,4}文件|文件里面|项目数据|数据内容"
|
|
|
|
|
|
r"|整理.{0,8}(数据|缺口)|排产还缺什么|还缺什么.*排产",
|
|
|
|
|
|
t, re.I,
|
|
|
|
|
|
):
|
|
|
|
|
|
return hit("data.analyze", {"query": t})
|
2026-07-23 13:38:43 +08:00
|
|
|
|
# MES 下发须先于泛化「下发」(否则「下发MES」会误进 schedule.publish)
|
|
|
|
|
|
if re.search(r"下发\s*MES|下发车间|MES\s*下发|推送车间", t, re.I):
|
|
|
|
|
|
tr = "fixed" if re.search(r"固定", t) else "flex"
|
|
|
|
|
|
return hit("mes.dispatch", {"track": tr})
|
|
|
|
|
|
if re.search(r"模拟报工|报工回流|工序报工|完工报工", t, re.I):
|
|
|
|
|
|
p: dict = {"track": "fixed" if re.search(r"固定", t) else "flex"}
|
|
|
|
|
|
if re.search(r"全部|一键|批量", t):
|
|
|
|
|
|
p["finishAll"] = True
|
|
|
|
|
|
m_wo = re.search(r"(?:工单|#)\s*(\d+)", t)
|
|
|
|
|
|
if m_wo:
|
|
|
|
|
|
p["woId"] = int(m_wo.group(1))
|
|
|
|
|
|
if re.search(r"完工|100|完成", t):
|
|
|
|
|
|
p["finish"] = True
|
|
|
|
|
|
return hit("mes.report", p)
|
|
|
|
|
|
if re.search(r"MES\s*状态|车间系统状态|执行进度", t, re.I):
|
|
|
|
|
|
return hit("mes.status")
|
|
|
|
|
|
if re.search(r"发布|下发", t): # 发布版本(P2;MES 下发已在上方拦截)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
return hit("schedule.publish")
|
2026-07-23 13:38:43 +08:00
|
|
|
|
# 批量导入(须先于单条新建:含「导入订单/物料」)
|
|
|
|
|
|
kind = None
|
|
|
|
|
|
from server.aps_domain.intake import detect_import_kind
|
|
|
|
|
|
kind = detect_import_kind(t)
|
|
|
|
|
|
if kind:
|
|
|
|
|
|
return hit("data.import", {"kind": kind, "text": t})
|
|
|
|
|
|
# 删除订单(物理移除;与「取消」区分)
|
|
|
|
|
|
m_order_del = re.search(r"(删除|删掉|移除).{0,6}(订单)?\s*(SO\d{6,})", t, re.I)
|
|
|
|
|
|
if m_order_del:
|
|
|
|
|
|
return hit("order.delete", {"orderNo": m_order_del.group(3).upper()})
|
|
|
|
|
|
# OR-03 订单池审核
|
|
|
|
|
|
if re.search(r"批量批准|全部批准待审|批准全部待审", t):
|
|
|
|
|
|
return hit("order.approve", {"orderIds": "pending"})
|
|
|
|
|
|
m_ap = re.search(r"(批准|通过审核|审核通过)\s*(订单)?\s*(SO\d{6,})", t, re.I)
|
|
|
|
|
|
if m_ap:
|
|
|
|
|
|
return hit("order.approve", {"orderNo": m_ap.group(3).upper()})
|
|
|
|
|
|
m_rj = re.search(r"(驳回|拒绝)\s*(订单)?\s*(SO\d{6,})", t, re.I)
|
|
|
|
|
|
if m_rj:
|
|
|
|
|
|
p = {"orderNo": m_rj.group(3).upper()}
|
|
|
|
|
|
m_note = re.search(r"原因[::\s]+(.+)$", t)
|
|
|
|
|
|
if m_note:
|
|
|
|
|
|
p["reviewNote"] = m_note.group(1).strip()
|
|
|
|
|
|
return hit("order.reject", p)
|
|
|
|
|
|
m_sub = re.search(r"(提交审核|送审|提交)\s*(订单)?\s*(SO\d{6,})", t, re.I)
|
|
|
|
|
|
if m_sub:
|
|
|
|
|
|
return hit("order.submit", {"orderNo": m_sub.group(3).upper()})
|
|
|
|
|
|
if re.search(r"提交审核|送审", t) and re.search(r"订单", t):
|
|
|
|
|
|
return hit("order.submit", {})
|
|
|
|
|
|
if re.search(r"订单池|待审核订单|订单看板", t):
|
|
|
|
|
|
return hit("order.pool")
|
|
|
|
|
|
# 一键清理订单池 / 主数据板块
|
|
|
|
|
|
if re.search(r"(一键)?(清理|清空|清除).{0,6}(全部)?(销售)?订单|清空订单池", t):
|
|
|
|
|
|
return hit("order.clear")
|
|
|
|
|
|
if re.search(r"(一键)?(清理|清空|清除).{0,8}(主数据|工艺模型|物料|维保|产线资源)", t):
|
|
|
|
|
|
scope = ("process" if re.search(r"工艺|物料|BOM|工序|路线", t)
|
|
|
|
|
|
else "calendar" if re.search(r"维保|日历", t)
|
|
|
|
|
|
else "resource" if re.search(r"资源|产线", t)
|
|
|
|
|
|
else "all")
|
|
|
|
|
|
return hit("master.clear", {"scope": scope})
|
2026-07-21 11:05:57 +08:00
|
|
|
|
m_order_cancel = re.search(r"(取消|作废).{0,6}(订单)?\s*(SO\d{8,}\d*)", t, re.I)
|
|
|
|
|
|
if m_order_cancel:
|
|
|
|
|
|
return hit("order.cancel", {"orderNo": m_order_cancel.group(3).upper()})
|
|
|
|
|
|
m_order_done = re.search(r"(完成|完结|关闭).{0,6}(订单)?\s*(SO\d{8,}\d*)", t, re.I)
|
|
|
|
|
|
if m_order_done:
|
|
|
|
|
|
return hit("order.complete", {"orderNo": m_order_done.group(3).upper()})
|
2026-07-23 13:38:43 +08:00
|
|
|
|
# 委外/采购建议问答(须先于下达/分解动作与泛化查数)
|
|
|
|
|
|
if re.search(r"委外|外协|采购建议|分解建议", t) and not re.search(
|
|
|
|
|
|
r"下达|确认下达|正式下单|转正式|生成(采购|委外)建议|订单分解|分解(全部|所有)?订单|展开\s*BOM", t, re.I
|
|
|
|
|
|
):
|
|
|
|
|
|
from server.aps_domain.master_query import extract_code, _mrp_aspect
|
|
|
|
|
|
code = extract_code(t)
|
|
|
|
|
|
params: dict = {"entity": "mrp", "query": t, "aspect": _mrp_aspect(t)}
|
|
|
|
|
|
if code:
|
|
|
|
|
|
params["code"] = code
|
|
|
|
|
|
elif re.search(r"(这个|该|此|本).{0,4}订单|订单.{0,6}(有没有|有无|有|吗)", t):
|
|
|
|
|
|
params["orderRef"] = "last"
|
|
|
|
|
|
return hit("master.query", params)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
# 订单分解(MRP):分解订单 SOxxx / 订单分解 / 生成采购建议 / 展开 BOM
|
|
|
|
|
|
m_decomp = re.search(r"(分解|展开).{0,6}(订单)?\s*(SO\d{8,}\d*)", t, re.I)
|
|
|
|
|
|
if m_decomp:
|
|
|
|
|
|
return hit("order.decompose", {"orderNo": m_decomp.group(3).upper()})
|
2026-07-23 13:38:43 +08:00
|
|
|
|
if re.search(r"(下达|确认下达|正式下单|转正式).{0,6}(采购|委外|建议单?|MRP)|把.{0,8}建议.{0,4}下达|建议单下达", t, re.I):
|
|
|
|
|
|
kind_mrp = ("purchase" if re.search(r"采购", t) and not re.search(r"委外", t)
|
|
|
|
|
|
else "outsource" if re.search(r"委外", t) and not re.search(r"采购", t)
|
|
|
|
|
|
else "all")
|
|
|
|
|
|
m_no = re.search(r"(SO\d{6,})", t, re.I)
|
|
|
|
|
|
p: dict = {"kind": kind_mrp}
|
|
|
|
|
|
if m_no:
|
|
|
|
|
|
p["orderNo"] = m_no.group(1).upper()
|
|
|
|
|
|
return hit("mrp.release", p)
|
|
|
|
|
|
# 计划追溯 / 订单钉扎(OR-06):固定 SO 或柔性 FO
|
|
|
|
|
|
if re.search(r"追溯|钉扎|排产依据|怎么排的|从哪排|单据链|看(一下)?(订单)?依据|(BOM|库存|负荷).{0,6}(追溯|依据)|订单.{0,4}(追溯|依据|钉扎)", t, re.I):
|
|
|
|
|
|
m_no = re.search(r"(SO\d{6,}|FO-[\w-]+|RUSH-\d+)", t, re.I)
|
|
|
|
|
|
params: dict = {}
|
|
|
|
|
|
if m_no:
|
|
|
|
|
|
params["orderNo"] = m_no.group(1).upper()
|
|
|
|
|
|
if re.search(r"柔性|钉扎|FO-|虚拟产线", t, re.I) or (params.get("orderNo") or "").startswith(("FO-", "RUSH-")):
|
|
|
|
|
|
params["track"] = "flex"
|
|
|
|
|
|
elif re.search(r"固定|销售订单", t):
|
|
|
|
|
|
params["track"] = "fixed"
|
|
|
|
|
|
return hit("plan.trace", params)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
if re.search(r"订单分解|分解(全部|所有)?订单|生成(采购|委外)建议|展开\s*BOM|物料需求(计划|分解)|MRP", t, re.I):
|
|
|
|
|
|
return hit("order.decompose")
|
2026-07-23 13:38:43 +08:00
|
|
|
|
# 新建物料(主数据对话录入)
|
|
|
|
|
|
if re.search(r"(新建|新增|创建|录入).{0,4}物料|加一?个?(原料|成品|半成品)", t):
|
|
|
|
|
|
from server.aps_domain.intake import parse_material_slots
|
|
|
|
|
|
return hit("master.material.upsert", parse_material_slots(t))
|
|
|
|
|
|
# MD-06 换型矩阵
|
|
|
|
|
|
if re.search(r"查看换型|换型矩阵|产品族换型|打开换型", t):
|
|
|
|
|
|
return hit("changeover.query")
|
|
|
|
|
|
m_co = re.search(
|
|
|
|
|
|
r"(设置|录入|修改|更新)?换型\s*([A-Za-z0-9_-]+)\s*(到|→|->|至)\s*([A-Za-z0-9_-]+)\s*(\d+(?:\.\d+)?)\s*分?",
|
|
|
|
|
|
t, re.I,
|
|
|
|
|
|
)
|
|
|
|
|
|
if m_co:
|
|
|
|
|
|
return hit("master.changeover.upsert", {
|
|
|
|
|
|
"fromFamily": m_co.group(2).upper(),
|
|
|
|
|
|
"toFamily": m_co.group(4).upper(),
|
|
|
|
|
|
"setupMinutes": float(m_co.group(5)),
|
|
|
|
|
|
})
|
|
|
|
|
|
# 主数据/订单只读检索(实例数据;工艺模式/模板走知识库)
|
|
|
|
|
|
if not re.search(r"模式|模板|怎么生成|总则|车削|铣削|磨削|特征驱动|知识清单", t) and (
|
|
|
|
|
|
re.search(r"(查|查看|看看|检索|有哪些|列出|查询).{0,8}(订单|物料|成品|原料|工序|BOM|物料清单|工艺路线|主数据|柔性资源|能力池)|主数据概览|订单池里有什么|现在有哪些订单", t, re.I)
|
|
|
|
|
|
or re.search(r"查(一下)?\s*(订单|物料|BOM|工艺路线|工序)\s*[\w\-]{4,}", t)
|
|
|
|
|
|
):
|
|
|
|
|
|
from server.aps_domain.master_query import detect_entity, extract_code
|
|
|
|
|
|
ent = detect_entity(t)
|
|
|
|
|
|
code = extract_code(t)
|
|
|
|
|
|
params: dict = {"entity": ent, "query": t}
|
|
|
|
|
|
if code:
|
|
|
|
|
|
params["code"] = code
|
|
|
|
|
|
return hit("master.query", params)
|
2026-07-28 02:12:46 +08:00
|
|
|
|
# M-F 引导式排产向导(靠前入口已覆盖;此处兜底小白说法)
|
|
|
|
|
|
if re.search(r"^我要排产$|怎么排产|排产向导|不(会|懂)排产|第一次排产|从头排产", t):
|
|
|
|
|
|
return hit("schedule.wizard")
|
|
|
|
|
|
# 「帮我排产」等祈使句已在上方走 flex/folder;此处勿再抢
|
2026-07-23 13:38:43 +08:00
|
|
|
|
# 现场完整生产路线导入(须先于泛化「柔性排产」)
|
|
|
|
|
|
if re.search(r"加载现场(生产)?(订单)?路线|导入完整生产路线|加载现场订单|替换柔性演示|现场数据导入", t):
|
|
|
|
|
|
p: dict = {}
|
|
|
|
|
|
if re.search(r"仅主单|只要主单|不带其它", t):
|
|
|
|
|
|
p["includeSiblings"] = False
|
|
|
|
|
|
return hit("flex.site.load", p)
|
|
|
|
|
|
# 新建/编辑订单(order.upsert,P2):动词 + "订单";尽力抽取槽位,缺失由 handler 追问
|
|
|
|
|
|
if re.search(r"(新建|新增|创建|录入|下|加|新来|来).{0,4}(一?[张个笔条])?\s*(销售)?订单|录单|建单|开一?张单", t) \
|
|
|
|
|
|
or re.search(r"(修改|编辑|更新|改).{0,4}(销售)?订单\s*(SO\d{6,})", t, re.I):
|
|
|
|
|
|
return hit("order.upsert", _order_upsert_slots(t, world))
|
|
|
|
|
|
# IND-02 SOP→约束(须在知识库问答之前,避免「SOP」只走 knowledge.query)
|
|
|
|
|
|
if re.search(r"应用.{0,8}(SOP|规则包)|把.{0,12}(SOP|规则).{0,8}(应用|落地|写入约束)", t, re.I):
|
|
|
|
|
|
q = "换线"
|
|
|
|
|
|
if re.search(r"齐套|库存|缺料", t):
|
|
|
|
|
|
q = "齐套"
|
|
|
|
|
|
elif re.search(r"插单|急单", t):
|
|
|
|
|
|
q = "插单"
|
|
|
|
|
|
elif re.search(r"换线|换型", t):
|
|
|
|
|
|
q = "换线"
|
|
|
|
|
|
m_t = re.search(r"[《「]?([\u4e00-\u9fffA-Za-z0-9\-_]{2,24}SOP)[》」]?", t)
|
|
|
|
|
|
if m_t:
|
|
|
|
|
|
q = m_t.group(1)
|
|
|
|
|
|
return hit("sop.apply", {"query": q})
|
|
|
|
|
|
if re.search(r"编译.{0,6}(SOP|规则)|SOP.{0,6}(编译|转约束|规则包)|把.{0,8}SOP.{0,8}(编译|转成约束)", t, re.I):
|
|
|
|
|
|
q = "换线"
|
|
|
|
|
|
if re.search(r"齐套|库存|缺料", t):
|
|
|
|
|
|
q = "齐套"
|
|
|
|
|
|
elif re.search(r"插单|急单", t):
|
|
|
|
|
|
q = "插单"
|
|
|
|
|
|
elif re.search(r"换线|换型", t):
|
|
|
|
|
|
q = "换线"
|
|
|
|
|
|
return hit("sop.compile", {"query": q})
|
|
|
|
|
|
# SC-06 敏感性分析(须在方案对比之前,避免「对比参数」误触)
|
|
|
|
|
|
if re.search(r"敏感性|敏感度|Tornado|龙卷风图|参数扰动|主控参数分析", t, re.I):
|
|
|
|
|
|
params: dict[str, Any] = {}
|
|
|
|
|
|
if re.search(r"交期优先", t):
|
|
|
|
|
|
params["strategy"] = "DELIVERY_FIRST"
|
|
|
|
|
|
elif re.search(r"均衡", t):
|
|
|
|
|
|
params["strategy"] = "CAPACITY_BALANCE"
|
|
|
|
|
|
elif re.search(r"换型", t):
|
|
|
|
|
|
params["strategy"] = "CHANGEOVER_MIN"
|
|
|
|
|
|
elif re.search(r"战役|批次", t):
|
|
|
|
|
|
params["strategy"] = "CAMPAIGN"
|
|
|
|
|
|
return hit("scenario.sensitivity", params)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
# 方案对比:多方案/多策略/对比几种策略/对比一下(Explore 沙盒 §5.1)
|
|
|
|
|
|
if re.search(r"多方案|多策略|方案对比|(对比|比较).{0,8}(策略|方案|排法)|对比一下|三种策略|沙盒对比", t):
|
|
|
|
|
|
return hit("scenario.compare")
|
2026-07-23 13:38:43 +08:00
|
|
|
|
# 报告生成(M3 §9.10):日报 / 版本对比 / 排产方案
|
|
|
|
|
|
if re.search(r"排产方案报告|排产报告|方案报告|下载排产|导出排产|生成排产方案|下载方案报告", t):
|
|
|
|
|
|
from server.aps_domain.master_query import extract_code
|
|
|
|
|
|
params: dict = {"reportType": "plan"}
|
|
|
|
|
|
code = extract_code(t)
|
|
|
|
|
|
if code:
|
|
|
|
|
|
params["orderNo"] = code
|
|
|
|
|
|
elif re.search(r"(这个|该|此|本).{0,4}订单|本单|当前订单", t):
|
|
|
|
|
|
params["orderRef"] = "last"
|
|
|
|
|
|
return hit("report.generate", params)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
if re.search(r"(生成|出|来一份|写)?(排产)?日报", t) and re.search(r"日报", t):
|
|
|
|
|
|
return hit("report.generate", {"reportType": "daily"})
|
|
|
|
|
|
if re.search(r"版本对比|对比报告|新旧版本", t) and re.search(r"报告|对比", t):
|
|
|
|
|
|
return hit("report.generate", {"reportType": "version-diff"})
|
2026-07-28 02:12:46 +08:00
|
|
|
|
# RAG:导入知识文档(须在知识问答之前,避免「知识」抢答)
|
|
|
|
|
|
m_imp = re.search(
|
|
|
|
|
|
r"(?:导入|加载|入库).{0,6}知识(文档|文件|资料)?\s*(.+)?$|知识(文档|文件).{0,6}(导入|入库)\s*(.+)?$",
|
|
|
|
|
|
t, re.I)
|
|
|
|
|
|
if m_imp or re.search(r"^导入知识", t):
|
|
|
|
|
|
path = None
|
|
|
|
|
|
m_path = re.search(r"([A-Za-z]:[\\/][^\s\"']+\.\w{1,8}|/[^\s\"']+\.\w{1,8})", t)
|
|
|
|
|
|
if m_path:
|
|
|
|
|
|
path = m_path.group(1).strip().rstrip("。,,")
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 取「导入知识文档」后的路径片段
|
|
|
|
|
|
m2 = re.search(r"导入知识(?:文档|文件|资料)?\s+(.+)$", t)
|
|
|
|
|
|
if m2:
|
|
|
|
|
|
path = m2.group(1).strip().rstrip("。,,")
|
|
|
|
|
|
kind = "sop"
|
|
|
|
|
|
if re.search(r"工艺", t):
|
|
|
|
|
|
kind = "process"
|
|
|
|
|
|
elif re.search(r"算法", t):
|
|
|
|
|
|
kind = "algorithm"
|
|
|
|
|
|
elif re.search(r"案例", t):
|
|
|
|
|
|
kind = "case"
|
|
|
|
|
|
return hit("knowledge.import", {"path": path, "kind": kind, "query": t})
|
|
|
|
|
|
# 外部算法 Skill(须在知识问答 / 柔性排产之前)
|
|
|
|
|
|
if re.search(r"(查看|列出|有哪些).{0,6}(算法\s*)?(skill|技能)|已接入算法|算法\s*skill\s*清单|查看已接入算法", t, re.I):
|
|
|
|
|
|
return hit("skill.list")
|
|
|
|
|
|
if re.search(r"(skill|算法).{0,6}健康检查|健康检查.{0,6}(skill|算法)|检查算法(服务|skill)", t, re.I):
|
|
|
|
|
|
# 仅接受 ASCII skill id(algo.xxx 或 skillId=xxx),避免「健康检查」中文被 \w 吞掉
|
|
|
|
|
|
m_sid = re.search(r"(?:skill[_ ]?id|skill)\s*[=::\s]+(algo\.[A-Za-z0-9._-]+|[A-Za-z][A-Za-z0-9._-]*)", t, re.I)
|
|
|
|
|
|
if not m_sid:
|
|
|
|
|
|
m_sid = re.search(r"(algo\.[A-Za-z0-9._-]+)", t, re.I)
|
|
|
|
|
|
sid = m_sid.group(1) if m_sid else None
|
|
|
|
|
|
return hit("skill.health", {"skillId": sid} if sid else {})
|
|
|
|
|
|
if re.search(r"登记(算法)?\s*skill|注册(算法)?\s*skill|接入(算法|外部算法)", t, re.I):
|
|
|
|
|
|
params: dict[str, Any] = {}
|
|
|
|
|
|
m_id = re.search(r"(?:id|skill[_]?id)\s*[=::]\s*([\w.-]+)", t, re.I)
|
|
|
|
|
|
m_ep = re.search(r"(?:endpoint|url|端点)\s*[=::]\s*(\S+)", t, re.I)
|
|
|
|
|
|
m_name = re.search(r"(?:name|名称)\s*[=::]\s*(\S+)", t, re.I)
|
|
|
|
|
|
if m_id:
|
|
|
|
|
|
params["skill_id"] = m_id.group(1)
|
|
|
|
|
|
if m_ep:
|
|
|
|
|
|
params["endpoint"] = m_ep.group(1).rstrip(",,。")
|
|
|
|
|
|
if m_name:
|
|
|
|
|
|
params["name"] = m_name.group(1)
|
|
|
|
|
|
if re.search(r"固定轨|fixed", t, re.I):
|
|
|
|
|
|
params["track"] = "fixed"
|
|
|
|
|
|
else:
|
|
|
|
|
|
params["track"] = "flex"
|
|
|
|
|
|
return hit("skill.register", params)
|
|
|
|
|
|
if re.search(r"(启用|开启|打开|停用|禁用|关闭)\s*(算法\s*)?skill", t, re.I):
|
|
|
|
|
|
enabled = not bool(re.search(r"停用|禁用|关闭", t))
|
|
|
|
|
|
m_sid = re.search(r"skill\s+(algo\.[A-Za-z0-9._-]+|[A-Za-z][A-Za-z0-9._-]*)", t, re.I)
|
|
|
|
|
|
sid = m_sid.group(1) if m_sid else None
|
|
|
|
|
|
return hit("skill.enable", {"skillId": sid, "enabled": enabled, "query": t})
|
|
|
|
|
|
if re.search(r"用外部算法(试排|排产)|外部算法试排|切换算法\s*skill|走外部(算法|skill)排产", t, re.I):
|
|
|
|
|
|
params: dict[str, Any] = {"sortMode": "BOTTLENECK", "engine": "EXTERNAL", "useExternal": True}
|
|
|
|
|
|
m_sid = re.search(r"(?:skill|算法)\s+(algo\.[A-Za-z0-9._-]+)", t, re.I)
|
|
|
|
|
|
if not m_sid:
|
|
|
|
|
|
m_sid = re.search(r"(algo\.[A-Za-z0-9._-]+)", t, re.I)
|
|
|
|
|
|
if m_sid:
|
|
|
|
|
|
params["skillId"] = m_sid.group(1)
|
|
|
|
|
|
return hit("flex.schedule", params)
|
|
|
|
|
|
# M-B 数据齐备度 / 工时维护(须在知识问答之前,避免「工时」被吞)
|
|
|
|
|
|
if re.search(r"工时(维护)?情况|哪些工时(还)?(没|未|待)维护|工时(缺失|齐不齐|完整吗)"
|
|
|
|
|
|
r"|数据(齐备|完整)度|排产数据(检查|齐不齐|够不够)|readiness", t, re.I):
|
|
|
|
|
|
return hit("readiness.query")
|
|
|
|
|
|
m_time = re.search(r"把\s*(\S{1,24}?)\s*(?:的)?工时(?:改|设|调)(?:成|为|到)?\s*(\d+(?:\.\d+)?)\s*分钟?", t)
|
|
|
|
|
|
if m_time:
|
|
|
|
|
|
target = m_time.group(1).strip()
|
|
|
|
|
|
params_t: dict[str, Any] = {"stdMin": float(m_time.group(2)), "operationCode": target}
|
|
|
|
|
|
# 「把产品X的部装工时…」形态:产品 空格/的 工序
|
|
|
|
|
|
m_pc = re.search(r"把\s*([A-Za-z0-9\-]{4,})\s+(\S{1,16}?)\s*(?:的)?工时", t)
|
|
|
|
|
|
if m_pc:
|
|
|
|
|
|
params_t = {"stdMin": float(m_time.group(2)),
|
|
|
|
|
|
"productCode": m_pc.group(1), "operationCode": m_pc.group(2)}
|
|
|
|
|
|
return hit("flex.time.update", params_t)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
# 知识库检索(M3 §8.2):@知识 引用 / 明确问规定与 SOP
|
|
|
|
|
|
m_kn = re.search(r"@知识[::]\s*(\S{1,24})", t)
|
|
|
|
|
|
if m_kn:
|
|
|
|
|
|
return hit("knowledge.query", {"query": m_kn.group(1), "assetTitle": m_kn.group(1)})
|
2026-07-23 13:38:43 +08:00
|
|
|
|
if re.search(r"有什么规定|什么规定|规定是什么|SOP|标准是什么|审批流程|知识库"
|
|
|
|
|
|
r"|机加工|工艺路线生成|工艺路线模式|工艺模式|车削工艺|铣削|磨削|装配工艺"
|
|
|
|
|
|
r"|特征驱动|工艺怎么(生成|排)|知识清单|有哪些知识|城轨机构装配路线", t, re.I):
|
2026-07-21 11:05:57 +08:00
|
|
|
|
return hit("knowledge.query", {"query": t})
|
|
|
|
|
|
# 回滚到指定检查点(时间线导轨点击会合成此口令;ID 为 10 位十六进制)
|
|
|
|
|
|
m_rb = re.search(r"回滚到检查点\s*([0-9a-f]{6,12})", t, re.I)
|
|
|
|
|
|
if m_rb:
|
|
|
|
|
|
return hit("checkpoint.rollback", {"pairId": m_rb.group(1)})
|
|
|
|
|
|
# 回滚到最近检查点(口语形态)
|
|
|
|
|
|
if re.search(r"回滚|回到上一?个?(检查点|存档|快照)", t):
|
|
|
|
|
|
return hit("checkpoint.rollback")
|
|
|
|
|
|
# 手动建档:存档/存个档/检查点/快照(可带名字:"存档 叫 发布前基线")
|
|
|
|
|
|
m_ck = re.search(r"(存个?档|建一?个?检查点|打个?快照)(?:[,,\s]*(?:叫|为|名字是)?\s*(.{1,16}))?$", t)
|
|
|
|
|
|
if m_ck and re.search(r"存个?档|检查点|快照", t):
|
|
|
|
|
|
label = (m_ck.group(2) or "").strip() # 提取可选命名
|
|
|
|
|
|
return hit("checkpoint.create", {"label": label} if label else {})
|
2026-07-23 13:38:43 +08:00
|
|
|
|
if re.search(r"MES\s*状态|车间系统状态|执行进度", t, re.I):
|
|
|
|
|
|
return hit("mes.status")
|
|
|
|
|
|
# SAP 集成(MD-05)
|
|
|
|
|
|
if re.search(r"回写\s*SAP|完工回写|推送\s*SAP|出站同步", t, re.I):
|
|
|
|
|
|
return hit("sap.sync.outbound")
|
|
|
|
|
|
if re.search(r"从\s*SAP\s*拉|同步\s*SAP|拉\s*SAP|入站同步|同步库存|SAP\s*拉单", t, re.I):
|
|
|
|
|
|
return hit("sap.sync.inbound")
|
|
|
|
|
|
if re.search(r"SAP\s*状态|SAP\s*连接|集成状态", t, re.I):
|
|
|
|
|
|
return hit("sap.status")
|
|
|
|
|
|
# 冲突中心 / 修复(EX-03)
|
|
|
|
|
|
if re.search(r"解决冲突|修复冲突|一键修复|处理冲突", t):
|
|
|
|
|
|
m_id = re.search(r"#?\s*(\d+)", t)
|
|
|
|
|
|
return hit("flex.conflict.resolve",
|
|
|
|
|
|
{"conflictId": int(m_id.group(1))} if m_id else {})
|
|
|
|
|
|
if re.search(r"查看冲突|冲突中心|有哪些冲突|冲突列表|看冲突", t):
|
|
|
|
|
|
scope = ("all" if re.search(r"全部|所有", t)
|
|
|
|
|
|
else "fixed" if re.search(r"固定", t) else "flex")
|
|
|
|
|
|
return hit("conflict.list", {"scope": scope})
|
|
|
|
|
|
# L1 局部换机须先于故障口令(同含设备码)
|
|
|
|
|
|
if re.search(r"局部换机|L1\s*换机|备机接手|换到备机|换机\s*(PRESS|WELD|CUT|TEST|LEAK)", t, re.I):
|
|
|
|
|
|
code = "PRESS-01"
|
|
|
|
|
|
m_code = re.search(r"(PRESS|WELD|CUT|TEST|LEAK)-\d+", t, re.I)
|
|
|
|
|
|
if m_code:
|
|
|
|
|
|
code = m_code.group(0).upper()
|
|
|
|
|
|
mark = not bool(re.search(r"不标故障|仅换机", t))
|
|
|
|
|
|
return hit("flex.swap", {"equipmentCode": code, "markFault": mark})
|
|
|
|
|
|
# 分级重排须先于普通柔性排产
|
|
|
|
|
|
if re.search(r"短窗重排|日窗重排|局部重排|分级重排|窗内重排|L2重排|L3重排|L4重排", t, re.I):
|
|
|
|
|
|
level = ("L4" if re.search(r"L4|全局", t, re.I)
|
|
|
|
|
|
else "L3" if re.search(r"L3|日窗|中窗|2\s*天", t, re.I)
|
|
|
|
|
|
else "L2")
|
|
|
|
|
|
return hit("flex.reschedule", {"level": level})
|
|
|
|
|
|
# 三模式对比须先于「正排/倒排」单模式排产
|
|
|
|
|
|
if re.search(r"三模式|正倒排对比|模式对比|(对比|比较).{0,6}(正排|倒排|瓶颈)", t):
|
|
|
|
|
|
return hit("flex.compare")
|
|
|
|
|
|
# 设备故障 / 恢复(DY-01)
|
|
|
|
|
|
if re.search(r"设备故障|恢复设备|设备.{0,8}(故障|坏了|宕机|停机|恢复)|(PRESS|WELD)-\d+.{0,6}(故障|停机|恢复)", t, re.I):
|
|
|
|
|
|
status = "RUNNING" if re.search(r"恢复|修好|上线", t) else "MAINTENANCE"
|
|
|
|
|
|
code = "WELD-01" if re.search(r"焊接|激光|WELD", t, re.I) else "PRESS-01"
|
|
|
|
|
|
m_code = re.search(r"(PRESS|WELD|CUT|TEST|LEAK)-\d+", t, re.I)
|
|
|
|
|
|
if m_code:
|
|
|
|
|
|
code = m_code.group(0).upper()
|
|
|
|
|
|
return hit("flex.fault", {"equipmentCode": code, "status": status})
|
|
|
|
|
|
# 柔性紧急插单(DY-01):须带「柔性」以免与 OR-04 固定轨插单冲突
|
|
|
|
|
|
if re.search(r"柔性\s*(紧急)?插单|柔性急单", t):
|
|
|
|
|
|
code = "PDU-UNIT" if re.search(r"PDU", t, re.I) else "HV-HARNESS"
|
|
|
|
|
|
qty_m = re.search(r"(\d+)\s*(套|件|个)", t)
|
|
|
|
|
|
qty = int(qty_m.group(1)) if qty_m else 50
|
|
|
|
|
|
return hit("flex.rush", {"productCode": code, "quantity": qty, "priority": 1})
|
|
|
|
|
|
# OR-04 固定轨紧急插单:采用 / 快评
|
|
|
|
|
|
if re.search(r"采用插单|确认采用插单|应用插单|插单采用", t):
|
|
|
|
|
|
return hit("rush.apply")
|
|
|
|
|
|
if re.search(r"插单快评|评估插单|插单影响|紧急插单评估|插单评估", t) or (
|
|
|
|
|
|
re.search(r"紧急插单|加急插单|插一?[张个]?急单", t) and not re.search(r"柔性", t)
|
|
|
|
|
|
):
|
|
|
|
|
|
params: dict[str, Any] = {}
|
|
|
|
|
|
m_so = re.search(r"(SO\d{8,})", t, re.I)
|
|
|
|
|
|
if m_so:
|
|
|
|
|
|
params["orderNo"] = m_so.group(1).upper()
|
|
|
|
|
|
qty_m = re.search(r"(\d+)\s*(套|件|个)", t)
|
|
|
|
|
|
if qty_m:
|
|
|
|
|
|
params["quantity"] = int(qty_m.group(1))
|
|
|
|
|
|
if not params.get("orderNo"):
|
|
|
|
|
|
if re.search(r"控制器A|A型|CTRL-A", t, re.I):
|
|
|
|
|
|
params["productCode"] = "CTRL-A"
|
|
|
|
|
|
elif re.search(r"控制器B|B型|CTRL-B", t, re.I):
|
|
|
|
|
|
params["productCode"] = "CTRL-B"
|
|
|
|
|
|
elif re.search(r"控制器C|C型|CTRL-C", t, re.I):
|
|
|
|
|
|
params["productCode"] = "CTRL-C"
|
|
|
|
|
|
else:
|
|
|
|
|
|
params.setdefault("productCode", "CTRL-A")
|
|
|
|
|
|
if re.search(r"交期优先", t):
|
|
|
|
|
|
params["strategy"] = "DELIVERY_FIRST"
|
|
|
|
|
|
elif re.search(r"产能均衡|均衡", t):
|
|
|
|
|
|
params["strategy"] = "CAPACITY_BALANCE"
|
|
|
|
|
|
m_cust = re.search(r"(?:给|为|客户)\s*([^\s,,。]{2,12})", t)
|
|
|
|
|
|
if m_cust and m_cust.group(1) not in ("插单", "急单", "订单"):
|
|
|
|
|
|
params["customerName"] = m_cust.group(1)
|
|
|
|
|
|
return hit("rush.evaluate", params)
|
|
|
|
|
|
# 把这个订单排产 / 给订单xxx做计划排产(须先于泛化「柔性排产」「试排」)
|
|
|
|
|
|
if re.search(r"排产", t) and not re.search(r"查|查看|有哪些|插单|分解|追溯|发布", t) and (
|
|
|
|
|
|
re.search(r"(这个|该|此|本).{0,4}订单|(给|把|对).{0,8}订单|订单\s*[\dA-Za-z\-]{5,}.{0,12}排产|计划排产", t)
|
|
|
|
|
|
):
|
|
|
|
|
|
from server.aps_domain.master_query import extract_code
|
|
|
|
|
|
code = extract_code(t)
|
|
|
|
|
|
# 现场/柔性订单优先走能力池排产
|
|
|
|
|
|
has_flex = bool(world.get("flexOrders"))
|
|
|
|
|
|
params: dict = {"sortMode": "BOTTLENECK"}
|
|
|
|
|
|
if code:
|
|
|
|
|
|
params["orderNo"] = code
|
|
|
|
|
|
else:
|
|
|
|
|
|
params["orderRef"] = "last"
|
|
|
|
|
|
if has_flex or re.search(r"柔性|能力池|虚拟产线", t):
|
|
|
|
|
|
return hit("flex.schedule", params)
|
|
|
|
|
|
# 仅固定轨时走规则试排
|
|
|
|
|
|
return hit("schedule.run", params)
|
|
|
|
|
|
# 柔性排产(能力池 + 虚拟产线):须在固定产线 schedule.run 之前判定
|
|
|
|
|
|
if re.search(r"柔性排产|柔性排一?版|能力池排产|按能力池|虚拟产线|多品种小批量"
|
|
|
|
|
|
r"|全流程排产|走一?个?全流程|正排|顺排|倒排|逆排|瓶颈锚|滚动排产|短窗排产|中窗排产"
|
|
|
|
|
|
r"|实时排产|分钟级排产", t):
|
|
|
|
|
|
mode = ("ASC" if re.search(r"正排|顺排", t)
|
|
|
|
|
|
else "DESC" if re.search(r"倒排|逆排", t)
|
|
|
|
|
|
else "BOTTLENECK")
|
|
|
|
|
|
win = ("realtime" if re.search(r"实时|分钟级", t)
|
|
|
|
|
|
else "short" if re.search(r"短窗", t)
|
|
|
|
|
|
else "mid" if re.search(r"中窗|日窗|2\s*天", t)
|
|
|
|
|
|
else "long" if re.search(r"长窗|周窗|7\s*天", t)
|
|
|
|
|
|
else "full" if re.search(r"全量|全窗", t)
|
|
|
|
|
|
else None)
|
|
|
|
|
|
params: dict = {"sortMode": mode}
|
|
|
|
|
|
if win:
|
|
|
|
|
|
params["window"] = win
|
|
|
|
|
|
elif re.search(r"滚动", t):
|
|
|
|
|
|
params["window"] = "short"
|
|
|
|
|
|
return hit("flex.schedule", params)
|
|
|
|
|
|
# 瓶颈产能评估(瓶颈产能法)
|
|
|
|
|
|
if re.search(r"瓶颈产能|产能池|产能评估|各?工序产能|瓶颈在哪|产能瓶颈|能做多少|日产能", t):
|
|
|
|
|
|
return hit("flex.capacity")
|
|
|
|
|
|
# 交期承诺模拟:问「何时/多久能交/做完」+ 产品 + 数量
|
|
|
|
|
|
m_prod = re.search(r"(高压线束|线束|PDU|HV-?HARNESS|PDU-?UNIT)", t, re.I)
|
|
|
|
|
|
m_qty = re.search(r"(\d{1,6})\s*(套|个|件|pcs)?", t, re.I)
|
|
|
|
|
|
if (re.search(r"(什么时候|多久|何时|几天).{0,6}(能|可以)?.{0,2}(交|交货|完成|做完|下线|出货)|交期(承诺|模拟|评估)|能不能按期",
|
|
|
|
|
|
t) and m_prod and m_qty):
|
|
|
|
|
|
code = "PDU-UNIT" if re.search(r"PDU", t, re.I) else "HV-HARNESS"
|
|
|
|
|
|
return hit("flex.simulate_due", {"productCode": code, "quantity": int(m_qty.group(1))})
|
|
|
|
|
|
# OR-05 预测订单(须在 schedule.run 之前)
|
|
|
|
|
|
if re.search(r"预测转正|转正预测", t):
|
|
|
|
|
|
m_fc = re.search(r"(FC\d{8,})", t, re.I)
|
|
|
|
|
|
return hit("forecast.convert", {"forecastNo": m_fc.group(1).upper()} if m_fc else {})
|
|
|
|
|
|
if re.search(r"删除预测|去掉预测", t):
|
|
|
|
|
|
m_fc = re.search(r"(FC\d{8,})", t, re.I)
|
|
|
|
|
|
return hit("forecast.delete", {"forecastNo": m_fc.group(1).upper()} if m_fc else {})
|
|
|
|
|
|
if re.search(r"新建预测|创建预测|加一?[条个]?预测", t):
|
|
|
|
|
|
params: dict[str, Any] = {"status": "ACTIVE", "bucket": "WEEK"}
|
|
|
|
|
|
qty_m = re.search(r"(\d+)\s*(套|件|个)", t)
|
|
|
|
|
|
if qty_m:
|
|
|
|
|
|
params["quantity"] = int(qty_m.group(1))
|
|
|
|
|
|
if re.search(r"控制器A|CTRL-A", t, re.I):
|
|
|
|
|
|
params["productCode"] = "CTRL-A"
|
|
|
|
|
|
elif re.search(r"控制器B|CTRL-B", t, re.I):
|
|
|
|
|
|
params["productCode"] = "CTRL-B"
|
|
|
|
|
|
elif re.search(r"控制器C|CTRL-C", t, re.I):
|
|
|
|
|
|
params["productCode"] = "CTRL-C"
|
|
|
|
|
|
else:
|
|
|
|
|
|
params["productCode"] = "CTRL-A"
|
|
|
|
|
|
if re.search(r"月", t):
|
|
|
|
|
|
params["bucket"] = "MONTH"
|
|
|
|
|
|
elif re.search(r"日|天", t):
|
|
|
|
|
|
params["bucket"] = "DAY"
|
|
|
|
|
|
return hit("forecast.upsert", params)
|
|
|
|
|
|
if re.search(r"预测订单|查看预测|长周期(订单|需求)|预测需求|预测台账", t):
|
|
|
|
|
|
return hit("forecast.query")
|
|
|
|
|
|
# PL-06 产供方向(先于削峰通用口令,避免「外协建议」落到 MRP)
|
|
|
|
|
|
if re.search(r"产供(方向|决策)?|产能决策|加班建议|扩线建议|外协建议|外协产能|打开产供", t):
|
|
|
|
|
|
params: dict[str, Any] = {"mode": "WEEK", "includeForecast": True, "targetLoad": 0.85}
|
|
|
|
|
|
if re.search(r"混合", t):
|
|
|
|
|
|
params["mode"] = "HYBRID"
|
|
|
|
|
|
elif re.search(r"月", t):
|
|
|
|
|
|
params["mode"] = "MONTH"
|
|
|
|
|
|
elif re.search(r"日|天", t):
|
|
|
|
|
|
params["mode"] = "DAY"
|
|
|
|
|
|
if re.search(r"不含预测|不要预测|仅确定", t):
|
|
|
|
|
|
params["includeForecast"] = False
|
|
|
|
|
|
return hit("plan.supply", params)
|
|
|
|
|
|
# PL-05 产能平衡/削峰(须在「产能均衡试排」之前;勿抢 schedule.run)
|
|
|
|
|
|
if re.search(r"削峰|产能平衡|负荷削峰|计划层均衡|平衡负荷|打开削峰|leveling", t, re.I):
|
|
|
|
|
|
params: dict[str, Any] = {"mode": "WEEK", "includeForecast": True, "targetLoad": 0.85}
|
|
|
|
|
|
if re.search(r"混合", t):
|
|
|
|
|
|
params["mode"] = "HYBRID"
|
|
|
|
|
|
elif re.search(r"月", t):
|
|
|
|
|
|
params["mode"] = "MONTH"
|
|
|
|
|
|
elif re.search(r"日|天", t):
|
|
|
|
|
|
params["mode"] = "DAY"
|
|
|
|
|
|
elif re.search(r"周", t):
|
|
|
|
|
|
params["mode"] = "WEEK"
|
|
|
|
|
|
if re.search(r"不含预测|不要预测|仅确定", t):
|
|
|
|
|
|
params["includeForecast"] = False
|
|
|
|
|
|
return hit("plan.leveling", params)
|
|
|
|
|
|
# PL-04 库存投影
|
|
|
|
|
|
if re.search(r"库存投影|库存曲线|投影库存|物料库存投影|打开库存投影", t):
|
|
|
|
|
|
params: dict[str, Any] = {"includeForecast": True, "bucket": "DAY", "horizonDays": 30}
|
|
|
|
|
|
if re.search(r"按周|周投影|周桶", t):
|
|
|
|
|
|
params["bucket"] = "WEEK"
|
|
|
|
|
|
if re.search(r"原料|原材料", t):
|
|
|
|
|
|
params["materialType"] = "RAW_MATERIAL"
|
|
|
|
|
|
elif re.search(r"成品", t):
|
|
|
|
|
|
params["materialType"] = "FINISHED_PRODUCT"
|
|
|
|
|
|
m_code = re.search(r"(CTRL-[ABC]|PCB-00[12]|RC-001|CASE-001|SCR-001|WIRE-001|LABEL-001)", t, re.I)
|
|
|
|
|
|
if m_code:
|
|
|
|
|
|
params["materialCode"] = m_code.group(1).upper()
|
|
|
|
|
|
if re.search(r"不含预测|不要预测|仅确定", t):
|
|
|
|
|
|
params["includeForecast"] = False
|
|
|
|
|
|
return hit("plan.inventory", params)
|
|
|
|
|
|
# PL-03 可行性(先于分桶/粗能力通用口令)
|
|
|
|
|
|
if re.search(r"可行性(分析|评估|检查)?|能否按期|交期可行性|计划可行性|可行不可行", t):
|
|
|
|
|
|
params: dict[str, Any] = {"includeForecast": True}
|
|
|
|
|
|
if re.search(r"不含预测|不要预测|仅确定|只要订单", t):
|
|
|
|
|
|
params["includeForecast"] = False
|
|
|
|
|
|
return hit("plan.feasibility", params)
|
|
|
|
|
|
# PL-02 粗能力双模式(先于通用分桶口令)
|
|
|
|
|
|
if re.search(r"有限无限|无限有限|粗能力对比|RCCP对比|对比粗能力", t, re.I):
|
|
|
|
|
|
return hit("plan.rccp", {"mode": "HYBRID", "includeForecast": True})
|
|
|
|
|
|
if re.search(r"无限产能|无限粗(能力|评|估)|无限RCCP", t, re.I):
|
|
|
|
|
|
return hit("plan.buckets", {"mode": "HYBRID", "capacityMode": "INFINITE", "includeForecast": True})
|
|
|
|
|
|
if re.search(r"有限产能粗|有限粗(能力|评|估)|粗能力评估|RCCP|有限RCCP", t, re.I):
|
|
|
|
|
|
return hit("plan.buckets", {"mode": "HYBRID", "capacityMode": "FINITE", "includeForecast": True})
|
|
|
|
|
|
# PL-01 时间分桶(须在「试排」等动词前,避免「周计划」误触排产)
|
|
|
|
|
|
if re.search(r"时间分桶|分桶计划|计划分桶|混合分桶|查看分桶|打开分桶|"
|
|
|
|
|
|
r"周计划表|月计划表|日计划桶|粗能力分桶|计划层分桶", t):
|
|
|
|
|
|
params: dict[str, Any] = {"mode": "HYBRID", "includeForecast": True, "capacityMode": "FINITE"}
|
|
|
|
|
|
if re.search(r"混合", t):
|
|
|
|
|
|
params["mode"] = "HYBRID"
|
|
|
|
|
|
elif re.search(r"月", t):
|
|
|
|
|
|
params["mode"] = "MONTH"
|
|
|
|
|
|
elif re.search(r"周", t):
|
|
|
|
|
|
params["mode"] = "WEEK"
|
|
|
|
|
|
elif re.search(r"日|天", t):
|
|
|
|
|
|
params["mode"] = "DAY"
|
|
|
|
|
|
if re.search(r"不含预测|不要预测|仅确定|只要订单", t):
|
|
|
|
|
|
params["includeForecast"] = False
|
|
|
|
|
|
if re.search(r"无限", t):
|
|
|
|
|
|
params["capacityMode"] = "INFINITE"
|
|
|
|
|
|
return hit("plan.buckets", params)
|
|
|
|
|
|
# SC-08 战役合并预览 / 试排(须在换型最小化与通用试排之前)
|
|
|
|
|
|
if re.search(r"战役(合并)?预览|批次(合并)?预览|合并预览|看(一下)?战役", t):
|
|
|
|
|
|
params: dict[str, Any] = {"windowDays": 7}
|
|
|
|
|
|
m_w = re.search(r"(\d+)\s*天", t)
|
|
|
|
|
|
if m_w:
|
|
|
|
|
|
params["windowDays"] = int(m_w.group(1))
|
|
|
|
|
|
return hit("campaign.preview", params)
|
|
|
|
|
|
if re.search(r"战役合并|批次合并|同品合并|合并战役|合并批次", t):
|
|
|
|
|
|
params = {"strategy": "CAMPAIGN", "engine": "HYBRID"}
|
|
|
|
|
|
if re.search(r"规则", t):
|
|
|
|
|
|
params["engine"] = "RULE"
|
|
|
|
|
|
elif re.search(r"约束规划|CP-?SAT|\bCP\b", t, re.I):
|
|
|
|
|
|
params["engine"] = "CP"
|
|
|
|
|
|
if re.search(r"预测", t):
|
|
|
|
|
|
params["includeForecast"] = True
|
|
|
|
|
|
return hit("schedule.run", params)
|
|
|
|
|
|
# SC-07 换型最小化试排(须在通用「试排」与「产能均衡」之前)
|
|
|
|
|
|
if re.search(r"换型最小化|减少换型|换型优先|最小化换型|少换型", t):
|
|
|
|
|
|
params: dict[str, Any] = {"strategy": "CHANGEOVER_MIN", "engine": "HYBRID"}
|
|
|
|
|
|
if re.search(r"规则", t):
|
|
|
|
|
|
params["engine"] = "RULE"
|
|
|
|
|
|
elif re.search(r"约束规划|CP-?SAT|\bCP\b", t, re.I):
|
|
|
|
|
|
params["engine"] = "CP"
|
|
|
|
|
|
if re.search(r"预测", t):
|
|
|
|
|
|
params["includeForecast"] = True
|
|
|
|
|
|
return hit("schedule.run", params)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
# 执行排产:要求"动词形态"命中(试排/重排/排一版/执行|开始|重新排产),
|
|
|
|
|
|
# 避免仅出现"排产"名词的闲聊被误判(该类输入应交给二级 LLM 判定)
|
2026-07-23 13:38:43 +08:00
|
|
|
|
if re.search(r"预测纳入试排|含预测试排|带预测试排|试排.*预测|预测.*试排", t):
|
|
|
|
|
|
return hit("schedule.run", {"includeForecast": True, "engine": "HYBRID"})
|
2026-07-21 11:05:57 +08:00
|
|
|
|
if re.search(r"试排|重排|排一版|(执行|开始|重新|跑一?版?)排产", t):
|
2026-08-26 00:25:46 +08:00
|
|
|
|
strategy = ("KITTING_FIRST" if re.search(r"齐套优先|优先齐套|按齐套", t)
|
|
|
|
|
|
else "SKILL_FIRST" if re.search(r"技能优先|优先技能|按技能", t)
|
|
|
|
|
|
else "DELIVERY_FIRST" if re.search(r"交期", t)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
else "CAPACITY_BALANCE" if re.search(r"均衡|产能", t)
|
2026-07-23 13:38:43 +08:00
|
|
|
|
else "CAMPAIGN" if re.search(r"战役|批次|合并", t)
|
|
|
|
|
|
else "CHANGEOVER_MIN" if re.search(r"换型", t)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
else "COST_FIRST" if re.search(r"成本", t)
|
|
|
|
|
|
else "FIFO" if re.search(r"FIFO|先进先出", t, re.I)
|
|
|
|
|
|
else None) # 未点名策略 → 交给偏好个性化(§8.3)
|
2026-07-23 13:38:43 +08:00
|
|
|
|
engine = None
|
|
|
|
|
|
if re.search(r"规则", t):
|
|
|
|
|
|
engine = "RULE"
|
|
|
|
|
|
elif re.search(r"约束规划|CP-?SAT|\bCP\b", t, re.I):
|
|
|
|
|
|
engine = "CP"
|
|
|
|
|
|
elif re.search(r"遗传|\bGA\b", t, re.I):
|
|
|
|
|
|
engine = "GA"
|
|
|
|
|
|
elif re.search(r"混合|\bHYBRID\b", t, re.I):
|
|
|
|
|
|
engine = "HYBRID"
|
|
|
|
|
|
params: dict = {}
|
|
|
|
|
|
if engine:
|
|
|
|
|
|
params["engine"] = engine
|
2026-07-21 11:05:57 +08:00
|
|
|
|
if strategy: # 用户明确点名才带策略槽位
|
|
|
|
|
|
params["strategy"] = strategy
|
2026-07-23 13:38:43 +08:00
|
|
|
|
if re.search(r"预测", t):
|
|
|
|
|
|
params["includeForecast"] = True
|
2026-07-21 11:05:57 +08:00
|
|
|
|
return hit("schedule.run", params)
|
2026-07-23 13:38:43 +08:00
|
|
|
|
if re.search(r"资源利用率|设备利用率|班组(负荷|并发)|利用率视口|打开利用率|利用率分析", t, re.I):
|
|
|
|
|
|
return hit("viewport.mode", {"mode": "util"})
|
|
|
|
|
|
if re.search(r"分桶视口|打开计划层|计划分桶视口", t):
|
|
|
|
|
|
return hit("viewport.mode", {"mode": "plan"})
|
|
|
|
|
|
if re.search(r"热力|负荷图|负荷热力", t): # 负荷热力图(避免单独「负荷」抢利用率)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
return hit("viewport.mode", {"mode": "load"})
|
2026-07-23 13:38:43 +08:00
|
|
|
|
if re.search(r"KPI\s*仪表盘|运营仪表盘|打开仪表盘", t, re.I):
|
|
|
|
|
|
return hit("query.kpi")
|
|
|
|
|
|
if re.search(r"方案对比表|打开对比表|对比表视口", t):
|
|
|
|
|
|
return hit("scenario.compare")
|
2026-07-21 11:05:57 +08:00
|
|
|
|
if re.search(r"交期|承诺|看板", t): # 交期承诺看板
|
|
|
|
|
|
return hit("viewport.mode", {"mode": "due"})
|
|
|
|
|
|
if re.search(r"甘特|排程图", t): # 甘特图
|
|
|
|
|
|
return hit("viewport.mode", {"mode": "gantt"})
|
|
|
|
|
|
if re.search(r"只看|过滤", t): # 过滤(VIP / 客户名)
|
|
|
|
|
|
if re.search(r"VIP", t, re.I): # VIP 过滤
|
|
|
|
|
|
return hit("viewport.filter", {"type": "vip"})
|
|
|
|
|
|
m = re.search(r"只看(.{1,8}?)的?(订单|单子)", t) # 提取客户名
|
|
|
|
|
|
if m:
|
|
|
|
|
|
return hit("viewport.filter", {"type": "customer", "name": m.group(1)})
|
|
|
|
|
|
m_code = re.search(r"L00(\d)", t, re.I) # 产线编码匹配
|
|
|
|
|
|
m_name = re.search(r"产线\s*([ABC])", t) # 产线口语匹配
|
|
|
|
|
|
if re.search(r"聚焦|放大|定位|只看|看看|看一下", t) and (m_code or m_name): # 聚焦产线
|
|
|
|
|
|
code = f"L00{m_code.group(1)}" if m_code else None # 编码直取
|
|
|
|
|
|
if not code and m_name: # 名称转编码(查产线表)
|
|
|
|
|
|
line = next((l for l in world["lines"] if l["name"].endswith(m_name.group(1))), None)
|
|
|
|
|
|
code = line["code"] if line else None # 命中则取编码
|
|
|
|
|
|
if code: # 拿到编码才算命中
|
|
|
|
|
|
return hit("viewport.focus", {"lineCode": code})
|
|
|
|
|
|
if re.search(r"超期|逾期|延迟", t): # 高亮超期
|
|
|
|
|
|
return hit("viewport.highlight", {"what": "overdue"})
|
|
|
|
|
|
if re.search(r"冲突", t): # 高亮冲突
|
|
|
|
|
|
return hit("viewport.highlight", {"what": "conflict"})
|
|
|
|
|
|
if re.search(r"按小时|小时粒度", t): # 小时粒度
|
|
|
|
|
|
return hit("viewport.timescale", {"scale": "hour"})
|
|
|
|
|
|
if re.search(r"按天|天粒度", t): # 天粒度
|
|
|
|
|
|
return hit("viewport.timescale", {"scale": "day"})
|
|
|
|
|
|
if re.search(r"重置|清除|复位", t): # 重置视口
|
|
|
|
|
|
return hit("viewport.reset")
|
2026-07-28 02:12:46 +08:00
|
|
|
|
if re.search(r"指标|KPI|利用率|仪表盘|(KPI|指标|利用率).{0,4}怎么样", t, re.I): # KPI 查询 / EX-08
|
2026-07-21 11:05:57 +08:00
|
|
|
|
return hit("query.kpi")
|
2026-07-23 13:38:43 +08:00
|
|
|
|
# SC-04 约束配置
|
|
|
|
|
|
if re.search(r"恢复默认约束|重置约束(配置|剖面)?", t):
|
|
|
|
|
|
return hit("constraint.profile.save", {"resetDefaults": True})
|
|
|
|
|
|
if re.search(r"关闭物料齐套|不管物料|先不管齐套|禁用齐套约束", t):
|
|
|
|
|
|
return hit("constraint.profile.save", {
|
|
|
|
|
|
"constraints": {"C6_material_kit": {"enabled": False}}})
|
|
|
|
|
|
if re.search(r"打开物料齐套|启用齐套约束|开启物料齐套", t):
|
|
|
|
|
|
return hit("constraint.profile.save", {
|
|
|
|
|
|
"constraints": {"C6_material_kit": {"enabled": True}}})
|
|
|
|
|
|
if re.search(r"齐套改(为|成)?软约束|物料齐套软约束", t):
|
|
|
|
|
|
return hit("constraint.profile.save", {
|
|
|
|
|
|
"constraints": {"C6_material_kit": {"enabled": True, "kind": "soft"}}})
|
|
|
|
|
|
if re.search(r"齐套改(为|成)?硬约束|物料齐套硬约束", t):
|
|
|
|
|
|
return hit("constraint.profile.save", {
|
|
|
|
|
|
"constraints": {"C6_material_kit": {"enabled": True, "kind": "hard"}}})
|
|
|
|
|
|
if re.search(r"打开约束配置|查看约束配置|约束剖面|约束中心|有哪些约束", t):
|
|
|
|
|
|
return hit("constraint.profile.query")
|
|
|
|
|
|
# OR-02 排产参数 / 客户等级权重
|
|
|
|
|
|
if re.search(r"恢复默认(排产)?参数|重置(客户)?等级权重|恢复权重默认", t):
|
|
|
|
|
|
return hit("params.update", {"resetDefaults": True})
|
|
|
|
|
|
m_vip_w = re.search(r"(提高|调高|设置|改|把).{0,8}VIP.{0,8}权重\s*(到|为|成|=)?\s*(\d+(?:\.\d+)?)", t, re.I)
|
|
|
|
|
|
if m_vip_w:
|
|
|
|
|
|
return hit("params.update", {"vipWeight": float(m_vip_w.group(3))})
|
|
|
|
|
|
if re.search(r"提高VIP权重|调高VIP权重|VIP权重提高", t, re.I):
|
|
|
|
|
|
return hit("params.update", {"vipWeight": 10.0})
|
|
|
|
|
|
if re.search(r"查看排产参数|当前(客户)?等级权重|排产权重|打开排产参数", t):
|
|
|
|
|
|
return hit("params.query")
|
|
|
|
|
|
if re.search(r"下一步建议|下一步做什么|该做什么|给我建议|主动引导|空态引导", t):
|
|
|
|
|
|
return hit("guidance.next")
|
2026-07-21 11:05:57 +08:00
|
|
|
|
if re.search(r"帮助|能做什么|help", t, re.I): # 帮助
|
|
|
|
|
|
return hit("help")
|
|
|
|
|
|
return None # 快路未命中 → 交给二级 LLM
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- 二级:LLM 语义解析(JSON Schema 强校验 + 降级) ----------------
|
|
|
|
|
|
async def parse_llm(text: str) -> IntentResult:
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"""调用 LLM 做意图分类+槽位填充;输出非法/不可用一律降级为 assistant.reply。"""
|
2026-07-21 11:05:57 +08:00
|
|
|
|
raw = await get_provider().chat_json(_LLM_SYSTEM, text) # 请求 LLM(强制 JSON)
|
2026-07-28 02:12:46 +08:00
|
|
|
|
if raw is None: # LLM 不可用 → 通用助理(带上下文作答)
|
|
|
|
|
|
return IntentResult(intent="assistant.reply", params={"query": text},
|
|
|
|
|
|
confidence=0.9, source="LLM")
|
2026-07-21 11:05:57 +08:00
|
|
|
|
try:
|
2026-07-28 02:12:46 +08:00
|
|
|
|
raw.setdefault("params", {})
|
|
|
|
|
|
raw["source"] = "LLM"
|
|
|
|
|
|
result = IntentResult(**raw)
|
|
|
|
|
|
except ValidationError:
|
|
|
|
|
|
return IntentResult(intent="assistant.reply", params={"query": text},
|
|
|
|
|
|
confidence=0.85, source="LLM")
|
|
|
|
|
|
if result.confidence < 0.5 or result.intent == "unknown":
|
|
|
|
|
|
return IntentResult(intent="assistant.reply", params={"query": text},
|
|
|
|
|
|
confidence=0.85, source="LLM")
|
|
|
|
|
|
# 保证下游能拿到原话
|
|
|
|
|
|
if "query" not in result.params:
|
|
|
|
|
|
result.params["query"] = text
|
|
|
|
|
|
return result
|
2026-07-21 11:05:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- 管线入口 ----------------
|
|
|
|
|
|
async def recognize(text: str, world: World) -> IntentResult:
|
|
|
|
|
|
"""两级意图管线总入口:快路优先,未命中走 LLM(P0:纯解析无副作用)。"""
|
|
|
|
|
|
fast = parse_fast(text, world) # 一级:规则快路
|
|
|
|
|
|
if fast is not None: # 命中即返回(毫秒级、零成本)
|
|
|
|
|
|
return fast
|
2026-07-28 02:12:46 +08:00
|
|
|
|
return await parse_llm(text) # 二级:LLM 或通用助理
|