aps-agent/server/aps_domain/workflow.py

2446 lines
147 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 排产工作流编排(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,
)