aps-agent/server/aps_domain/workflow.py

1932 lines
120 KiB
Python
Raw Normal View History

2026-07-21 11:05:57 +08:00
# ============================================================
# 排产工作流编排(moduleId: domain-workflow, 可重生 ✅)
# 职责:意图 → 动作 → AgentReply(文本 + 视口命令 + UI 块)
# 权力路由:P0/P1 直接执行;P2(发布/重置)经门禁出确认卡(§3.3)
# ============================================================
from __future__ import annotations # 前向类型引用
import re
2026-07-21 11:05:57 +08:00
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)
2026-07-21 11:05:57 +08:00
from server.knowledge import get_knowledge, get_preferences # 知识库与偏好仓(M3 §8)
from server.knowledge.retrieval import search as kn_search # 混合检索(带出处 §8.2)
from server.contracts import UIBlock # 报告/证据块组装
# 帮助文案:能力清单(help 意图与拒识兜底共用)
_HELP = "\n".join([
"可执行的排产能力:",
"· 查询现场数据:有哪些订单 / 查订单 102285668 / 这个订单有委外吗 / 查物料 / 查 BOM / 查工艺路线",
"· 知识库:机加工工艺怎么生成 / 车削工艺模式 / 装配工艺模式 / 知识清单",
2026-07-21 11:05:57 +08:00
"· 生成排产草案:试排一版交期优先 / 产能均衡重排 / 用规则引擎试排",
"· 柔性排产(能力池+虚拟产线):跑一版柔性排产 / 把这个订单排产 / 正排 / 倒排 / 瓶颈锚",
"· 瓶颈产能:瓶颈产能 / 各工序能做多少(按能力池实时算)",
"· 交期承诺:某产品 N 套什么时候能交",
2026-07-21 11:05:57 +08:00
"· 方案评估:多策略方案对比(沙盒运行,不影响当前版本)",
"· 紧急插单:插单快评 / 采用插单(需确认)",
"· 预测订单:查看预测 / 新建预测 / 预测纳入试排",
"· 切到负荷热力图 / 交期承诺看板 / 甘特图 / 柔性甘特",
2026-07-21 11:05:57 +08:00
"· 标出超期订单 / 查看冲突 / 按小时看 / 重置视图",
"· 建一个检查点 / 回滚到上一个检查点(需确认)",
"· 换线有什么规定 / @知识:机加工工艺路线生成总则(知识库带出处)",
"· 生成排产方案报告(下载 Excel 工作计划表) / 生成日报 / 生成版本对比报告",
"· 查看当前KPI / 下一步建议 / 发布当前排产版本(需确认)/ 重置数据(需确认)",
2026-07-21 11:05:57 +08:00
])
# 策略模板的中文名(回复文案用)
_STRATEGY_CN = {"DELIVERY_FIRST": "交期优先", "CAPACITY_BALANCE": "产能均衡",
"COST_FIRST": "成本最优(换型代理)", "CHANGEOVER_MIN": "换型最小化",
"CAMPAIGN": "战役合并", "FIFO": "先进先出", "COMPREHENSIVE": "综合优化"}
# OR-04:最近一次插单快评(供「采用插单」复用;进程内存)
_LAST_RUSH_EVAL: dict[str, Any] = {}
2026-07-21 11:05:57 +08:00
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)
2026-07-21 11:05:57 +08:00
params = EngineParams( # 组装引擎入参
orderIds=[], # M1:全部待排订单
engineType=intent.params.get("engine") or sp.get("defaultEngine") or "HYBRID",
2026-07-21 11:05:57 +08:00
strategyTemplate=strategy, # 策略模板(点名或偏好)
planningHorizonDays=int(sp.get("planningHorizonDays") or 14), # 展望期(OR-02 可配)
2026-07-21 11:05:57 +08:00
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,
2026-07-21 11:05:57 +08:00
)
engine = get_engine(params.engineType) # CP / HYBRID 真管线;GA 仍 RULE 代跑
2026-07-21 11:05:57 +08:00
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})
2026-07-21 11:05:57 +08:00
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 痕)"
2026-07-21 11:05:57 +08:00
text = (f"排产完成 ✅ 版本 {result.versionNo}({_STRATEGY_CN.get(result.strategy, result.strategy)})\n"
f"生产订单 {result.poCount} 个 / 工单 {result.woCount} 个"
+ ("(含预测)" if params.includeForecast else "") + "\n"
2026-07-21 11:05:57 +08:00
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=[ # 联动命令:切甘特 + 重拉世界数据
2026-07-21 11:05:57 +08:00
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])
2026-07-21 11:05:57 +08:00
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"
"可说「查看冲突」或「打开约束配置」。"))
2026-07-21 11:05:57 +08:00
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])
2026-07-21 11:05:57 +08:00
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)} 项未解决硬约束冲突。"
"请先处理冲突或调整约束配置。")
2026-07-21 11:05:57 +08:00
# 回滚防线(§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"): # ---- 批准:订单写入 / 审核 ----
2026-07-21 11:05:57 +08:00
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")})
2026-07-21 11:05:57 +08:00
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']])}")
2026-07-21 11:05:57 +08:00
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)}。"
"后续可重新导入或新建订单。")
2026-07-21 11:05:57 +08:00
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.aps_domain.kangni_intake import load_site_into_world
get_checkpoints().create(store.data, label="现场路线导入前基线", reason="auto:flex.site.load",
conversation_note="批准加载完整生产路线")
meta = load_site_into_world(
store.data,
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 4),
)
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 == "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),"
"可在订单面板「分解建议」查看状态。")
2026-07-21 11:05:57 +08:00
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}(变更前已自动建档)。"
2026-07-21 11:05:57 +08:00
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}"
)
2026-07-21 11:05:57 +08:00
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
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"])
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),
"steps": _steps(v["id"]), # 工单级依据(点开可见)
} for v in vls]
lines.sort(key=lambda x: (x["due"] or "", x["orderNo"])) # 按交期排序,消除“混乱观感”
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,
})
# 同步生成 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)
2026-07-21 11:05:57 +08:00
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 有委外吗」。")
2026-07-21 11:05:57 +08:00
# ---- 排产(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)
2026-07-21 11:05:57 +08:00
# ---- 订单状态动作(P2 → 确认卡;对话与页面共用 action) ----
if name in ("order.cancel", "order.complete", "order.delete"):
2026-07-21 11:05:57 +08:00
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比亚迪,CTRL-C,200,2026-08-01,VIP\n客户 华为 产品 智能控制器A型 数量 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])
2026-07-21 11:05:57 +08:00
# ---- 订单分解(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")])
2026-07-21 11:05:57 +08:00
# ---- 方案对比(P1:Explore 沙盒,不碰主干 §5.1/§9.7) ----
if name == "scenario.compare":
from server.aps_domain.analytics import matrix_from_fixed_cards
2026-07-21 11:05:57 +08:00
text, block = compare_scenarios(store.data) # 沙盒并行试排三策略
block.props["table"] = matrix_from_fixed_cards(
block.props.get("cards") or [], baseline=block.props.get("baseline"))
2026-07-21 11:05:57 +08:00
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])
2026-07-21 11:05:57 +08:00
# ---- 手动建档(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", ""), "已切换视图。"),
2026-07-21 11:05:57 +08:00
"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) ----
if name == "knowledge.query":
kb = get_knowledge() # 知识库
kb.ensure_seed_assets() # 幂等补种机加工工艺模式等
q = str(intent.params.get("query") or "")
2026-07-21 11:05:57 +08:00
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))
2026-07-21 11:05:57 +08:00
if title_hint: # 显式引用优先(§4.7)
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 = kn_search(kb.assets, q, top_k=3)
2026-07-21 11:05:57 +08:00
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: # 未命中:诚实告知(禁止编造 §8.1)
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])
2026-07-21 11:05:57 +08:00
# 回答正文:直接引用最相关资产内容 + 出处(强制带出处 §8.1)
top = hits[0] # 最相关命中
text = (f"{top['content']}\n\n"
f"—— 出处:【{top['title']}】{top['version']}({top['kind']})")
block = UIBlock( # 证据块:全部命中带出处(前端渲染)
blockId=f"evidence-{top['assetId']}", type="evidence",
props={"hits": [{k: h[k] for k in ("assetId", "title", "kind", "version", "score", "snippet")}
for h in hits]})
return AgentReply(text=text, 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) # 冻结快照 → 模板生成
2026-07-21 11:05:57 +08:00
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"
2026-07-21 11:05:57 +08:00
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(
2026-07-21 11:05:57 +08:00
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) ----
2026-07-21 11:05:57 +08:00
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」「恢复默认排产参数」。"
)
2026-07-21 11:05:57 +08:00
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])
2026-07-21 11:05:57 +08:00
# ---- 帮助(P0) ----
if name == "help":
return AgentReply(text=_HELP)
# ---- 拒识兜底 ----
return AgentReply(text="这句话我没有把握理解对(置信度不足)。\n" + _HELP)