# ============================================================ # 排产工作流编排(moduleId: domain-workflow, 可重生 ✅) # 职责:意图 → 动作 → AgentReply(文本 + 视口命令 + UI 块) # 权力路由:P0/P1 直接执行;P2(发布/重置)经门禁出确认卡(§3.3) # ============================================================ from __future__ import annotations # 前向类型引用 import re import os from typing import Any # 类型标注 from server.agent_core import harness # 门禁 v1 from server.agent_core.audit import write_audit # 审计写入 from server.contracts import ( # 跨层契约 AgentReply, IntentResult, ScheduleResult, ViewportCommand, ) from server.engines import get_engine # 引擎工厂 from server.engines.base import EngineParams # 引擎入参 from server.state.checkpoints import get_checkpoints # 成对快照仓(§4.3) from server.state.store import WorldStore # 世界状态存储 from server.timeutil import add_minutes, fmt_date, today0 # 日期工具 from server.aps_domain.scenario import compare_scenarios # 方案对比(Explore 沙盒 §9.7) from server.aps_domain.reports import build_report, persist_report_xlsx # 报告生成 v1(§9.10) from server.knowledge import get_knowledge, get_preferences # 知识库与偏好仓(M3 §8) from server.contracts import UIBlock # 报告/证据块组装 # 帮助文案:能力清单(help 意图与拒识兜底共用) _HELP = "\n".join([ "可执行的排产能力:", "· 查询现场数据:有哪些订单 / 查订单 102285668 / 这个订单有委外吗 / 查物料 / 查 BOM / 查工艺路线", "· 知识库:机加工工艺怎么生成 / 车削工艺模式 / 装配工艺模式 / 知识清单 / 导入知识文档", "· 生成排产草案:试排一版交期优先 / 产能均衡重排 / 用规则引擎试排", "· 柔性排产(能力池+虚拟产线):跑一版柔性排产 / 把这个订单排产 / 正排 / 倒排 / 瓶颈锚", "· 外部算法:用外部算法试排 / 查看已接入算法 / skill 健康检查", "· 瓶颈产能:瓶颈产能 / 各工序能做多少(按能力池实时算)", "· 交期承诺:某产品 N 套什么时候能交", "· 方案评估:多策略方案对比(沙盒运行,不影响当前版本)", "· 紧急插单:插单快评 / 采用插单(需确认)", "· 预测订单:查看预测 / 新建预测 / 预测纳入试排", "· 切到负荷热力图 / 交期承诺看板 / 甘特图 / 柔性甘特", "· 标出超期订单 / 查看冲突 / 按小时看 / 重置视图", "· 建一个检查点 / 回滚到上一个检查点(需确认)", "· 换线有什么规定 / @知识:机加工工艺路线生成总则(知识库带出处)", "· 生成排产方案报告(下载 Excel 工作计划表) / 生成日报 / 生成版本对比报告", "· 查看当前KPI / 下一步建议 / 发布当前排产版本(需确认)/ 重置数据(需确认)", ]) # 策略模板的中文名(回复文案用) _STRATEGY_CN = {"DELIVERY_FIRST": "交期优先", "CAPACITY_BALANCE": "产能均衡", "COST_FIRST": "成本最优(换型代理)", "CHANGEOVER_MIN": "换型最小化", "CAMPAIGN": "战役合并", "FIFO": "先进先出", "COMPREHENSIVE": "综合优化"} # OR-04:最近一次插单快评(供「采用插单」复用;进程内存) _LAST_RUSH_EVAL: dict[str, Any] = {} def _run_schedule(store: WorldStore, intent: IntentResult, actor: str) -> AgentReply: """执行试排(P1:写草稿版本 + 落盘;发布另走 P2 门禁)。""" # 偏好个性化(§8.3 第一层):用户没点名策略时用其历史最常用策略 explicit = intent.params.get("strategy") # 用户明确点名的策略(可能为空) pref_note = "" # 个性化说明(透明可解释) if explicit: # 点名 → 直接用 strategy = explicit else: # 未点名 → 查偏好仓 strategy, scores = get_preferences().preferred_strategy(default="COMPREHENSIVE") if scores: # 有历史信号才标注(避免误导) pref_note = f"\n(按你的使用偏好选了{_STRATEGY_CN.get(strategy, strategy)},历史使用 {scores.get(strategy, 0)} 次;点名策略可覆盖)" from server.aps_domain.params import get_schedule_params from server.aps_domain.constraints import engine_constraint_flags sp = get_schedule_params(store.data) params = EngineParams( # 组装引擎入参 orderIds=[], # M1:全部待排订单 engineType=intent.params.get("engine") or sp.get("defaultEngine") or "HYBRID", strategyTemplate=strategy, # 策略模板(点名或偏好) planningHorizonDays=int(sp.get("planningHorizonDays") or 14), # 展望期(OR-02 可配) startDate=fmt_date(add_minutes(today0(), 24 * 60)), # 明天开排 includeForecast=bool(intent.params.get("includeForecast")), # OR-05:预测纳入试排 constraints=engine_constraint_flags(store.data), # SC-04 约束剖面 → 引擎开关 timeLimitSeconds=float(sp["cpTimeLimitSeconds"]) if sp.get("cpTimeLimitSeconds") is not None else 8.0, ) engine = get_engine(params.engineType) # CP / HYBRID 真管线;GA 仍 RULE 代跑 result: ScheduleResult = engine.solve(store.data, params, store.next_id) # 求解(写内存世界) # 审计:算法运行留痕(§3.6.2 算法运行类:引擎/策略/计数可复算) write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="schedule.run", target={"type": "SCHEDULE_VERSION", "id": result.versionId}, power="P1", rationale={"strategy": params.strategyTemplate, "engine": params.engineType, "evidence": result.evidenceRefs, "solveStatus": result.solveStatus, "solveTimeSec": result.solveTimeSec, "gap": result.optimalityGap}) store.save() # 试排结果落盘(草稿版本也持久化) get_preferences().record(params.strategyTemplate, source="schedule.run", actor=actor) # 偏好信号沉淀(§8.3) engine_note = "" if params.engineType in ("CP", "HYBRID"): st = result.solveStatus or "—" gap = result.optimalityGap gap_txt = f"{gap:.2%}" if gap is not None else "—" tsec = result.solveTimeSec t_txt = f"{tsec:.2f}s" if tsec is not None else "—" pipe = "RULE→CP-SAT" if params.engineType == "HYBRID" else "CP-SAT" engine_note = ( f"\n({pipe}:状态 {st} · 耗时 {t_txt} · gap {gap_txt};工序落槽=班次占槽)" ) elif params.engineType == "GA": engine_note = "\n(注:GA 真管线尚未接入,本次由规则引擎代跑并留 engineType 痕)" text = (f"排产完成 ✅ 版本 {result.versionNo}({_STRATEGY_CN.get(result.strategy, result.strategy)})\n" f"生产订单 {result.poCount} 个 / 工单 {result.woCount} 个" + ("(含预测)" if params.includeForecast else "") + "\n" f"冲突 {result.conflictCount} 项 · 总延迟 {round(result.totalTardiness)}h · " f"平均利用率 {round(result.avgUtilization * 100)}%{engine_note}{pref_note}\n" f"右侧甘特已刷新为该版本。执行“发布当前排产版本”可进入 P2 发布确认。") # 回复正文(含下一步引导) blocks: list[UIBlock] = [] if result.conflictCount >= 3: from server.aps_domain.guidance import build_guidance blocks.append(UIBlock( blockId="guidance-after-run", type="guidance", props=build_guidance(store.data, context="high_conflict"))) return AgentReply(text=text, blocks=blocks, commands=[ # 联动命令:切甘特 + 重拉世界数据 ViewportCommand(cmd="viewport.mode", params={"mode": "gantt"}, issuedBy="LLM"), ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM"), ]) def _stage_publish(store: WorldStore, session_id: str, actor: str) -> AgentReply: """把“发布版本”压入门禁(P2:出确认卡,不立即执行)。""" versions = store.data["scheduleVersions"] # 版本表 if not versions: # 无版本可发布 from server.aps_domain.guidance import build_guidance block = UIBlock(blockId="guidance-no-version", type="guidance", props=build_guidance(store.data, context="empty_schedule")) return AgentReply( text="当前没有可发布的排产版本。请先生成排产草案,例如:试排一版交期优先。", blocks=[block]) v = versions[-1] # 最新版本 if v["status"] == "PUBLISHED": # 已发布防重 return AgentReply(text=f"版本 {v['versionNo']} 已是发布状态,无需重复发布。") from server.aps_domain.constraints import hard_blocking_conflicts blockers = hard_blocking_conflicts(store.data, version_id=v["id"], track="fixed") if blockers: names = "、".join(sorted({b.get("constraintId", "?") for b in blockers})[:5]) return AgentReply( text=(f"版本 {v['versionNo']} 存在 {len(blockers)} 项硬约束冲突({names})," "按约束配置不可发布。请先解决冲突,或将对应约束改为软约束/关闭后再试。\n" "可说「查看冲突」或「打开约束配置」。")) block = harness.stage_confirmation( # 生成确认卡并登记待确认队列 session_id, "schedule.publish", {"versionId": v["id"]}, title=f"发布排产版本 {v['versionNo']}", summary_lines=[ # 影响面说明(计划员据此决策) f"生产订单 {v['poCount']} 个 / 工单 {v['woCount']} 个将转入执行准备", f"未解决冲突 {v['conflictCount']} 项 · 总延迟 {round(v['totalTardiness'])}h", "发布后该版本成为执行基准(P2 写主干世界状态)", ]) # 审计:门禁出卡事件(GATE 类) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="schedule.publish.stage", target={"type": "SCHEDULE_VERSION", "id": v["id"]}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() # 审计落盘 return AgentReply(text=f"发布版本 {v['versionNo']} 属于 P2 写操作,需要你确认(见下方确认卡)。", blocks=[block]) def _stage_order_upsert(store: WorldStore, session_id: str, intent: IntentResult, actor: str) -> AgentReply: """对话侧新建/编辑订单(P2 → 确认卡):解析槽位 + 成品,缺项则追问。""" from server.aps_domain.orders import (confirmation_for_order_action, find_order, find_product_by_hint) p = dict(intent.params or {}) order_no = str(p.get("orderNo") or "") existing = find_order(store.data, order_no=order_no) if order_no else None if order_no and existing is None: return AgentReply(text=f"没找到订单 {order_no},无法修改。可在订单面板核对订单号。") payload: dict[str, Any] = {} if existing: # 编辑:以现单兜底未给字段 first = (existing.get("items") or [{}])[0] payload.update({ "id": existing["id"], "customerName": existing["customerName"], "customerLevel": existing["customerLevel"], "deliveryDate": existing["deliveryDate"], "priority": existing.get("priority", 5), "status": existing.get("status", "CONFIRMED"), "productId": first.get("productId"), "quantity": first.get("quantity", 1), "isRush": existing.get("isRush", False), "specialRequirements": existing.get("specialRequirements", ""), }) # 覆盖用户明确给出的字段 for k in ("customerName", "customerLevel", "deliveryDate", "priority", "quantity"): if p.get(k) not in (None, ""): payload[k] = p[k] if "isRush" in p: payload["isRush"] = p["isRush"] prod_hint = p.get("productCode") or p.get("productName") if prod_hint: prod = find_product_by_hint(store.data, str(prod_hint)) if prod is None: names = "、".join(m["name"] for m in store.data["materials"] if m["type"] == "FINISHED_PRODUCT") return AgentReply(text=f"没找到成品「{prod_hint}」。可选成品:{names}。") payload["productId"] = prod["id"] missing = [label for key, label in (("customerName", "客户名称"), ("productId", "产品"), ("deliveryDate", "交期"), ("quantity", "数量")) if not payload.get(key)] if missing: verb = "修改" if existing else "新建" return AgentReply(text=f"要{verb}订单还差:{'、'.join(missing)}。\n" "可以一次说清,例如:「新建订单 客户 康尼机电 产品 PDU配电单元 数量 500 交期 2026-08-20」。") try: title, lines = confirmation_for_order_action(store.data, "order.upsert", payload) except ValueError as exc: return AgentReply(text=f"订单信息有误:{exc}") block = harness.stage_confirmation(session_id, "order.upsert", payload, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="order.upsert.stage", target={"type": "SALES_ORDER", "id": payload.get("id") or "NEW"}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 订单写入,需要你确认。", blocks=[block]) def _stage_reset(store: WorldStore, session_id: str, actor: str) -> AgentReply: """把“重置数据”压入门禁(P2:破坏性操作必须确认)。""" block = harness.stage_confirmation( # 生成确认卡 session_id, "data.reset", {}, title="清空并重置演示数据", summary_lines=["将删除全部排产版本/工单/审计并重新播种(不可恢复)"]) return AgentReply(text="重置数据是破坏性操作(P2),需要你确认。", blocks=[block]) def execute_confirmed(store: WorldStore, confirm_id: str, approve: bool, actor: str) -> str: """执行/驳回一条已出卡的 P2 动作(确认卡回传入口;一次性令牌)。 权力等级:P2(本函数是 P2 动作的唯一执行通道,§3.3 门禁放行后)。 Returns: 面向用户的结果文案。 """ pending = harness.take_confirmation(confirm_id) # 取出待确认动作(取后即失效) if pending is None: # 过期/重复点击 return "该确认卡已失效(可能已处理过)。" action, params = pending["action"], pending["params"] # 解构动作 if not approve: # ---- 驳回分支 ---- write_audit(store.data, store.next_id, actor=actor, category="GATE", action=action + ".reject", target=params, power="P2", rationale={"confirmId": confirm_id}, result="DENIED") # 驳回留痕 store.save() # 落盘 return "已驳回,未做任何变更。" if action == "schedule.publish": # ---- 批准:发布版本 ---- v = next(x for x in store.data["scheduleVersions"] if x["id"] == params["versionId"]) # 目标版本 from server.aps_domain.constraints import hard_blocking_conflicts blockers = hard_blocking_conflicts(store.data, version_id=v["id"], track="fixed") if blockers: return (f"发布被硬约束门禁拦截:仍有 {len(blockers)} 项未解决硬约束冲突。" "请先处理冲突或调整约束配置。") # 回滚防线(§3.3 防线 3):P2 写主干前自动·强制建成对快照(回滚锚点) get_checkpoints().create(store.data, label=f"发布前基线 {v['versionNo']}", reason="auto:publish", conversation_note=f"批准发布 {v['versionNo']}") v["status"] = "PUBLISHED" # 置为已发布 from datetime import datetime # 局部导入避免顶部循环 from server.timeutil import fmt_dt # 时间格式化 v["publishedAt"] = fmt_dt(datetime.now()) # 发布时间 for po in store.data["productionOrders"]: # 该版本 PO 推进为已确认 if po["schedulingVersionId"] == v["id"]: po["status"] = "CONFIRMED" write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="schedule.publish", target={"type": "SCHEDULE_VERSION", "id": v["id"]}, power="P2", rationale={"confirmId": confirm_id, "approver": actor}) # 发布留痕(谁批的) store.save() # 主干写入落盘 return f"版本 {v['versionNo']} 已发布 ✅ 生产订单已转入执行准备(审计已留痕)。" if action == "data.reset": # ---- 批准:重置数据 ---- # 回滚防线:重置前自动建档(重置本身也可被撤销) get_checkpoints().create(store.data, label="重置前基线", reason="auto:reset", conversation_note="批准重置数据") store.reset() # 重新播种(含落盘) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="data.reset", target={"type": "WORLD", "id": 0}, power="P2", rationale={"confirmId": confirm_id, "approver": actor}) # 重置留痕 store.save() # 审计落盘 return "数据已重置为种子状态 ✅(重置前状态已自动存档,可回滚)" if action == "checkpoint.rollback": # ---- 批准:回滚到检查点 ---- pair = get_checkpoints().get(params["pairId"]) # 取目标快照(完整世界) if pair is None: # 快照不存在(被淘汰) return "目标检查点不存在(可能已被容量策略淘汰)。" # 回滚防线:回滚前先把"现在"也存档(允许撤销这次回滚——时间旅行可往返 §4.4) get_checkpoints().create(store.data, label="回滚前状态", reason="auto:rollback", conversation_note=f"回滚到 {pair['label']}") store.restore(pair["world"]) # 整体替换主干世界(成对恢复的世界侧) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="checkpoint.rollback", target={"type": "CHECKPOINT", "id": params["pairId"]}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "label": pair["label"]}) # 回滚留痕 store.save() # 落盘 return f"已回滚到检查点【{pair['label']}】({pair['createdAt']})✅ 回滚前状态已自动存档。" if action in ("order.upsert", "order.cancel", "order.complete", "order.delete", "order.clear", "order.approve", "order.reject"): # ---- 批准:订单写入 / 审核 ---- from server.aps_domain.orders import apply_order_action get_checkpoints().create(store.data, label="订单变更前基线", reason=f"auto:{action}", conversation_note=f"批准执行 {action}") # P2 写前自动建档 applied = apply_order_action(store.data, store.next_id, action, params) order = applied["order"] write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action=action, target={"type": "SALES_ORDER", "id": order["id"], "orderNo": order["orderNo"]}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "beforeStatus": applied.get("beforeStatus"), "afterStatus": order["status"], "approvedCount": applied.get("approvedCount"), "rejectedCount": applied.get("rejectedCount")}) store.save() if action == "order.approve": return (f"已批准 ✅ {applied.get('approvedCount', 1)} 条订单 → APPROVED," f"可参与正式排产:{'、'.join(applied.get('orderNos') or [order['orderNo']])}") if action == "order.reject": return (f"已驳回 ✅ {applied.get('rejectedCount', 1)} 条:" f"{'、'.join(applied.get('orderNos') or [order['orderNo']])}") if action == "order.upsert": verb = "已新增" if applied.get("created") else "已更新" return f"{verb}订单 {order['orderNo']} ✅ 后续试排会使用最新订单池。" if action == "order.cancel": return f"订单 {order['orderNo']} 已取消 ✅ 后续试排将不再纳入。" if action == "order.delete": return f"订单 {order['orderNo']} 已删除 ✅ 已从订单池移除。" if action == "order.clear": return (f"订单池已清空 ✅ 销售 {applied.get('clearedOrders', 0)} 条 / " f"采购 {applied.get('clearedPurchase', 0)} / 委外 {applied.get('clearedOutsource', 0)}。" "后续可重新导入或新建订单。") return f"订单 {order['orderNo']} 已完成 ✅ 后续试排将不再纳入。" if action == "rush.apply": # ---- OR-04:采用紧急插单 ---- from server.aps_domain.rush import apply_rush get_checkpoints().create(store.data, label="插单采用前基线", reason="auto:rush.apply", conversation_note="采用紧急插单前自动建档") applied = apply_rush(store.data, store.next_id, params) order = applied["order"] result = applied["result"] write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="rush.apply", target={"type": "SALES_ORDER", "id": order["id"], "orderNo": order["orderNo"]}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "versionNo": result.versionNo, "strategy": applied.get("strategy"), "evalId": params.get("evalId")}) store.save() _LAST_RUSH_EVAL.clear() return (f"插单已采用 ✅ {order['orderNo']}(急单)已入池;" f"草稿版本 {result.versionNo} · 订单 {result.orderCount} · " f"冲突 {result.conflictCount} · 延迟 {round(result.totalTardiness)}h。" f"可用检查点回滚到采用前。") if action in ("forecast.upsert", "forecast.delete", "forecast.convert"): from server.aps_domain.forecast import apply_forecast_action get_checkpoints().create(store.data, label="预测变更前基线", reason=f"auto:{action}", conversation_note=f"批准执行 {action}") applied = apply_forecast_action(store.data, store.next_id, action, params) fc = applied["forecast"] write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action=action, target={"type": "FORECAST_ORDER", "id": fc.get("id"), "forecastNo": fc.get("forecastNo")}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "beforeStatus": applied.get("beforeStatus"), "convertedOrderNo": applied.get("convertedOrderNo")}) store.save() if action == "forecast.convert": return (f"预测已转正 ✅ {fc.get('forecastNo')} → 销售订单 {applied.get('convertedOrderNo')}," "可参与正式排产。") if action == "forecast.delete": return f"预测 {fc.get('forecastNo')} 已删除 ✅" verb = "已新建" if applied.get("created") else "已更新" return f"{verb}预测 {fc.get('forecastNo')} ✅(默认不进正式排产,可说「预测纳入试排」)。" if action == "data.import": # ---- 批准:批量导入 ---- from server.aps_domain.intake import apply_import get_checkpoints().create(store.data, label="批量导入前基线", reason="auto:data.import", conversation_note=f"批准导入 {params.get('kind')}") applied = apply_import(store.data, store.next_id, params) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="data.import", target={"type": "IMPORT", "id": params.get("kind")}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "count": applied["count"]}) store.save() label = "订单" if applied["kind"] == "orders" else "物料" sample = "、".join(applied["created"][:5]) more = f" 等 {applied['count']} 条" if applied["count"] > 5 else f"共 {applied['count']} 条" return f"已导入{label} ✅ {sample}{more}。" if action == "flex.site.load": # ---- 批准:现场生产路线 ---- from server.importers import import_site_excel get_checkpoints().create(store.data, label="现场路线导入前基线", reason="auto:flex.site.load", conversation_note="批准加载完整生产路线") meta = import_site_excel( store.data, profile_id=params.get("profileId") or "kangni", route_path=params.get("routePath") or None, data_dir=params.get("dataDir") or None, include_sibling_orders=params.get("includeSiblings", False) is True, station_count=int(params.get("stationCount") or 0) or None, ) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="flex.site.load", target={"type": "FLEX_SITE", "id": meta.get("primaryOrder")}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "meta": meta}) store.save() return (f"现场生产路线已加载 ✅ 主单 {meta.get('primaryOrder')}," f"订单 {meta.get('orderCount')} / 工序 {meta.get('operationCount')} / BOM {meta.get('bomCount')}。" f"演示 flex* 已清空。可说「柔性排产」。") if action == "import.commit": # ---- 批准:Excel/CSV 入库 ---- from server.aps_domain.importers import apply_import_commit get_checkpoints().create(store.data, label="文件导入前基线", reason="auto:import.commit", conversation_note=f"批准导入 {params.get('filename')}") applied = apply_import_commit(store.data, store.next_id, params.get("batches") or []) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="import.commit", target={"type": "IMPORT", "id": params.get("filename")}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "summary": applied["summary"]}) store.save() detail = "、".join(f"{k}×{v}" for k, v in applied["summary"].items()) or "无行" return f"文件导入完成 ✅ 共 {applied['total']} 条({detail})。" if action == "folder.schedule": from server.aps_domain.importers import apply_import_commit from server.aps_domain.folder_pack import analyze_work_dir get_checkpoints().create(store.data, label="工程目录导入前基线", reason="auto:folder.schedule", conversation_note="批准导入工程目录并试排") batches = params.get("batches") or [] sql_already = bool(params.get("sqlApplied")) if not batches and not sql_already: report = analyze_work_dir( store.data, params.get("sessionId"), force_sql_replace=True) batches = report.get("batches") or [] sql_already = bool(report.get("sqlApplied")) applied = {"total": 0, "summary": {}} if batches: applied = apply_import_commit(store.data, store.next_id, batches) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="folder.schedule.import", target={"type": "FOLDER_PACK", "id": params.get("workDir") or "workDir"}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "summary": applied.get("summary"), "sqlApplied": sql_already}) store.save() from server.aps_domain.readiness import check_readiness, readiness_text ready = check_readiness(store.data) detail = "、".join(f"{k}×{v}" for k, v in (applied.get("summary") or {}).items()) or ( "SQL 已入库" if sql_already else "无行" ) head = f"工程目录已导入 ✅({detail})。" # 有订单+工艺+设备即可开排(工时推断/缺 BOM 不拦死) has_core = ( bool(store.data.get("flexOrders")) and bool(store.data.get("flexRoutings")) and any(e.get("status") == "RUNNING" for e in (store.data.get("flexEquipment") or [])) ) if ready["summary"].get("ready", 0) == 0 and not has_core: return head + "\n\n导入后仍不具备开排条件:\n" + readiness_text(ready) from server.aps_domain.flex import run_flex_schedule result = run_flex_schedule( store, sort_mode=params.get("sortMode") or "BOTTLENECK", actor=actor) ver = (result or {}).get("versionNo") or "" return (head + f"\n已按目录数据试排一版:**{ver}**" f"(虚拟产线 {(result or {}).get('vlCount', 0)} · " f"冲突 {(result or {}).get('conflictCount', 0)})。" "可在右侧柔性工作台看甘特,或再说「查看冲突」。") if action == "flex.reschedule": from server.aps_domain.flex import reschedule_by_level get_checkpoints().create(store.data, label=f"{params.get('level', 'L2')} 重排前基线", reason="auto:flex.reschedule", conversation_note=f"批准 {params.get('level')} 重排") r = reschedule_by_level(store, level=params.get("level") or "L2", sort_mode=params.get("sortMode"), actor=actor) sch = r["schedule"] # EX-03:若从冲突修复卡进入,标记原冲突已解决 cid = params.get("resolveConflictId") if cid is not None: for cf in store.data.get("flexConflicts", []): if cf.get("id") == cid: cf["isResolved"] = True cf["resolutionAction"] = f"flex.reschedule:{r['level']}" cf["resolvedBy"] = actor break write_audit(store.data, store.next_id, actor=actor, category="GATE", action="flex.reschedule.approve", target={"type": "FLEX_VERSION", "id": sch.get("versionId")}, power="P2", rationale={"confirmId": confirm_id, "level": r["level"], "frozenWo": r.get("frozenWo"), "mutableOrders": r.get("mutableOrders"), "resolveConflictId": cid}) store.save() return (f"{r['level']} 重排已执行 ✅ 版本 {sch.get('versionNo')}:" f"冻结 {r.get('frozenWo', 0)} / 重排 {r.get('mutableOrders', 0)} 单," f"冲突 {sch.get('conflictCount', 0)}。") if action == "flex.adjust.commit": from server.aps_domain.adjust import commit_flex_adjust get_checkpoints().create(store.data, label="甘特调程前基线", reason="auto:flex.adjust.commit", conversation_note=f"批准拖拽工单 #{params.get('woId')}") r = commit_flex_adjust(store, int(params["woId"]), str(params["newStart"]), actor=actor) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="flex.adjust.approve", target={"type": "FLEX_VERSION", "id": r.get("versionId")}, power="P2", rationale={"confirmId": confirm_id, "woId": params.get("woId")}) store.save() return r.get("message") or "调程已执行 ✅" if action == "schedule.adjust.commit": from server.aps_domain.adjust import commit_fixed_adjust get_checkpoints().create(store.data, label="固定甘特调程前基线", reason="auto:schedule.adjust.commit", conversation_note=f"批准拖拽工单 #{params.get('woId')}") r = commit_fixed_adjust(store, int(params["woId"]), str(params["newStart"]), actor=actor) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="schedule.adjust.approve", target={"type": "SCHEDULE_VERSION", "id": r.get("versionId")}, power="P2", rationale={"confirmId": confirm_id, "woId": params.get("woId")}) store.save() return r.get("message") or "调程已执行 ✅" if action == "sap.sync.inbound": from server.aps_domain.sap_sync import apply_inbound get_checkpoints().create(store.data, label="SAP 入站前基线", reason="auto:sap.sync.inbound", conversation_note="批准 SAP 入站同步") r = apply_inbound(store, actor=actor) return r.get("message") or "SAP 入站完成 ✅" if action == "sap.sync.outbound": from server.aps_domain.sap_sync import apply_outbound get_checkpoints().create(store.data, label="SAP 出站前基线", reason="auto:sap.sync.outbound", conversation_note="批准 SAP 出站回写") r = apply_outbound(store, actor=actor) return r.get("message") or "SAP 出站完成 ✅" if action == "mes.dispatch": from server.aps_domain.mes import apply_dispatch get_checkpoints().create(store.data, label="MES 下发前基线", reason="auto:mes.dispatch", conversation_note=f"批准 MES 下发 {params.get('track')}") r = apply_dispatch(store, track=params.get("track") or "flex", actor=actor) return r.get("message") or "MES 下发完成 ✅" if action == "mrp.release": # ---- 批准:MRP 建议单下达 ---- from server.aps_domain.mrp import apply_mrp_release get_checkpoints().create(store.data, label="MRP 下达前基线", reason="auto:mrp.release", conversation_note="批准下达 MRP 建议单") # P2 写前自动建档 applied = apply_mrp_release(store.data, params.get("orderNo"), params.get("kind", "all")) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="mrp.release", target={"type": "MRP", "id": params.get("orderNo") or "ALL"}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "purchase": applied["purchaseCount"], "outsource": applied["outsourceCount"]}) store.save() return (f"已下达 ✅ 采购单 {applied['purchaseCount']} 条 / 委外单 {applied['outsourceCount']} 条转为正式(RELEASED)," "可在订单面板「分解建议」查看状态。") if action.startswith("master."): # ---- 批准:主数据写入(白名单见 masterdata.MASTER_ACTIONS) ---- from server.aps_domain.masterdata import apply_master_action get_checkpoints().create(store.data, label="主数据变更前基线", reason=f"auto:{action}", conversation_note=f"批准执行 {action}") # P2 写前自动建档 applied = apply_master_action(store.data, store.next_id, action, params) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action=action, target={"type": f"MASTER_{applied['kind']}", "id": applied["id"], "name": applied["name"]}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "before": applied.get("beforeStatus", applied.get("beforeStock")), "after": applied.get("afterStatus", applied.get("afterStock"))}) store.save() kind_cn = { "LINE": "产线", "MATERIAL": "物料", "MAINTENANCE": "维保计划", "BOM_ITEM": "BOM 明细", "BOM": "BOM", "ROUTING_STEP": "工艺步骤", "ROUTING": "工艺路线", "OPERATION": "工序", "LINE_PRODUCT": "产线绑定", "MASTER_CLEAR": "主数据清理", }.get(applied["kind"], applied["kind"]) if applied["kind"] == "MASTER_CLEAR": detail = ", ".join(applied.get("cleared") or applied.get("restored") or []) or "无数据" return f"主数据已按【{applied['name']}】清空 ✅ {detail}(变更前已自动建档)。" return f"{kind_cn}【{applied['name']}】主数据已更新 ✅ 只影响后续新排产版本(变更前已自动建档)。" if action == "params.update": # ---- 批准:排产参数(OR-02) ---- from server.aps_domain.params import apply_params_update get_checkpoints().create(store.data, label="排产参数变更前基线", reason="auto:params.update", conversation_note="批准更新排产参数") applied = apply_params_update(store.data, params) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="params.update", target={"type": "SCHEDULE_PARAMS", "id": "scheduleParams"}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "before": applied["before"].get("customerLevelWeights"), "after": applied["after"].get("customerLevelWeights"), "reset": applied.get("reset")}) store.save() lw = applied["after"]["customerLevelWeights"] return (f"排产参数已更新 ✅ 客户等级权重 VIP={lw['VIP']} / A={lw['A']} / B={lw['B']} / C={lw['C']}。" "只影响后续新排产版本(变更前已自动建档)。") if action == "constraint.profile.save": # ---- 批准:约束剖面(SC-04) ---- from server.aps_domain.constraints import apply_profile_save get_checkpoints().create(store.data, label="约束配置变更前基线", reason="auto:constraint.profile.save", conversation_note="批准更新约束配置") applied = apply_profile_save(store.data, params) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="constraint.profile.save", target={"type": "CONSTRAINT_PROFILE", "id": applied["id"]}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "reset": applied.get("reset")}) store.save() enabled = sum(1 for c in applied["after"]["constraints"] if c["enabled"]) hard_n = sum(1 for c in applied["after"]["constraints"] if c["kind"] == "hard" and c["enabled"]) return (f"约束配置已更新 ✅ 启用 {enabled} 条 / 其中硬约束 {hard_n} 条。" "硬约束违反将阻止发布(变更前已自动建档)。") if action == "sop.apply": # ---- 批准:SOP 规则包(IND-02) ---- from server.aps_domain.sop_rules import apply_sop_pack, compile_sop_by_query pack = params.get("pack") if not isinstance(pack, dict): compiled = compile_sop_by_query(get_knowledge().assets, params.get("query")) if not compiled.get("ok"): return compiled.get("error") or "SOP 编译失败" pack = compiled["pack"] get_checkpoints().create(store.data, label="SOP规则包应用前基线", reason="auto:sop.apply", conversation_note="批准应用 SOP→约束") applied = apply_sop_pack(store.data, pack) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="sop.apply", target={"type": "RULE_PACK", "id": applied["packId"]}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "applied": applied.get("applied"), "source": pack.get("source")}) store.save() note = ";".join(applied.get("notes") or []) or "无额外流程提示" return ( f"已应用 SOP 规则包 ✅ {pack.get('title')}({applied['packId']})。\n" f"生效项:{', '.join(applied.get('applied') or [])}\n{note}" ) if action == "knowledge.import": from server.knowledge.embedding import index_units from server.knowledge.ingest import apply_knowledge_import, preview_ingest path = params.get("path") if not path: return "缺少文件路径,无法导入知识文档" try: preview = preview_ingest( params.get("filename") or path, path=path, kind=params.get("kind") or "sop", title=params.get("title"), ) applied = apply_knowledge_import(preview) # 尝试建向量索引(无嵌入后端则跳过) from server.knowledge.assets import get_knowledge index_units(get_knowledge().iter_search_units()) except (FileNotFoundError, ValueError) as exc: return str(exc) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="knowledge.import", target={"type": "KNOWLEDGE", "id": applied["assetId"]}, power="P2", rationale={"confirmId": confirm_id, "chunkCount": applied.get("chunkCount"), "title": applied.get("title")}) store.save() return (f"知识文档已入库 ✅ 《{applied['title']}》{applied['version']}," f"切块 {applied['chunkCount']} 段(资产 {applied['assetId']})。") if action == "skill.register": from server.agent_core.skills import get_skills payload = { "skill_id": params.get("skill_id") or params.get("skillId"), "name": params.get("name") or params.get("skill_id") or params.get("skillId"), "endpoint": params.get("endpoint"), "auth": params.get("auth") or "", "track": params.get("track") or "flex", "enabled": params.get("enabled", True), "description": params.get("description") or "", } saved = get_skills().upsert(payload) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="skill.register", target={"type": "SKILL", "id": saved["skill_id"]}, power="P2", rationale={"confirmId": confirm_id, "endpoint": saved.get("endpoint")}) store.save() return f"已登记算法 Skill ✅ {saved['skill_id']}({saved['name']})→ {saved['endpoint']}" if action == "skill.enable": from server.agent_core.skills import get_skills sid = params.get("skill_id") or params.get("skillId") enabled = bool(params.get("enabled", True)) saved = get_skills().set_enabled(sid, enabled) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="skill.enable", target={"type": "SKILL", "id": sid}, power="P2", rationale={"confirmId": confirm_id, "enabled": enabled}) store.save() return f"算法 Skill {sid} 已{'启用' if enabled else '停用'} ✅" if action == "flex.time.update": # ---- 批准:工时维护 ---- op_code = str(params.get("operationCode") or "") op_codes = set(params.get("operationCodes") or ([op_code] if op_code else [])) pc = str(params.get("productCode") or "") std_min = float(params.get("stdMin") or 0) source = params.get("source") or "实测" hits = 0 for step in store.data.get("flexRoutings") or []: if op_codes and step.get("operationCode") not in op_codes: continue if pc and step.get("productCode") != pc: continue step["stdTimePerUnit"] = std_min step["stdTimeSource"] = source hits += 1 # 同步工序库标准工时(主数据页显示口径) for op in store.data.get("flexOperations") or []: if op.get("code") in op_codes and not pc: op["stdTimeMin"] = std_min write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="flex.time.update", target={"type": "ROUTING_TIME", "id": f"{pc or '*'}/{op_code or '*'}"}, power="P2", rationale={"confirmId": confirm_id, "stdMin": std_min, "hits": hits, "source": source}) store.save() ops_label = "、".join(sorted(op_codes)) or "全部工序" if not hits: return f"未找到匹配的路线步骤(产品={pc or '任意'},工序={ops_label}),未做修改。" return (f"工时已更新 ✅ {pc or '全部产品'} × {ops_label} → " f"{std_min} 分钟/件(来源={source},{hits} 条路线步骤)。") if action == "routing.template.apply": # ---- 批准:模板生成工艺路线 ---- from server.knowledge.routing_templates import apply_template_to_product get_checkpoints().create(store.data, label="模板生成路线前基线", reason="auto:routing.template.apply", conversation_note=f"模板 {params.get('templateCode')} → {params.get('productCode')}") try: applied = apply_template_to_product( store.data, str(params.get("templateCode") or ""), str(params.get("productCode") or ""), str(params.get("productName") or "")) except ValueError as exc: return str(exc) write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="routing.template.apply", target={"type": "ROUTING", "id": applied["productCode"]}, power="P2", rationale={"confirmId": confirm_id, "template": applied["template"], "steps": applied["steps"], "assetId": applied.get("assetId")}) store.save() return (f"工艺路线已生成 ✅ {applied['productCode']} ← 模板「{applied['templateName']}」" f"({applied['steps']} 步,工时标「模板」,出处 {applied.get('assetId') or '内置'})。" f"可说「数据齐备度」复查后「柔性排产」。") return "未知动作,已忽略。" # 白名单外兜底(理论不可达) # ---------------- 柔性排产(M5:能力池 + 虚拟产线 / 瓶颈产能 / 交期承诺) ---------------- _FLEX_MODE_CN = {"ASC": "正排(交期优先)", "DESC": "倒排(交期倒推)", "BOTTLENECK": "瓶颈锚"} def _flex_product_name(store: WorldStore, code: str) -> str: """物料/产品编码 → 中文名(回复文案友好化)。""" m = next((x for x in store.data.get("flexMaterials", []) if x["code"] == code), None) return f"{m['name']}({code})" if m else code def _resolve_order_reference(store: WorldStore, session_id: str, intent: IntentResult) -> IntentResult: """把 orderRef=last / 口令中的订单号 解析为 flex orderIds 或 orderNo。""" from server.agent_core.session_focus import get_last_order_no, set_focus params = dict(intent.params or {}) wanted_ref = params.get("orderRef") in ("last", "this", "focus") order_no = str(params.get("orderNo") or "").strip() if not order_no and wanted_ref: order_no = get_last_order_no(session_id) or "" if not order_no: # 世界里只有一单时自动锁定(现场常见:刚导入的主单) flex = store.data.get("flexOrders") or [] sales = store.data.get("salesOrders") or [] if len(flex) == 1: order_no = str(flex[0].get("orderNo") or "") elif len(sales) == 1: order_no = str(sales[0].get("orderNo") or "") if order_no: params.pop("_missingFocus", None) params["orderNo"] = order_no set_focus(session_id, orderNo=order_no) fo = next((o for o in (store.data.get("flexOrders") or []) if str(o.get("orderNo")) == order_no), None) if fo and intent.intent == "flex.schedule": params["orderIds"] = [fo["id"]] elif wanted_ref: # 「这个订单」但会话未锁定、且池内多单 → 不瞎排全池 params["_missingFocus"] = True return IntentResult(intent=intent.intent, params=params, confidence=intent.confidence, source=intent.source) def _run_flex(store: WorldStore, intent: IntentResult, actor: str) -> AgentReply: """触发柔性排产并回执(短文案 + 结构化 flex-schedule 块:KPI + 虚拟产线表 + 瓶颈)。""" from server.aps_domain.flex import run_flex_schedule from server.timeutil import parse_dt mode = intent.params.get("sortMode") or "BOTTLENECK" window = intent.params.get("window") order_ids = intent.params.get("orderIds") if isinstance(order_ids, list): order_ids = [int(x) for x in order_ids if str(x).isdigit() or isinstance(x, int)] else: order_ids = None engine = str(intent.params.get("engine") or "").upper() use_ext = bool(engine.startswith("EXTERNAL") or intent.params.get("skillId") or intent.params.get("useExternal")) if use_ext: from server.engines.external_engine import run_external_flex try: result = run_external_flex( store, skill_id=intent.params.get("skillId"), order_ids=order_ids, actor=actor, ) except ValueError as exc: return AgentReply(text=str(exc)) write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="flex.schedule.external", target={"type": "FLEX", "id": result.get("versionNo")}, power="P1", rationale={"skillId": result.get("skillId"), "runId": result.get("runId")}) store.save() else: result = run_flex_schedule(store, sort_mode=mode, order_ids=order_ids or None, actor=actor, window=window) # P1 mode_cn = _FLEX_MODE_CN.get(result["sortMode"], result["sortMode"]) if result.get("sortMode") == "EXTERNAL" or use_ext: mode_cn = f"外部算法({result.get('skillId') or 'skill'})" win_cn = {"short": "短窗2h", "mid": "中窗2d", "long": "长窗7d", "full": "全量"}.get( result.get("window") or "full", result.get("window") or "full") vls = [v for v in store.data["flexVirtualLines"] if v["versionId"] == result["versionId"]] orders_by_no = {o["orderNo"]: o for o in store.data["flexOrders"]} def _on_time(v) -> bool: # 完工是否不晚于交期 o = orders_by_no.get(v["orderNo"]) return bool(o and v["plannedEnd"] and parse_dt(v["plannedEnd"]) <= parse_dt(o["dueDate"] + " 18:00")) wos_by_vl: dict[int, list] = {} # 工单按虚拟产线归集(下钻依据) for w in store.data["flexWorkOrders"]: if w["versionId"] == result["versionId"]: wos_by_vl.setdefault(w["vlId"], []).append(w) def _steps(vl_id: int) -> list[dict]: # 单条虚拟产线的工序级明细 rows = sorted(wos_by_vl.get(vl_id, []), key=lambda w: w["seq"]) return [{ "seq": w["seq"], "operationName": w["operationName"], "equipmentName": w["equipmentName"], "equipmentCode": w["equipmentCode"], "zone": w.get("zone"), "moldCode": w.get("moldCode"), "changeoverMin": w.get("changeoverMin", 0), "moveMin": w.get("moveMin", 0), "runMin": w.get("runMin", 0), "start": w["plannedStartTime"], "end": w["plannedEndTime"], "isBottleneck": w.get("isBottleneck", False), } for w in rows] lines = [{ "vlNo": v["vlNo"], "orderNo": v["orderNo"], "product": _flex_product_name(store, v["productCode"]), "quantity": v["quantity"], "start": v["plannedStart"], "end": v["plannedEnd"], "due": (orders_by_no.get(v["orderNo"]) or {}).get("dueDate"), "priority": (orders_by_no.get(v["orderNo"]) or {}).get("priority"), "stepCount": len(wos_by_vl.get(v["id"], [])), "onTime": _on_time(v), "status": "scheduled", "steps": _steps(v["id"]), # 工单级依据(点开可见) } for v in vls] # 未排出的订单也上表,避免「有 KPI、表却空」 scheduled_nos = {ln["orderNo"] for ln in lines} for c in store.data.get("flexConflicts") or []: if c.get("versionId") != result["versionId"]: continue ono = c.get("orderNo") if not ono or ono in scheduled_nos: continue o = orders_by_no.get(ono) or {} lines.append({ "vlNo": "—", "orderNo": ono, "product": _flex_product_name(store, o.get("productCode") or ""), "quantity": o.get("quantity"), "start": None, "end": None, "due": o.get("dueDate"), "priority": o.get("priority"), "stepCount": 0, "onTime": False, "status": "blocked", "steps": [], "blockReason": c.get("description") or c.get("conflictType"), }) scheduled_nos.add(ono) lines.sort(key=lambda x: (0 if x.get("status") == "scheduled" else 1, x.get("due") or "", x.get("orderNo") or "")) conflicts = [{ "type": c["conflictType"], "orderNo": c.get("orderNo"), "severity": c.get("severity"), "description": c.get("description"), "suggestion": c.get("suggestedSolution"), } for c in store.data["flexConflicts"] if c["versionId"] == result["versionId"]] block = UIBlock( blockId=f"flex-sched-{result['versionId']}", type="flex-schedule", props={ "versionNo": result["versionNo"], "modeCn": mode_cn, "stats": {"vlCount": result["vlCount"], "woCount": result["woCount"], "makespan": result.get("makespan"), "onTimeCount": result.get("onTimeCount", 0), "conflictCount": result["conflictCount"], "utilization": round(result["avgUtilization"] * 100)}, "bottleneck": [{"name": b["operationName"], "count": b["poolEquipmentCount"]} for b in result.get("bottleneck", [])], "lines": lines, "conflicts": conflicts, "orderNo": intent.params.get("orderNo"), "downloadUrl": None, "filename": None, "emptyHint": None if lines else "本版没有排出虚拟产线:多半是订单缺工艺/设备。请先「分析一下这个项目」。", }) # 同步生成 Excel 工作计划表,排产结果块可直接下载 try: plan = build_report(store.data, "plan", order_no=intent.params.get("orderNo")) if plan.get("reportId") and plan.get("xlsxBytes"): saved = persist_report_xlsx(plan["reportId"], plan["xlsxBytes"], plan.get("filename")) block.props["downloadUrl"] = f"/api/reports/files/{plan['reportId']}" block.props["filename"] = saved["filename"] block.props["reportId"] = plan["reportId"] except Exception: pass deferred = result.get("deferredCount") or 0 focus_no = intent.params.get("orderNo") focus_hint = f"订单 {focus_no} · " if focus_no and order_ids else "" dl_hint = "点结果块「下载 Excel 工作计划表」可导出完整工序计划。" if block.props.get("downloadUrl") else "可说「生成排产方案报告」下载。" text = (f"柔性排产完成 ✅ {focus_hint}版本 {result['versionNo']}({mode_cn}·{win_cn}):" f"{result['vlCount']} 条虚拟产线 / {result['woCount']} 个工单,冲突 {result['conflictCount']} 项" + (f",窗外延期 {deferred}" if deferred else "") + f"。{dl_hint}") return AgentReply(text=text, blocks=[block], commands=[ # 联动:切柔性甘特 + 重拉世界数据 ViewportCommand(cmd="viewport.mode", params={"mode": "flex"}, issuedBy="LLM"), ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM")]) def _flex_reschedule(store: WorldStore, intent: IntentResult, session_id: str, actor: str) -> AgentReply: """分级重排(DY-01):L2/L3/L4 一律出 P2 确认卡。""" from server.aps_domain.flex import preview_reschedule from server.agent_core import harness level = str(intent.params.get("level") or "L2").upper() title, lines = preview_reschedule(store, level) params = {"level": level, "sortMode": intent.params.get("sortMode")} block = harness.stage_confirmation(session_id, "flex.reschedule", params, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="flex.reschedule.stage", target={"type": "FLEX", "id": level}, power="P2", rationale={"confirmId": block.props["confirmId"], "level": level}) store.save() return AgentReply(text=f"{title} 属于 P2,需要你确认后执行。", blocks=[block]) def _flex_swap(store: WorldStore, intent: IntentResult, actor: str) -> AgentReply: """L1 局部换机:池内备机接手。""" from server.aps_domain.flex import local_swap_equipment code = intent.params.get("equipmentCode") wo_id = intent.params.get("woId") try: r = local_swap_equipment( store, equipment_code=code, wo_id=int(wo_id) if wo_id else None, target_code=intent.params.get("targetCode"), mark_fault=bool(intent.params.get("markFault", True)), actor=actor) except ValueError as exc: return AgentReply(text=str(exc)) lines = [f"{s['orderNo']} {s['operationCode']}:{s['from']} → {s['to']}" for s in r["swapped"][:5]] more = f" 等 {len(r['swapped'])} 条" if len(r["swapped"]) > 5 else "" fail_n = len(r.get("failed") or []) text = (f"L1 局部换机完成 ✅ {len(r['swapped'])} 条工单改挂备机" + (f";{fail_n} 条无备机" if fail_n else "") + (f";设备 {r['faulted']} 已标故障" if r.get("faulted") else "") + (":" + ";".join(lines) + more if lines else "。")) return AgentReply(text=text, commands=[ ViewportCommand(cmd="viewport.mode", params={"mode": "flex"}, issuedBy="LLM"), ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM"), ]) def _flex_capacity(store: WorldStore) -> AgentReply: """瓶颈产能法:短文案 + flex-capacity 块(各池日产能条形 + 瓶颈标注)。""" from server.aps_domain.flex import capacity_analysis cap = capacity_analysis(store.data) if not cap["pools"]: return AgentReply(text="尚无可用的柔性产能池数据(无在运设备)。") max_daily = max((p["dailyCapacity"] for p in cap["pools"]), default=1) or 1 pools = [{ "name": p["operationName"], "equipmentCount": p["equipmentCount"], "dailyCapacity": p["dailyCapacity"], "workDays": p["workDays"], "isBottleneck": p["isBottleneck"], "pct": round(p["dailyCapacity"] / max_daily * 100), # 相对最大池的产能占比(条形宽度) } for p in cap["pools"]] bp = cap["bottleneckPool"] block = UIBlock(blockId="flex-cap", type="flex-capacity", props={"bottleneckName": bp["operationName"], "bottleneckDaily": bp["dailyCapacity"], "pools": pools}) text = (f"瓶颈产能评估完成:限制性瓶颈是【{bp['operationName']}】," f"全厂节拍上界≈{bp['dailyCapacity']}件/天(按能力池实时算)。") return AgentReply(text=text, blocks=[block], commands=[ # EX-10:切产能池仪表盘 + 刷新世界 ViewportCommand(cmd="viewport.mode", params={"mode": "pool"}, issuedBy="LLM"), ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM")]) def _flex_simulate_due(store: WorldStore, intent: IntentResult) -> AgentReply: """交期承诺模拟:短文案 + flex-due 块(乐观/预计/悲观 + 缺口)。""" from server.aps_domain.flex import simulate_due code = str(intent.params.get("productCode") or "") qty = int(intent.params.get("quantity") or 0) if not code or qty <= 0: return AgentReply(text="请告诉我产品与数量,例如:「PDU 500套什么时候能交」或「高压线束 800件多久能做完」。") r = simulate_due(store, code, qty, sort_mode=intent.params.get("sortMode")) block = UIBlock(blockId=f"flex-due-{code}", type="flex-due", props={"productName": _flex_product_name(store, code), "quantity": qty, "optimistic": r.get("optimisticFinish"), "expected": r.get("expectedFinish"), "pessimistic": r.get("pessimisticFinish"), "gaps": r.get("gaps") or [], "bottleneck": [{"name": b["operationName"], "count": b["poolEquipmentCount"]} for b in r.get("bottleneck", [])]}) gaps_txt = ";".join(g["message"] for g in (r.get("gaps") or [])[:2]) text = (f"交期承诺模拟完成:{_flex_product_name(store, code)} × {qty}" f"(乐观 {r.get('optimisticFinish') or '—'} / 预计 {r.get('expectedFinish') or '—'} / " f"悲观 {r.get('pessimisticFinish') or '—'})。{gaps_txt}") return AgentReply(text=text, blocks=[block]) def _flex_compare(store: WorldStore) -> AgentReply: from server.aps_domain.analytics import matrix_from_flex_rows from server.aps_domain.flex import compare_sort_modes cmp = compare_sort_modes(store, compress_due=True) block = UIBlock(blockId="flex-compare", type="flex-compare", props={"rows": cmp["rows"], "hint": cmp["hint"], "compressedDue": cmp["compressedDue"], "table": matrix_from_flex_rows(cmp["rows"], hint=cmp.get("hint"))}) best = min(cmp["rows"], key=lambda r: (r["totalTardiness"], -r["onTimeCount"])) text = (f"三模式对比完成(已压缩交期制造压力)。当前压力下较优:" f"【{best['label']}】延迟 {best['totalTardiness']}h、准时 {best['onTimeCount']}/{best['orderCount']}。" "已打开对比表视口。") return AgentReply( text=text, blocks=[block], commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "compare"}, issuedBy="LLM")]) def _flex_rush(store: WorldStore, intent: IntentResult, actor: str) -> AgentReply: from server.aps_domain.flex import insert_rush_order code = str(intent.params.get("productCode") or "HV-HARNESS") qty = int(intent.params.get("quantity") or 50) try: r = insert_rush_order(store, code, qty, due_date=intent.params.get("dueDate"), priority=int(intent.params.get("priority") or 1), actor=actor) except ValueError as exc: return AgentReply(text=str(exc)) o = r["order"] s = r["schedule"] text = (f"急单 {o['orderNo']} 已插入({_flex_product_name(store, code)} × {qty},交期 {o['dueDate']})," f"瓶颈锚重排完成:版本 {s['versionNo']},虚拟产线 {s['vlCount']}," f"急单完工 {r.get('plannedEnd') or '—'},冲突 {s['conflictCount']}。") return AgentReply(text=text, commands=[ ViewportCommand(cmd="viewport.mode", params={"mode": "flex"}, issuedBy="LLM"), ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM"), ]) def _flex_fault(store: WorldStore, intent: IntentResult, actor: str) -> AgentReply: from server.aps_domain.flex import apply_equipment_fault code = str(intent.params.get("equipmentCode") or "PRESS-01") status = str(intent.params.get("status") or "MAINTENANCE").upper() try: r = apply_equipment_fault(store, code, status=status, reschedule=True, actor=actor) except ValueError as exc: return AgentReply(text=str(exc)) eq = r["equipment"] bn_before = r.get("capacityBefore") or {} bn_after = r.get("capacityAfter") or {} no_cap = [c for c in r.get("conflicts") or [] if c.get("conflictType") == "NO_CAPABILITY"] level = r.get("responseLevel") text = (f"设备 {eq['code']}({eq['name']}){eq['beforeStatus']} → {eq['afterStatus']}。" f"瓶颈日产能 {bn_before.get('dailyCapacity', '—')} → {bn_after.get('dailyCapacity', '—')} 件/天。") if level == "L1": n = len((r.get("swap") or {}).get("swapped") or []) text += f" 已按 L1 局部换机处理({n} 条工单改挂备机,他单不动)。" elif no_cap: text += f" 出现 {len(no_cap)} 条无能力设备冲突(单点瓶颈停摆风险)。" elif eq["afterStatus"] == "MAINTENANCE" and "PRESS" in eq["code"]: text += " 压接池仍有冗余;L1 不可用时已缩池重排。" s = r.get("schedule") or {} if s: text += f" 已重排版本 {s.get('versionNo')},冲突 {s.get('conflictCount', 0)}。" return AgentReply(text=text, commands=[ ViewportCommand(cmd="viewport.mode", params={"mode": "pool" if level != "L1" else "flex"}, issuedBy="LLM"), ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM"), ]) def _conflict_list(store: WorldStore, intent: IntentResult) -> AgentReply: from server.aps_domain.conflicts import list_conflict_center from server.contracts import UIBlock scope = str(intent.params.get("scope") or "flex") data = list_conflict_center(store.data, scope=scope) if not data["rows"]: return AgentReply(text="当前最新版本无未解决冲突 ✅") lines = [] for r in data["rows"][:8]: top = (r["fixes"][0]["label"] if r.get("fixes") else "—") lines.append(f"#{r['id']} [{r['severity']}] {r['conflictType']}:{r['description']} → {top}") more = f"\n…共 {data['total']} 条" if data["total"] > 8 else "" block = UIBlock( blockId="conflict-center", type="conflict-center", props={"total": data["total"], "critical": data["critical"], "major": data["major"], "rows": data["rows"][:20]}) return AgentReply( text=(f"冲突中心:共 {data['total']} 条(危急 {data['critical']} / 重大 {data['major']})。\n" + "\n".join(lines) + more + "\n可在柔性工作台「冲突」页一键修复。"), blocks=[block], commands=[ViewportCommand(cmd="viewport.highlight", params={"what": "conflict"}, issuedBy="LLM")], ) def _conflict_resolve(store: WorldStore, intent: IntentResult, session_id: str, actor: str) -> AgentReply: from server.aps_domain.conflicts import apply_conflict_fix, list_conflict_center from server.contracts import UIBlock cid = intent.params.get("conflictId") action = intent.params.get("action") if not cid: data = list_conflict_center(store.data, scope="flex") if not data["rows"]: return AgentReply(text="没有可修复的冲突。") row = data["rows"][0] fixes = ";".join(f["label"] for f in row["fixes"][:3]) return AgentReply(text=f"建议先处理 #{row['id']}:{row['description']}\n可选:{fixes}") try: r = apply_conflict_fix(store, int(cid), str(action or "note"), params=intent.params.get("fixParams") or {}, actor=actor, session_id=session_id) except (ValueError, PermissionError) as exc: return AgentReply(text=str(exc)) blocks = [] if r.get("block"): blocks.append(UIBlock.model_validate(r["block"])) cmds = [ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM")] if r.get("applied"): cmds.insert(0, ViewportCommand(cmd="viewport.mode", params={"mode": "flex"}, issuedBy="LLM")) return AgentReply(text=r.get("message") or "已处理", blocks=blocks, commands=cmds) async def handle_intent(store: WorldStore, session_id: str, intent: IntentResult, actor: str = "planner") -> AgentReply: """意图 → 回复 的总编排(gateway 调用;P0/P1 直通,P2 出卡)。""" name = intent.intent # 意图名 # 解析「这个订单」等指代 → 具体订单号 / flex orderIds if name in ("flex.schedule", "schedule.run", "plan.trace", "master.query", "report.generate"): intent = _resolve_order_reference(store, session_id, intent) if intent.params.get("_missingFocus") and name != "master.query": return AgentReply( text="刚才没有锁定具体订单。请先说「查询订单 102285668」," "或直接说「给订单 102285668 排产」。") if intent.params.get("_missingFocus") and name == "master.query" and intent.params.get("entity") == "mrp": return AgentReply( text="刚才没有锁定具体订单。请先说「查询订单 102285668」," "或直接问「订单 102285668 有委外吗」。") # ---- 排产(P1) ---- if name == "schedule.run": return _run_schedule(store, intent, actor) # 试排并回执 # ---- 发布(P2 → 确认卡) ---- if name == "schedule.publish": return _stage_publish(store, session_id, actor) # ---- 重置数据(P2 → 确认卡) ---- if name == "data.reset": return _stage_reset(store, session_id, actor) # ---- 新建/编辑订单(P2 → 确认卡;对话侧建单) ---- if name == "order.upsert": return _stage_order_upsert(store, session_id, intent, actor) # ---- 订单状态动作(P2 → 确认卡;对话与页面共用 action) ---- if name in ("order.cancel", "order.complete", "order.delete"): from server.aps_domain.orders import confirmation_for_order_action, find_order order_no = str(intent.params.get("orderNo") or "") order = find_order(store.data, order_no=order_no) if order is None: return AgentReply(text=f"没找到订单 {order_no},可以在订单面板核对订单号。") title, lines = confirmation_for_order_action(store.data, name, {"id": order["id"]}) block = harness.stage_confirmation(session_id, name, {"id": order["id"]}, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action=name + ".stage", target={"type": "SALES_ORDER", "id": order["id"], "orderNo": order["orderNo"]}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 订单写入,需要你确认。", blocks=[block]) if name == "order.clear": from server.aps_domain.orders import confirmation_for_order_action try: title, lines = confirmation_for_order_action(store.data, "order.clear", {}) except ValueError as exc: return AgentReply(text=str(exc)) block = harness.stage_confirmation(session_id, "order.clear", {}, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="order.clear.stage", target={"type": "SALES_ORDER", "id": "ALL"}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 破坏性写入,需要你确认。", blocks=[block]) # ---- OR-03 订单池:提交(P1)/ 批准驳回(P2)/ 摘要(P0) ---- if name == "order.pool": from server.aps_domain.orders import list_orders, pool_summary sm = pool_summary(store.data) pending = [o for o in list_orders(store.data) if o["status"] == "SUBMITTED"] lines = [ f"订单池:全部 {sm['all']} · 可排产 {sm['schedulable']} · 待审 {sm['pending']} · " f"已批 {sm['approved']} · 变更 {sm['changed']} · 草稿/驳回 {sm['draft']}", ] if pending: lines.append("待审:" + "、".join(f"{o['orderNo']}({o['customerName']})" for o in pending[:8])) lines.append("可说「批量批准」或「批准订单 SOxxx」。") else: lines.append("当前无待审订单。新建订单默认为 DRAFT,需「提交审核」后再批准。") return AgentReply(text="\n".join(lines)) # ---- OR-04 紧急插单:快评(P1 沙盒)/ 采用(P2) ---- if name == "rush.evaluate": from server.aps_domain.rush import evaluate_rush, format_impact_text, impact_to_block p = dict(intent.params or {}) try: impact = harness.guard("rush.evaluate", p, lambda: evaluate_rush(store.data, p)) except ValueError as exc: return AgentReply(text=str(exc)) _LAST_RUSH_EVAL.clear() _LAST_RUSH_EVAL.update(impact) write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="rush.evaluate", target={"type": "SANDBOX", "id": impact["evalId"]}, power="P1", rationale={"affected": impact["affectedOrderCount"], "delayDelta": impact["delayDelta"], "conflictDelta": impact["conflictDelta"]}) store.save() return AgentReply( text=format_impact_text(impact), blocks=[impact_to_block(impact)], commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "due"}, issuedBy="LLM")]) if name == "rush.apply": p = dict(intent.params or {}) if not p.get("payload") and not p.get("orderNo") and not p.get("productCode") and not p.get("productId"): if not _LAST_RUSH_EVAL: return AgentReply(text="还没有插单快评结果。请先说「插单快评」或「紧急插单 200件控制器A」。") p = { "payload": _LAST_RUSH_EVAL.get("payload"), "strategy": _LAST_RUSH_EVAL.get("strategy"), "evalId": _LAST_RUSH_EVAL.get("evalId"), } rush = p.get("payload") or p label = rush.get("orderNo") or f"{rush.get('customerName', '急单')}×{rush.get('quantity', '?')}" block = harness.stage_confirmation( session_id, "rush.apply", p, title=f"采用紧急插单 · {label}", summary_lines=[ f"策略:{p.get('strategy') or 'DELIVERY_FIRST'}", "将写入主干订单池(APPROVED + isRush),并生成 DRAFT 排产版本", "基准已发布版本不回写;执行前自动建档,可回滚", f"快评 ID:{p.get('evalId') or '(直接采用)'}", ], ) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="rush.apply.stage", target={"type": "SALES_ORDER", "id": rush.get("orderNo") or "NEW"}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text="采用插单属于 P2 写操作,需要你确认(见下方确认卡)。", blocks=[block]) # ---- OR-05 预测 / 长周期订单 ---- if name == "forecast.query": from server.aps_domain.forecast import forecast_summary, list_forecasts sm = forecast_summary(store.data) rows = list_forecasts(store.data) lines = [ f"预测订单:全部 {sm['all']} · 生效 {sm['active']} · 草稿 {sm['draft']} · " f"已转正 {sm['consumed']} · 取消 {sm['cancelled']}", ] for f in rows[:8]: lines.append( f"· {f['forecastNo']} {f.get('productName')}×{f.get('quantity')} " f"{f.get('periodStart')}~{f.get('periodEnd')} · {f.get('status')} · " f"置信{int((f.get('confidence') or 0)*100)}%" ) if sm["active"]: lines.append("可说「预测纳入试排」或「预测转正 FCxxx」。") return AgentReply(text="\n".join(lines)) if name in ("forecast.upsert", "forecast.delete", "forecast.convert"): from server.aps_domain.forecast import confirmation_for_forecast_action p = dict(intent.params or {}) try: title, lines = confirmation_for_forecast_action(store.data, name, p) except ValueError as exc: return AgentReply(text=str(exc)) block = harness.stage_confirmation(session_id, name, p, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action=f"{name}.stage", target={"type": "FORECAST_ORDER", "id": p.get("id") or p.get("forecastNo") or "NEW"}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 写操作,需要你确认。", blocks=[block]) # ---- PL-01/PL-02 时间分桶 + 粗能力 ---- if name == "plan.buckets": from server.aps_domain.planning import build_plan_buckets p = dict(intent.params or {}) try: report = build_plan_buckets( store.data, mode=str(p.get("mode") or "HYBRID"), start_date=p.get("startDate"), horizon_days=int(p.get("horizonDays") or 90), include_forecast=bool(p.get("includeForecast", True)), capacity_mode=str(p.get("capacityMode") or "FINITE"), ) except ValueError as exc: return AgentReply(text=str(exc)) sm = report["summary"] cap_label = "无限" if report.get("capacityMode") == "INFINITE" else "有限" lines = [ f"时间分桶({report['mode']} · {cap_label}产能):桶 {sm['bucketCount']} · " f"超载 {sm['overCount']} · 预警 {sm['warnCount']} · 正常 {sm['okCount']}", f"日产能 {report['dailyCapacity']} 件 · 展望 {report['horizonDays']} 天 · " + ( f"峰值所需日产能 {sm.get('peakRequiredDaily')} · 有限参照超载桶 {sm.get('finiteOverCount')}" if report.get("capacityMode") == "INFINITE" else f"总负荷 {round(sm['overallLoad']*100)}%" ) + ("(含预测)" if report["includeForecast"] else "(仅确定订单)"), ] for b in report["buckets"]: show = b["status"] in ("OVER", "WARN") or b["demandQty"] > 0 or b.get("finiteStatus") == "OVER" if not show: continue if report.get("capacityMode") == "INFINITE": lines.append( f"· {b['label']}({b['kind']}) 需求{b['demandQty']} 需日产{b.get('requiredDaily')} " f"有限参照{round((b.get('finiteLoadRatio') or 0)*100)}% [{b.get('finiteStatus')}]" ) else: lines.append( f"· {b['label']}({b['kind']}) 需求{b['demandQty']}/能力{b['capacity']} " f"负荷{round(b['loadRatio']*100)}% [{b['status']}]" ) if len(lines) > 14: lines.append("…(完整表见右侧「分桶计划」视口)") break return AgentReply( text="\n".join(lines), commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "plan"}, issuedBy="LLM")], ) if name == "plan.rccp": from server.aps_domain.planning import build_rccp_compare p = dict(intent.params or {}) try: cmp_ = build_rccp_compare( store.data, mode=str(p.get("mode") or "HYBRID"), start_date=p.get("startDate"), horizon_days=int(p.get("horizonDays") or 90), include_forecast=bool(p.get("includeForecast", True)), ) except ValueError as exc: return AgentReply(text=str(exc)) d = cmp_["delta"] lines = [ f"RCCP 粗能力对照({cmp_['mode']}):{d['message']}", f"日产能缺口(峰值所需−当前)≈ {d['capacityGapDaily']} 件/天", ] for s in (cmp_.get("shortfallBuckets") or [])[:6]: lines.append( f"· {s['label']} 有限缺口 {s['finiteGap']} · 所需日产 {s['requiredDaily']} " f"· 负荷 {round((s.get('finiteLoadRatio') or 0)*100)}%" ) lines.append("可说「无限产能粗评」或在分桶视口切换有限/无限。") return AgentReply( text="\n".join(lines), commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "plan"}, issuedBy="LLM")], ) if name == "plan.feasibility": from server.aps_domain.planning import build_feasibility p = dict(intent.params or {}) try: report = build_feasibility( store.data, start_date=p.get("startDate"), horizon_days=int(p.get("horizonDays") or 90), include_forecast=bool(p.get("includeForecast", True)), ) except ValueError as exc: return AgentReply(text=str(exc)) sm = report["summary"] verdict = report["verdict"] lines = [ f"可行性结论:【{verdict}】 · 订单 {sm['all']} " f"(可行 {sm['feasible']} / 风险 {sm['atRisk']} / 不可行 {sm['infeasible']} / 过期 {sm['late']})", f"日产能 {report['dailyCapacity']} · 超载桶 {sm['shortfallBucketCount']} · " f"分桶总负荷 {round((sm.get('overallLoad') or 0)*100)}%" + ("(含预测)" if report["includeForecast"] else "(仅确定订单)"), ] for g in (report.get("gaps") or [])[:5]: lines.append(f"· 缺口桶 {g['label']} 负荷{round((g.get('loadRatio') or 0)*100)}% 缺口{g.get('gap')}") risky = [o for o in report["orders"] if o["status"] in ("INFEASIBLE", "AT_RISK", "LATE")] for o in risky[:6]: reason = (";".join(o.get("reasons") or []) or o["status"]) lines.append(f"· {o['ref']} 交期{o['dueDate']} slack={o['slack']} [{o['status']}] {reason}") if not risky: lines.append("当前累计粗能力下,已批准需求均可按期覆盖。") lines.append("完整表见右侧「分桶计划」→ 可行性。") return AgentReply( text="\n".join(lines), commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "plan"}, issuedBy="LLM")], ) if name == "plan.inventory": from server.aps_domain.inventory import build_inventory_projection p = dict(intent.params or {}) try: report = build_inventory_projection( store.data, material_code=p.get("materialCode"), material_type=p.get("materialType"), horizon_days=int(p.get("horizonDays") or 30), bucket=str(p.get("bucket") or "DAY"), include_forecast=bool(p.get("includeForecast", True)), start_date=p.get("startDate"), ) except ValueError as exc: return AgentReply(text=str(exc)) sm = report["summary"] lines = [ f"库存投影({report['bucket']} · {report['horizonDays']}天):" f"物料 {sm['materialCount']} · 断料 {sm['stockoutCount']} · " f"低于安全库存 {sm['belowSafetyCount']} · 正常 {sm['okCount']}", ] risky = [m for m in report["materials"] if m["alert"] != "OK"] for m in (risky or report["materials"])[:8]: lines.append( f"· {m['code']} {m['name']} 期初{m['openingStock']}→末{m['endingProjected']} " f"[{m['alert']}]" + (f" 首警{m['firstAlertDate']}" if m.get("firstAlertDate") else "") ) lines.append("完整曲线见右侧「分桶计划」→ 库存投影。") return AgentReply( text="\n".join(lines), commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "plan"}, issuedBy="LLM")], ) if name == "plan.leveling": from server.aps_domain.planning import build_leveling p = dict(intent.params or {}) try: report = build_leveling( store.data, mode=str(p.get("mode") or "WEEK"), start_date=p.get("startDate"), horizon_days=int(p.get("horizonDays") or 90), include_forecast=bool(p.get("includeForecast", True)), target_load=float(p.get("targetLoad") or 0.85), ) except ValueError as exc: return AgentReply(text=str(exc)) b, a, imp = report["before"], report["after"], report["improvement"] lines = [ f"产能削峰({report['mode']} · 目标≤{int(report['targetLoad']*100)}%):" f"峰值负荷 {round(b['peakLoad']*100)}%→{round(a['peakLoad']*100)}% · " f"超载桶 {b['overCount']}→{a['overCount']} · 建议挪动 {imp['moveCount']} 笔/{imp['movedQty']} 件", ] for m in report["moves"][:8]: arrow = "↑提前" if m["direction"] == "PULL_AHEAD" else "↓延后" lines.append( f"· {arrow} {m['quantity']}×{m.get('productCode') or '?'} " f"{m['fromLabel']}→{m['toLabel']}" ) if not report["moves"]: lines.append("当前分桶无明显超载,无需削峰。") lines.append("完整对照见右侧「分桶计划」→ 削峰。本切片不改交期。") return AgentReply( text="\n".join(lines), commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "plan"}, issuedBy="LLM")], ) if name == "plan.supply": from server.aps_domain.planning import build_supply_decisions p = dict(intent.params or {}) try: report = build_supply_decisions( store.data, mode=str(p.get("mode") or "WEEK"), start_date=p.get("startDate"), horizon_days=int(p.get("horizonDays") or 90), include_forecast=bool(p.get("includeForecast", True)), target_load=float(p.get("targetLoad") or 0.85), ) except ValueError as exc: return AgentReply(text=str(exc)) lines = [ f"产供决策【{report['verdict']}】:{report['message']}", f"削峰挪动 {report['leveling']['movedQty']} 件 · 残留缺口 {report['summary']['residualGap']} · " f"选项 {report['summary']['optionCount']} 条", ] for o in report["options"][:8]: lines.append( f"· [{o['label']}] {o['bucketLabel']} 覆盖{o['coverQty']} " f"成本指数{o['costIndex']} · {o['detail']}" ) if report["verdict"] == "BALANCED": lines.append("可继续试排或查看库存投影。") else: lines.append("完整方案见右侧「分桶计划」→ 产供。本切片不写主干。") return AgentReply( text="\n".join(lines), commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "plan"}, issuedBy="LLM")], ) if name == "order.submit": from server.aps_domain.orders import apply_order_submit, find_order p = dict(intent.params or {}) if not p.get("id") and not p.get("orderNo"): # 无单号:取最近一条可提交订单 cand = next((o for o in reversed(store.data.get("salesOrders", [])) if o.get("status") in ("DRAFT", "REJECTED", "CHANGED")), None) if not cand: return AgentReply(text="没有可提交审核的订单(需 DRAFT/REJECTED/CHANGED)。") p["id"] = cand["id"] try: applied = harness.guard("order.submit", p, lambda: apply_order_submit(store.data, p)) except ValueError as exc: return AgentReply(text=str(exc)) order = applied["order"] write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="order.submit", target={"type": "SALES_ORDER", "id": order["id"], "orderNo": order["orderNo"]}, power="P1", rationale={"from": applied.get("beforeStatus"), "to": "SUBMITTED"}) store.save() return AgentReply( text=(f"订单 {order['orderNo']} 已提交审核 ✅({applied.get('beforeStatus')} → SUBMITTED)。" f"可说「批准订单 {order['orderNo']}」或在订单面板批量批准。"), commands=[ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM")]) if name in ("order.approve", "order.reject"): from server.aps_domain.orders import confirmation_for_order_action, _resolve_order_ids payload = dict(intent.params or {}) if payload.get("orderIds") == "pending": payload["orderIds"] = "pending" elif payload.get("orderNo") and not payload.get("id"): pass try: # 预检可解析到订单 ids = _resolve_order_ids(store.data, payload) if not ids and payload.get("orderNo"): return AgentReply(text=f"订单 {payload['orderNo']} 不在待审状态(需 SUBMITTED/CHANGED)。") if not ids: return AgentReply(text="没有可审核的订单。可先「提交审核」或查看「订单池」。") payload["orderIds"] = ids title, lines = confirmation_for_order_action(store.data, name, payload) except ValueError as exc: return AgentReply(text=str(exc)) block = harness.stage_confirmation(session_id, name, payload, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action=name + ".stage", target={"type": "SALES_ORDER", "id": ids[0] if len(ids) == 1 else "BATCH"}, power="P2", rationale={"confirmId": block.props["confirmId"], "count": len(ids)}) store.save() return AgentReply(text=f"{title} 属于 P2 写操作,需要你确认。", blocks=[block]) if name == "master.clear": from server.aps_domain.masterdata import confirmation_for_master_action scope = str(intent.params.get("scope") or "all") try: title, lines = confirmation_for_master_action(store.data, "master.clear", {"scope": scope}) except ValueError as exc: return AgentReply(text=str(exc)) block = harness.stage_confirmation(session_id, "master.clear", {"scope": scope}, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="master.clear.stage", target={"type": "MASTER_CLEAR", "id": scope}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 主数据写入,需要你确认。", blocks=[block]) # ---- 对话新建物料(P2) ---- if name == "master.material.upsert": from server.aps_domain.masterdata import confirmation_for_master_action payload = dict(intent.params or {}) missing = [label for key, label in (("code", "编码"), ("name", "名称")) if not payload.get(key)] if missing: return AgentReply(text=f"要新建物料还差:{'、'.join(missing)}。\n" "例如:「新建物料 编码 SCR-NEW 名称 不锈钢螺丝 类型 原料 库存 800」。") payload.setdefault("type", "RAW_MATERIAL") payload.setdefault("unit", "件") try: title, lines = confirmation_for_master_action(store.data, "master.material.upsert", payload) except ValueError as exc: return AgentReply(text=f"物料信息有误:{exc}") block = harness.stage_confirmation(session_id, "master.material.upsert", payload, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="master.material.upsert.stage", target={"type": "MATERIAL", "id": payload.get("code")}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 主数据写入,需要你确认。", blocks=[block]) if name == "changeover.query": from server.aps_domain.changeover import build_changeover_view view = build_changeover_view(store.data) lines = [ f"换型矩阵:{len(view['rows'])} 条显式规则 · 产品族 {len(view['families'])} 个", f"缺省跨族 {view['defaultCrossFamilyMin']} 分;同族 0 分", ] for r in view["rows"][:12]: lines.append(f"· {r['fromFamily']} → {r['toFamily']} = {r['setupMinutes']} 分" + (f"({r['note']})" if r.get("note") else "")) if not view["rows"]: lines.append("尚无显式规则,跨族走缺省。可说「设置换型 CTRL-STD 到 CTRL-HF 45分」。") lines.append("完整编辑见主数据 → 工艺模型 → 换型。") return AgentReply(text="\n".join(lines)) if name == "campaign.preview": from server.aps_domain.campaign import preview_campaigns p = dict(intent.params or {}) report = preview_campaigns( store.data, window_days=int(p.get("windowDays") or 7), include_forecast=bool(p.get("includeForecast", False)), ) s = report["summary"] lines = [ f"战役预览:输入 {s['inputOrders']} 单 → {s['campaignCount']} 场" f"(合并 {s['mergedCampaigns']} 场,省 {s['poSaved']} 个 PO)", f"交期窗口 {report['windowDays']} 天", ] for c in report["campaigns"][:10]: tag = "战役" if c["isCampaign"] else "单排" nos = "/".join(str(x) for x in (c.get("sourceOrderNos") or [])[:4]) lines.append( f"· [{tag}] {c['productCode']} ×{c['totalQty']} · {c['memberCount']} 单" f" · 交期 {c.get('earliestDue')}~{c.get('latestDue')} · {nos}" ) lines.append("确认后可说「战役合并试排」。") return AgentReply(text="\n".join(lines)) if name == "master.changeover.upsert": from server.aps_domain.masterdata import confirmation_for_master_action payload = dict(intent.params or {}) try: title, lines = confirmation_for_master_action(store.data, "master.changeover.upsert", payload) except ValueError as exc: return AgentReply(text=f"换型矩阵有误:{exc}") block = harness.stage_confirmation( session_id, "master.changeover.upsert", payload, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="master.changeover.upsert.stage", target={"type": "CHANGEOVER", "id": f"{payload.get('fromFamily')}→{payload.get('toFamily')}"}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 主数据写入,需要你确认。", blocks=[block]) # ---- 自然语言批量导入订单/物料(P2) ---- if name == "data.import": from server.aps_domain.intake import confirmation_for_import, parse_import text = str(intent.params.get("text") or "") parsed = parse_import(text, store.data) if intent.params.get("kind") in ("orders", "materials"): parsed["kind"] = intent.params["kind"] if not parsed["rows"]: hint = ("订单格式示例:\n导入订单\n客户A,成品料号,200,2026-08-01,VIP\n客户 客户B 产品 成品名称 数量 100 交期 8月20日" if parsed["kind"] == "orders" else "物料格式示例:\n导入物料\nSCR-NEW,不锈钢螺丝,原料,包,800\n编码 PCB-X 名称 试验板 类型 原料 库存 200") err = ";".join(parsed.get("errors", [])[:3]) return AgentReply(text=f"没有解析出可导入的行。{err}\n{hint}") title, lines = confirmation_for_import(parsed) payload = {"kind": parsed["kind"], "rows": parsed["rows"]} block = harness.stage_confirmation(session_id, "data.import", payload, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="data.import.stage", target={"type": "IMPORT", "id": parsed["kind"]}, power="P2", rationale={"confirmId": block.props["confirmId"], "count": len(parsed["rows"])}) store.save() return AgentReply(text=f"{title} 属于 P2 写入,需要你确认。", blocks=[block]) # ---- 订单分解(P1:MRP 建议草稿,不碰主数据与订单本体) ---- if name == "order.decompose": from server.aps_domain.mrp import decompose_orders, summarize_decomposition try: result = decompose_orders(store.data, store.next_id, intent.params.get("orderNo")) except ValueError as exc: return AgentReply(text=str(exc)) write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="order.decompose", target={"type": "MRP", "id": intent.params.get("orderNo") or "ALL"}, power="P1", rationale={"purchase": len(result["purchase"]), "outsource": len(result["outsource"])}) store.save() return AgentReply(text=summarize_decomposition(result)) # ---- 主数据/订单自然语言查询(P0 只读) ---- if name == "master.query": from server.agent_core.session_focus import set_focus from server.aps_domain.master_query import format_master_query, run_master_query code = intent.params.get("code") or intent.params.get("orderNo") result = run_master_query( store.data, entity=intent.params.get("entity"), code=code, text=str(intent.params.get("query") or ""), aspect=intent.params.get("aspect"), ) # 记住刚查到的订单,供「把这个订单排产 / 有委外吗」指代 rows = result.get("rows") or [] if result.get("entity") == "order" and rows: set_focus(session_id, orderNo=rows[0].get("orderNo"), productCode=rows[0].get("productCode")) elif result.get("entity") == "mrp" and result.get("orderNo"): set_focus(session_id, orderNo=result.get("orderNo"), productCode=result.get("productCode")) elif result.get("productCode"): set_focus(session_id, productCode=result.get("productCode")) write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="master.query", target={"type": "MASTER", "id": result.get("entity") or "overview"}, power="P0", rationale={"entity": result.get("entity"), "code": code, "count": result.get("count")}) store.save() return AgentReply(text=format_master_query(result)) # ---- 计划追溯(P0:SO→主数据→分解→PO/WO→负荷/库存) ---- if name == "plan.trace": from server.aps_domain.trace import plan_trace, summarize_trace track = intent.params.get("track") try: result = plan_trace(store.data, intent.params.get("orderNo"), track=track) except ValueError as exc: return AgentReply(text=str(exc)) # both 形态:聊天只摘要;单轨带前 3 单 if result.get("track") == "both": block = UIBlock(blockId="plan-trace", type="plan-trace", props={"track": "both", "count": result["count"], "fixedCount": (result.get("fixed") or {}).get("count", 0), "flexCount": (result.get("flex") or {}).get("count", 0), "orders": ((result.get("flex") or {}).get("orders") or (result.get("fixed") or {}).get("orders") or [])[:2]}) else: preview = {**result, "orders": result["orders"][:3]} block = UIBlock(blockId="plan-trace", type="plan-trace", props=preview) hint = ("完整链路可在「柔性工作台 → 钉扎」或「订单 → 追溯」查看。" if (result.get("track") == "flex" or (intent.params.get("orderNo") or "").upper().startswith("FO-")) else "完整链路可在右侧「订单 → 追溯」查看。") return AgentReply(text=summarize_trace(result) + "\n" + hint, blocks=[block]) # ---- MRP 建议单下达(P2 → 确认卡:DRAFT 采购/委外转正式) ---- if name == "mrp.release": from server.aps_domain.mrp import confirmation_for_mrp_release order_no = str(intent.params.get("orderNo") or "") or None kind = str(intent.params.get("kind") or "all") try: title, lines = confirmation_for_mrp_release(store.data, order_no, kind) except ValueError as exc: return AgentReply(text=str(exc)) params = {"orderNo": order_no, "kind": kind} block = harness.stage_confirmation(session_id, "mrp.release", params, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="mrp.release.stage", target={"type": "MRP", "id": order_no or "ALL"}, power="P2", rationale={"confirmId": block.props["confirmId"], "kind": kind}) store.save() return AgentReply(text=f"{title} 属于 P2 写操作,需要你确认。", blocks=[block]) # ---- 现场完整生产路线(P2:替换 flex*) ---- if name == "flex.site.load": from server.aps_domain.kangni_intake import build_flex_bundle, confirmation_for_site_load try: bundle = build_flex_bundle( intent.params.get("routePath") or None, intent.params.get("dataDir") or None, include_sibling_orders=intent.params.get("includeSiblings", False) is True, station_count=int(intent.params.get("stationCount") or 4), ) except (FileNotFoundError, ValueError) as exc: return AgentReply(text=f"无法加载现场路线:{exc}") meta = bundle.get("_meta") or {} title, lines = confirmation_for_site_load(meta) params = { "routePath": intent.params.get("routePath"), "dataDir": intent.params.get("dataDir"), "includeSiblings": intent.params.get("includeSiblings", False) is True, "stationCount": int(intent.params.get("stationCount") or 4), } block = harness.stage_confirmation(session_id, "flex.site.load", params, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="flex.site.load.stage", target={"type": "FLEX_SITE", "id": meta.get("primaryOrder")}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 写操作,需要你确认。", blocks=[block]) # ---- 柔性排产(P1:能力池动态组虚拟产线,写草稿版本 §M5) ---- if name == "flex.schedule": return _run_flex(store, intent, actor) if name == "flex.reschedule": return _flex_reschedule(store, intent, session_id, actor) if name == "flex.swap": return _flex_swap(store, intent, actor) # ---- 瓶颈产能评估(P0:只读,瓶颈产能法) ---- if name == "flex.capacity": return _flex_capacity(store) # ---- 交期承诺模拟(P0:深拷贝沙盒试插单,不碰主干) ---- if name == "flex.simulate_due": return _flex_simulate_due(store, intent) if name == "flex.compare": return _flex_compare(store) if name == "flex.rush": return _flex_rush(store, intent, actor) if name == "flex.fault": return _flex_fault(store, intent, actor) if name == "conflict.list": return _conflict_list(store, intent) if name == "flex.conflict.resolve": return _conflict_resolve(store, intent, session_id, actor) # ---- SAP 集成(MD-05:状态 P0 / 入出站 P2 确认卡) ---- if name == "sap.status": from server.aps_domain.sap_sync import sap_connection_status st = sap_connection_status() return AgentReply( text=(f"SAP 连接:{'在线' if st.get('connected') else '离线'} · " f"{st.get('mode')} · {st.get('system')}/{st.get('plant')} · " f"待拉单 {st.get('openOrders')} · 已回写 {st.get('receiptCount')}。")) if name in ("sap.sync.inbound", "sap.sync.outbound"): from server.aps_domain.sap_sync import stage_sap_sync direction = "inbound" if name.endswith("inbound") else "outbound" r = stage_sap_sync(store, direction, session_id=session_id, actor=actor) return AgentReply(text=r["message"], blocks=[r["block"]]) # ---- MES 下发 / 报工(EX-05 / EX-09) ---- if name == "mes.status": from server.aps_domain.mes import list_execution, mes_connection_status st = mes_connection_status() ex = list_execution(store.data, "flex") return AgentReply( text=(f"MES:{'在线' if st.get('connected') else '离线'} · {st.get('system')} · " f"外部工单 {st.get('woCount')}(未完 {st.get('openCount')})· " f"柔性已下发 {ex.get('total', 0)} / 完工 {ex.get('completed', 0)}。")) if name == "mes.dispatch": from server.aps_domain.mes import stage_dispatch r = stage_dispatch(store, track=intent.params.get("track") or "flex", session_id=session_id, actor=actor) if r.get("block"): return AgentReply(text=r["message"], blocks=[r["block"]]) return AgentReply(text=r.get("message") or "无法下发") if name == "mes.report": from server.aps_domain.mes import apply_report, list_execution track = intent.params.get("track") or "flex" wo_id = intent.params.get("woId") finish = bool(intent.params.get("finish") or intent.params.get("finishAll")) if intent.params.get("finishAll") or wo_id is None: ex = list_execution(store.data, track) targets = [r for r in ex.get("rows") or [] if r.get("status") != "COMPLETED" and (r.get("progressPct") or 0) < 100] if not targets: return AgentReply(text="没有已下发且未完工的工单。请先「下发MES」。") msgs = [] for row in targets[:30]: try: r = apply_report(store, int(row["woId"]), track=track, finish=True, actor=actor) msgs.append(r["message"]) except (ValueError, PermissionError) as exc: msgs.append(str(exc)) return AgentReply( text=f"批量报工完成 ✅ {len(msgs)} 条。\n" + "\n".join(msgs[:5]), commands=[ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM")]) try: r = apply_report(store, int(wo_id), track=track, progress_pct=intent.params.get("progressPct"), finish=finish, actor=actor) except (ValueError, PermissionError) as exc: return AgentReply(text=str(exc)) return AgentReply(text=r["message"], commands=[ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM")]) # ---- 方案对比(P1:Explore 沙盒,不碰主干 §5.1/§9.7) ---- if name == "scenario.compare": from server.aps_domain.analytics import matrix_from_fixed_cards text, block = compare_scenarios(store.data) # 沙盒并行试排三策略 block.props["table"] = matrix_from_fixed_cards( block.props.get("cards") or [], baseline=block.props.get("baseline")) write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="scenario.compare", target={"type": "SANDBOX", "id": block.blockId}, power="P1", rationale={"strategies": [c["strategy"] for c in block.props["cards"]]}) # 沙盒运行留痕 store.save() # 审计落盘(世界业务数据未变) return AgentReply( text=text + "\n已打开方案对比表视口。", blocks=[block], commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "compare"}, issuedBy="LLM")]) if name == "scenario.sensitivity": from server.aps_domain.sensitivity import run_sensitivity, sensitivity_as_block strategy = str((intent.params or {}).get("strategy") or "COMPREHENSIVE") report = run_sensitivity(store.data, strategy=strategy) block = sensitivity_as_block(report) write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="scenario.sensitivity", target={"type": "SANDBOX", "id": block.blockId}, power="P1", rationale={"strategy": strategy, "factors": [r["factorId"] for r in report["rows"]]}) store.save() top = report["rows"][0] if report["rows"] else None head = ( f"敏感性分析完成(策略 {strategy})。基线延期 {report['baseline']['tardiness']} h。" ) if top: head += f"\n最敏感因子:{top['label']}(摆幅 {top['swing']})。" head += f"\n{report.get('hint') or ''}" return AgentReply(text=head, blocks=[block]) # ---- 手动建档(P1:只新增快照 §4.3) ---- if name == "checkpoint.create": label = intent.params.get("label") or f"手动存档 {fmt_date(today0())}" # 缺省自动命名 meta = get_checkpoints().create(store.data, label=label, reason="manual", conversation_note="用户口令建档") # 建成对快照 write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="checkpoint.create", target={"type": "CHECKPOINT", "id": meta["pairId"]}, power="P1", rationale={"label": label}) # 建档留痕 store.save() # 审计落盘 return AgentReply(text=f"已创建检查点【{label}】(ID: {meta['pairId']})✅\n" f"随时可以说“回滚到检查点 {meta['pairId']}”回到此刻。", commands=[ViewportCommand(cmd="world.refresh", issuedBy="SYSTEM")]) # 刷新时间线导轨 # ---- 回滚(P2 → 确认卡 §4.3) ---- if name == "checkpoint.rollback": ckpt = get_checkpoints() # 快照仓 pair_id = intent.params.get("pairId") # 指定目标(时间线点击合成口令携带) pair = ckpt.get(pair_id) if pair_id else ckpt.latest() # 缺省=最近一个 if pair is None: # 无档可回 return AgentReply(text="当前没有可回滚的检查点。可先执行“建一个检查点”。") block = harness.stage_confirmation( # 回滚是 P2:出确认卡 session_id, "checkpoint.rollback", {"pairId": pair["pairId"]}, title=f"回滚到检查点【{pair['label']}】", summary_lines=[ # 影响面说明 f"目标时刻:{pair['createdAt']}(当时版本 {pair.get('versionNo') or '无'})", "当前世界状态将被整体替换(对话与世界成对回滚 §4.2)", "回滚前会自动再存档一次,本次回滚可撤销", ]) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="checkpoint.rollback.stage", target={"type": "CHECKPOINT", "id": pair["pairId"]}, power="P2", rationale={"confirmId": block.props["confirmId"]}) # 出卡留痕 store.save() # 审计落盘 return AgentReply(text=f"回滚是 P2 写操作,需要你确认(目标:{pair['label']})。", blocks=[block]) # ---- 视口命令族(P0:直接回传命令由前端执行) ---- if name.startswith("viewport."): replies = { # 各命令的回执文案 "viewport.mode": {"gantt": "已切换为【甘特图】。", "load": "已切换为【负荷热力图】。", "due": "已切换为【交期承诺看板】。", "pool": "已切换为【产能池】。", "flex": "已切换为【柔性甘特】。", "kpi": "已切换为【KPI 仪表盘】。", "compare": "已切换为【方案对比表】。", "util": "已切换为【资源利用率】。", "plan": "已切换为【分桶计划】。", }.get(intent.params.get("mode", ""), "已切换视图。"), "viewport.filter": "已应用过滤,右侧只显示匹配的工单。", "viewport.focus": f"已聚焦产线 {intent.params.get('lineCode', '')},其他产线已隐藏。", "viewport.highlight": "已高亮" + ("【超期风险】(红)" if intent.params.get("what") == "overdue" else "【冲突】(黄)") + ",其余淡出。", "viewport.timescale": "已切换时间粒度。", "viewport.reset": "已重置视口:清除过滤、聚焦与高亮。", } return AgentReply(text=replies.get(name, "已执行。"), commands=[ # 回传结构化视口命令 ViewportCommand(cmd=name, params=intent.params, # 命令与槽位原样下发 target=intent.params.get("lineCode"), issuedBy="LLM")]) # ---- 知识库检索(P0:只读带出处 §8.2;可选 LLM 综合) ---- if name == "knowledge.query": from server.agent_core.providers import get_provider from server.knowledge.retrieval import hybrid_search kb = get_knowledge() kb.ensure_seed_assets() q = str(intent.params.get("query") or "") title_hint = intent.params.get("assetTitle") if re.search(r"^(知识库|知识清单)$|有哪些知识|知识库(有什么|清单|目录)|工艺模式(有哪些|清单)", q.strip()): metas = kb.list_meta() by_kind: dict[str, list] = {} for m in metas: by_kind.setdefault(m.get("kind") or "other", []).append(m["title"]) lines = [f"知识库共 {len(metas)} 条资产:"] kind_cn = {"sop": "SOP", "algorithm": "算法", "process": "工艺", "case": "案例", "report": "报告"} for k, titles in by_kind.items(): lines.append(f"【{kind_cn.get(k, k)}】{len(titles)} 条") for t in titles[:12]: lines.append(f" · {t}") if len(titles) > 12: lines.append(f" · …另有 {len(titles) - 12} 条") lines.append("可说「@知识:机加工工艺路线生成总则」或「导入知识文档」扩展语料。") write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="knowledge.query", target={"type": "KNOWLEDGE", "id": "catalog"}, power="P0", rationale={"query": q, "mode": "catalog", "count": len(metas)}) store.save() return AgentReply(text="\n".join(lines)) if title_hint: asset = kb.find_by_title(str(title_hint)) hits = [{"assetId": asset["assetId"], "title": asset["title"], "kind": asset["kind"], "version": asset["version"], "score": 1.0, "snippet": asset["content"][:120], "content": asset["content"]}] if asset else [] else: hits = hybrid_search(kb.iter_search_units(), q, top_k=5) write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="knowledge.query", target={"type": "KNOWLEDGE", "id": hits[0]["assetId"] if hits else "miss"}, power="P0", rationale={"query": intent.params.get("query"), "hits": [h["assetId"] for h in hits]}) store.save() if not hits: from server.aps_domain.guidance import build_guidance block = UIBlock(blockId="guidance-kb-miss", type="guidance", props=build_guidance(store.data, context="knowledge_miss")) return AgentReply( text="知识库里没有找到相关内容(不编造)。\n" "可以换个说法,或说「知识库」查看已有资产清单。", blocks=[block]) # 尝试 LLM 综合;失败则吐最相关原文 top = hits[0] text = None ctx_parts = [] for i, h in enumerate(hits[:4], 1): ctx_parts.append( f"[{i}] 《{h['title']}》{h['version']}" + (f" §{h['heading']}" if h.get("heading") else "") + f"\n{h['content'][:800]}" ) sys_prompt = ( "你是 APS 排产知识助手。只根据给定资料回答,禁止编造资料外的数字与规定。" "回答末尾列出引用的资料标题与版本。" ) user_prompt = f"问题:{q}\n\n资料:\n" + "\n\n".join(ctx_parts) try: text = await get_provider().chat_text(sys_prompt, user_prompt) except Exception: text = None if not text: text = (f"{top['content']}\n\n" f"—— 出处:【{top['title']}】{top['version']}({top['kind']})") else: cites = ";".join(f"【{h['title']}】{h['version']}" for h in hits[:4]) if "出处" not in text and "【" not in text: text = text.rstrip() + f"\n\n—— 出处:{cites}" block = UIBlock( blockId=f"evidence-{top['assetId']}", type="evidence", props={"hits": [{k: h.get(k) for k in ("assetId", "title", "kind", "version", "score", "snippet", "chunkId", "heading", "page")} for h in hits]}) return AgentReply(text=text, blocks=[block]) # ---- 知识文档导入(P2 确认卡) ---- if name == "knowledge.import": from server.knowledge.ingest import ( confirmation_for_knowledge_import, preview_ingest, ) path = intent.params.get("path") or intent.params.get("filePath") if not path: return AgentReply(text="请指定文件路径,例如:「导入知识文档 D:/docs/sop.md」") try: preview = preview_ingest( os.path.basename(str(path)), path=str(path), kind=str(intent.params.get("kind") or "sop"), title=intent.params.get("title"), ) except (FileNotFoundError, ValueError) as exc: return AgentReply(text=str(exc)) # 确认卡不塞全文 chunks,只留摘要;执行时再读 path title, lines = confirmation_for_knowledge_import(preview) slim = { "path": str(path), "filename": preview["filename"], "title": preview["title"], "kind": preview["kind"], "chunkCount": preview["chunkCount"], "sectionCount": preview["sectionCount"], } block = harness.stage_confirmation( session_id, "knowledge.import", slim, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="knowledge.import.stage", target={"type": "KNOWLEDGE", "id": preview["filename"]}, power="P2", rationale={"confirmId": block.props["confirmId"], "chunkCount": preview["chunkCount"]}) store.save() return AgentReply(text=f"{title} 属于 P2,需要你确认后入库。", blocks=[block]) # ---- 外部算法 Skill ---- if name == "skill.list": from server.agent_core.skills import get_skills rows = get_skills().list() if not rows: return AgentReply(text="尚未登记外部算法 skill。") lines = ["已接入算法 Skill:"] for s in rows: flag = "启用" if s.get("enabled") else "停用" lines.append(f"· {s['skill_id']} 【{s['name']}】{flag} · {s.get('endpoint')} · 轨={s.get('track')}") lines.append("可说「用外部算法试排」或「skill 健康检查」。") return AgentReply(text="\n".join(lines)) if name == "skill.health": from server.agent_core.skills import get_skills health = get_skills().health(intent.params.get("skillId")) lines = ["Skill 健康检查:"] for h in health: ok = "OK" if h.get("ok") else "FAIL" lines.append(f"· {h['skill_id']} [{ok}] {h.get('detail')} " f"{('· ' + str(h.get('latencyMs')) + 'ms') if h.get('latencyMs') is not None else ''}") return AgentReply(text="\n".join(lines)) # ---- 引导式排产向导(M-F:readiness→缺口引导→模板推荐→试排) ---- if name == "schedule.wizard": from server.agent_core.dialog import start_wizard return start_wizard(store, session_id, actor=actor) # ---- 数据齐备度 / 工时维护(M-B:排产数据闭环) ---- if name == "readiness.query": from server.aps_domain.readiness import check_readiness, readiness_text, time_matrix report = harness.guard("readiness.query", {}, lambda: check_readiness(store.data)) matrix = time_matrix(store.data) block = UIBlock( blockId="readiness-report", type="readiness", props={"summary": report["summary"], "orders": report["orders"], "globalIssues": report["globalIssues"], "timeMatrix": [r for r in matrix if r["source"] in ("推断", "待维护")][:40]}, ) return AgentReply(text=readiness_text(report), blocks=[block]) if name == "data.analyze": from server.aps_domain.project_analyze import analyze_project_deep deep = harness.guard( "data.analyze", {}, lambda: analyze_project_deep( store.data, session_id, apply_sql=True, query=str(intent.params.get("query") or ""), ), ) store.save() block = UIBlock(blockId="project-analyze", type="project-analyze", props={ "projectName": deep.get("projectName"), "workDir": deep.get("workDir"), "sources": deep.get("sources") or [], "summary": deep.get("summary") or {}, "orders": deep.get("orders") or [], "materials": deep.get("materials") or [], "routings": deep.get("routings") or [], "equipment": deep.get("equipment") or [], "plan": deep.get("plan") or [], "kbActions": deep.get("kbActions") or [], "sqlPreview": deep.get("sqlPreview"), "canSchedule": bool(deep.get("canSchedule")), "applied": deep.get("applied") or {}, }) return AgentReply(text=deep.get("markdown") or "分析完成。", blocks=[block]) if name == "folder.analyze": from server.aps_domain.folder_pack import analyze_work_dir report = harness.guard( "folder.analyze", {}, lambda: analyze_work_dir(store.data, session_id), ) block = UIBlock(blockId="folder-pack", type="folder-pack", props={ "projectName": report.get("projectName"), "workDir": report.get("workDir"), "files": report.get("files") or [], "coverage": report.get("coverage") or {}, "coverageDetail": report.get("coverageDetail") or [], "missing": report.get("missing") or [], "softMissing": report.get("softMissing") or [], "canSchedule": bool(report.get("canSchedule")), "totalOk": report.get("totalOk") or 0, "totalErrors": report.get("totalErrors") or 0, "error": report.get("error"), }) return AgentReply( text=( f"**工程目录数据包 · {report.get('projectName') or '当前项目'}**\n\n" f"目录:`{report.get('workDir') or ''}`\n\n" + (report.get("error") or ( f"共读到 **{len(report.get('files') or [])}** 个表格," f"有效行 {report.get('totalOk') or 0},问题行 {report.get('totalErrors') or 0}。" + (" **还不能开排**,详见下表。" if not report.get("canSchedule") else " **基本可以开排**,详见下表。") )) ), blocks=[block], ) if name == "folder.schedule": from server.aps_domain.folder_pack import analyze_work_dir, confirmation_for_folder_schedule # 排产口令:强制用工程目录 SQL/表覆盖当前项目世界 report = analyze_work_dir(store.data, session_id, force_sql_replace=True) store.save() analyze_block = UIBlock(blockId="folder-pack", type="folder-pack", props={ "projectName": report.get("projectName"), "workDir": report.get("workDir"), "files": report.get("files") or [], "coverage": report.get("coverage") or {}, "coverageDetail": report.get("coverageDetail") or [], "missing": report.get("missing") or [], "softMissing": report.get("softMissing") or [], "canSchedule": bool(report.get("canSchedule")), "totalOk": report.get("totalOk") or 0, "totalErrors": report.get("totalErrors") or 0, "error": report.get("error"), "sqlApplied": bool(report.get("sqlApplied")), "sqlStats": report.get("sqlStats") or {}, }) sql_ok = bool(report.get("sqlApplied")) and bool(report.get("canSchedule")) has_core = ( bool(store.data.get("flexOrders")) and bool(store.data.get("flexRoutings")) and any(e.get("status") == "RUNNING" for e in (store.data.get("flexEquipment") or [])) ) # SQL 完整库:用户已说「根据这个排产」→ 直接开排,不再卡在「缺 Excel 批次/再确认」 if sql_ok or (report.get("sqlApplied") and has_core): st = report.get("sqlStats") or {} write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="folder.schedule.sql_direct", target={"type": "FOLDER_PACK", "id": report.get("workDir") or "workDir"}, power="P2", rationale={"sqlStats": st, "direct": True}) store.save() flex_reply = _run_flex( store, IntentResult(intent="flex.schedule", params={"sortMode": "BOTTLENECK"}, confidence=1.0, source="RULE_FAST"), actor, ) head = ( f"已按工程目录 SQL 入库并开排:" f"订单 {st.get('orders', 0)} · 工艺步 {st.get('routing', 0)} · " f"已挂工艺产品 {st.get('routedProducts', 0)}/" f"{st.get('orderProducts', 0)}。\n\n" ) return AgentReply( text=head + (flex_reply.text or ""), blocks=[analyze_block, *(flex_reply.blocks or [])], commands=flex_reply.commands or [], ) if not report.get("batches") and not report.get("sqlApplied"): return AgentReply( text=(report.get("markdown") or "") + "\n\n目录里还没有可入库的有效行,我没法按这些表开排。" "请先把订单/物料/工艺/设备表或 SQL 库补齐。", blocks=[analyze_block], ) if report.get("missing") and not has_core: return AgentReply( text=(report.get("markdown") or "") + "\n\n**还不能排:**还缺 " + "、".join(report["missing"]) + "。补齐后再说「根据这些数据排产」。", blocks=[analyze_block], ) title, lines = confirmation_for_folder_schedule(report) slim = [{"kind": b["kind"], "sheet": b.get("sheet"), "okRows": b.get("okRows") or []} for b in report["batches"] if b.get("okRows")] params = { "workDir": report.get("workDir"), "sessionId": session_id, "batches": slim, "sortMode": "BOTTLENECK", "sqlApplied": bool(report.get("sqlApplied")), } block = harness.stage_confirmation( session_id, "folder.schedule", params, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="folder.schedule.stage", target={"type": "FOLDER_PACK", "id": report.get("workDir") or "workDir"}, power="P2", rationale={"confirmId": block.props["confirmId"], "totalOk": report.get("totalOk")}) store.save() return AgentReply( text=(report.get("markdown") or "") + "\n\n必备数据已齐。下面请确认:**导入这些表格并试排一版**(可回滚)。", blocks=[analyze_block, block], ) if name in ("assistant.reply", "unknown"): from server.agent_core.assistant import reply as assistant_reply q = str(intent.params.get("query") or intent.params.get("text") or "") hist = intent.params.get("_history") or [] return await assistant_reply(store.data, q, history=hist, session_id=session_id) if name == "flex.time.update": pc = str(intent.params.get("productCode") or "") op_code = str(intent.params.get("operationCode") or "") std_min = intent.params.get("stdMin") if not op_code or not std_min: return AgentReply(text="请说明工序与工时,例如「把部装工时改成 50 分钟」" "或「把 0503727Z1M 工时改成 45 分钟」。") # 允许用工序名匹配(「部装」→ 所有名称含部装的工序编码) codes = [op_code] if not any(r.get("operationCode") == op_code for r in store.data.get("flexRoutings") or []): named = [o["code"] for o in store.data.get("flexOperations") or [] if op_code in (o.get("name") or "")] if named: codes = named staged_blocks = [] title = f"更新工时:{pc or '全部产品'} × {'、'.join(codes)} → {std_min} 分钟/件" block = harness.stage_confirmation( session_id, "flex.time.update", {"productCode": pc, "operationCode": codes[0] if len(codes) == 1 else "", "operationCodes": codes, "stdMin": float(std_min), "source": "实测"}, title=title, summary_lines=[f"匹配工序:{'、'.join(codes)}", f"单件工时 → {std_min} 分钟(来源标记为「实测」)", "确认后写入柔性工艺路线并同步数据库。"]) staged_blocks.append(block) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="flex.time.update.stage", target={"type": "ROUTING_TIME", "id": f"{pc or '*'}/{op_code}"}, power="P2", rationale={"confirmId": block.props["confirmId"], "stdMin": std_min}) store.save() return AgentReply(text=f"{title} 属于 P2 写操作,请确认。", blocks=staged_blocks) if name == "skill.register": payload = dict(intent.params or {}) if not payload.get("skill_id") or not payload.get("endpoint"): return AgentReply(text="登记 skill 需要 skill_id 与 endpoint,例如:" "「登记算法skill id=algo.x endpoint=http://127.0.0.1:8101 name=我的算法」") title = f"登记算法 Skill {payload.get('skill_id')}" lines = [f"ID:{payload.get('skill_id')}", f"端点:{payload.get('endpoint')}", f"名称:{payload.get('name') or payload.get('skill_id')}", f"轨道:{payload.get('track') or 'flex'}"] block = harness.stage_confirmation(session_id, "skill.register", payload, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="skill.register.stage", target={"type": "SKILL", "id": payload.get("skill_id")}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2,确认后写入配置。", blocks=[block]) if name == "skill.enable": sid = intent.params.get("skillId") or intent.params.get("skill_id") if not sid: return AgentReply(text="请指定 skillId,例如「启用 skill algo.stub」或「停用 skill algo.stub」。") enabled = intent.params.get("enabled") if enabled is None: enabled = not re.search(r"停用|禁用|关闭", str(intent.params.get("query") or "")) title = f"{'启用' if enabled else '停用'}算法 Skill {sid}" block = harness.stage_confirmation( session_id, "skill.enable", {"skill_id": sid, "enabled": bool(enabled)}, title=title, summary_lines=[f"目标:{sid}", f"状态 → {'启用' if enabled else '停用'}"]) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="skill.enable.stage", target={"type": "SKILL", "id": sid}, power="P2", rationale={"confirmId": block.props["confirmId"], "enabled": enabled}) store.save() return AgentReply(text=f"{title} 属于 P2,请确认。", blocks=[block]) # ---- 报告生成(P1:产出文档并入知识库 §9.10) ---- if name == "report.generate": report_type = str(intent.params.get("reportType") or "daily") # 报告类型(缺省日报) order_no = str(intent.params.get("orderNo") or "").strip() or None report = build_report(store.data, report_type, order_no=order_no) # 冻结快照 → 模板生成 if not report["reportId"]: # 前置条件不满足(无版本/不足两版) return AgentReply(text=report["markdown"]) # 报告入知识库(§9.10 规则3:报告本身成为 RAG 语料) asset = get_knowledge().add(kind="report", title=report["title"], content=report["markdown"], tags=[report_type, "报告"]) download_url = None download_name = None fmt = "md" xlsx = report.get("xlsxBytes") if xlsx and report_type in ("plan", "schedule-plan", "flex-plan"): saved = persist_report_xlsx(report["reportId"], xlsx, report.get("filename")) download_url = f"/api/reports/files/{report['reportId']}" download_name = saved["filename"] fmt = "xlsx" write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="report.generate", target={"type": "REPORT", "id": report["reportId"]}, power="P1", rationale={"reportType": report_type, "assetId": asset["assetId"], "orderNo": order_no, "format": fmt}) store.save() block = UIBlock( blockId=f"report-{report['reportId']}", type="report", props={"reportId": report["reportId"], "title": report["title"], "markdown": report["markdown"], "reportType": report_type, "assetId": asset["assetId"], "format": fmt, "downloadUrl": download_url, "filename": download_name}) tip = ("点下方「下载 Excel 工作计划表」保存完整工序计划。" if fmt == "xlsx" else "可预览并下载 Markdown。") return AgentReply( text=f"已生成【{report['title']}】(数字取自冻结快照)。\n{tip}" f"报告已入知识库(资产 {asset['assetId']})。", blocks=[block]) # ---- KPI 查询 / 仪表盘(P0,EX-08) ---- if name == "query.kpi": from server.aps_domain.analytics import build_kpi_dashboard dash = build_kpi_dashboard(store.data) lines = [f"· {c['label']}:{c['value']}" for c in dash.get("cards") or []] text = "运营 KPI 仪表盘:\n" + ("\n".join(lines) if lines else "暂无数据,请先排产。") block = UIBlock(blockId="kpi-dash", type="kpi-dashboard", props=dash) return AgentReply( text=text, blocks=[block], commands=[ViewportCommand(cmd="viewport.mode", params={"mode": "kpi"}, issuedBy="LLM")]) # ---- 主动引导(P0,AG-07) ---- if name == "guidance.next": from server.aps_domain.guidance import build_guidance guide = build_guidance(store.data) lines = [f"· {s['label']}({s['reason']})" for s in guide.get("suggestions") or []] sig = guide.get("signals") or [] head = "当前建议下一步:\n" if not sig else f"状态:{sig[0]['title']}。建议:\n" return AgentReply( text=head + ("\n".join(lines) if lines else "暂无特别建议,可说「帮助」。"), blocks=[UIBlock(blockId="guidance-next", type="guidance", props=guide)]) # ---- 排产参数查询 / 更新(OR-02) ---- if name == "params.query": from server.aps_domain.params import get_schedule_params p = get_schedule_params(store.data) lw = p["customerLevelWeights"] ow = p["weights"] text = ( "当前排产参数:\n" f"· 客户等级权重:VIP={lw['VIP']} / A={lw['A']} / B={lw['B']} / C={lw['C']}\n" f"· 目标权重:交期={ow['tardiness']} / 成本={ow['cost']} / 利用率={ow['utilization']} / 均衡={ow['balance']}\n" f"· 展望期 {p['planningHorizonDays']} 天\n" "可在设置 → 排产参数调整,或说「提高VIP权重到10」「恢复默认排产参数」。" ) return AgentReply(text=text) if name == "params.update": from server.aps_domain.params import confirmation_for_params_update raw = dict(intent.params or {}) payload: dict[str, Any] = {} if raw.get("resetDefaults"): payload["resetDefaults"] = True else: levels: dict[str, float] = {} mapping = {"vipWeight": "VIP", "aWeight": "A", "bWeight": "B", "cWeight": "C", "VIP": "VIP", "A": "A", "B": "B", "C": "C"} for src, dst in mapping.items(): if raw.get(src) is not None and src in ("vipWeight", "aWeight", "bWeight", "cWeight", "VIP", "A", "B", "C"): try: levels[dst] = float(raw[src]) except (TypeError, ValueError): return AgentReply(text=f"{dst} 权重必须是数字。") if raw.get("customerLevelWeights"): payload["customerLevelWeights"] = raw["customerLevelWeights"] elif levels: payload["customerLevelWeights"] = levels if raw.get("weights"): payload["weights"] = raw["weights"] if raw.get("planningHorizonDays") is not None: payload["planningHorizonDays"] = raw["planningHorizonDays"] try: title, lines = confirmation_for_params_update(store.data, payload) except ValueError as exc: return AgentReply(text=f"排产参数有误:{exc}") block = harness.stage_confirmation(session_id, "params.update", payload, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="params.update.stage", target={"type": "SCHEDULE_PARAMS", "id": "scheduleParams"}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 写操作,需要你确认。", blocks=[block]) # ---- 约束配置(SC-04) ---- if name == "constraint.profile.query": from server.aps_domain.constraints import get_constraint_profile profile = get_constraint_profile(store.data) lines = [] for c in profile["constraints"]: if not c.get("configurable") and c["enabled"]: continue state = "开" if c["enabled"] else "关" kind = "硬" if c["kind"] == "hard" else "软" flag = "" if c.get("configurable") else "(内建)" lines.append(f"· [{c['code']}] {c['name']}:{state}/{kind}{flag}") text = f"约束剖面【{profile['name']}】:\n" + "\n".join(lines) text += "\n可说「关闭物料齐套」「齐套改为硬约束」或在设置 → 约束配置调整。" return AgentReply(text=text) if name == "constraint.profile.save": from server.aps_domain.constraints import confirmation_for_profile_save raw = dict(intent.params or {}) payload: dict[str, Any] = {} if raw.get("resetDefaults"): payload["resetDefaults"] = True elif raw.get("constraints"): payload["constraints"] = raw["constraints"] else: cid = str(raw.get("constraintId") or "") if not cid: return AgentReply(text="请指定约束,例如:「关闭物料齐套」或「齐套改为硬约束」。") patch: dict[str, Any] = {} if "enabled" in raw: patch["enabled"] = bool(raw["enabled"]) if raw.get("kind") in ("hard", "soft"): patch["kind"] = raw["kind"] payload["constraints"] = {cid: patch} try: title, lines = confirmation_for_profile_save(store.data, payload) except ValueError as exc: return AgentReply(text=f"约束配置有误:{exc}") block = harness.stage_confirmation( session_id, "constraint.profile.save", payload, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="constraint.profile.save.stage", target={"type": "CONSTRAINT_PROFILE", "id": "default"}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 写操作,需要你确认。", blocks=[block]) # ---- IND-02 SOP→约束 ---- if name == "sop.compile": from server.aps_domain.sop_rules import compile_sop_by_query, list_compilable_sops, sop_as_block q = str((intent.params or {}).get("query") or "换线") compiled = compile_sop_by_query(get_knowledge().assets, q) if not compiled.get("ok"): avail = compiled.get("available") or list_compilable_sops(get_knowledge().assets) titles = "、".join(a.get("title") or "?" for a in avail[:6]) return AgentReply(text=f"{compiled.get('error')}\n可编译 SOP:{titles or '(无)'}") pack = compiled["pack"] write_audit(store.data, store.next_id, actor=actor, category="ALGO_RUN", action="sop.compile", target={"type": "SOP", "id": (pack.get("source") or {}).get("assetId")}, power="P0", rationale={"query": q, "effects": len(pack.get("effects") or [])}) store.save() text = ( f"已编译 SOP《{pack.get('title')}》→ 规则包预览。\n" f"{pack.get('summary')}\n" "确认落地请说「应用换线SOP」或「应用该规则包」(P2)。" ) return AgentReply(text=text, blocks=[sop_as_block(pack)]) if name == "sop.apply": from server.aps_domain.sop_rules import ( compile_sop_by_query, confirmation_for_sop_apply, ) raw = dict(intent.params or {}) pack = raw.get("pack") if isinstance(raw.get("pack"), dict) else None if pack is None: compiled = compile_sop_by_query(get_knowledge().assets, raw.get("query") or "换线") if not compiled.get("ok"): return AgentReply(text=compiled.get("error") or "SOP 编译失败") pack = compiled["pack"] title, lines = confirmation_for_sop_apply(pack) block = harness.stage_confirmation( session_id, "sop.apply", {"pack": pack, "query": raw.get("query")}, title=title, summary_lines=lines) write_audit(store.data, store.next_id, actor=actor, category="GATE", action="sop.apply.stage", target={"type": "RULE_PACK", "id": pack.get("packId")}, power="P2", rationale={"confirmId": block.props["confirmId"]}) store.save() return AgentReply(text=f"{title} 属于 P2 写操作,需要你确认。", blocks=[block]) # ---- 帮助(P0) ---- if name == "help": return AgentReply(text=_HELP) # ---- 拒识兜底:走通用助理(带上下文)---- from server.agent_core.assistant import reply as assistant_reply return await assistant_reply( store.data, str(intent.params.get("query") or ""), history=intent.params.get("_history") or [], session_id=session_id, )