aps-agent/server/agent_core/harness.py

270 lines
20 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.

# ============================================================
# 门禁 Harness v1(moduleId: core-harness, 可重生 ✅)
# plan.md §3.3:所有写操作的唯一入口。M1 实现:
# - 权力分级:P0/P1 放行;P2 生成确认卡(人工批准后才执行)
# - 待确认队列:confirmId → 待执行动作(会话内存)
# - 执行/驳回均写审计(§3.6)
# ============================================================
from __future__ import annotations # 前向类型引用
import uuid # 确认卡 ID
from typing import Any, Callable # 类型标注
from server.contracts import UIAction, UIBlock # 确认卡的 UI 块契约
# 待确认动作表:confirmId → {action, params, summary, sessionId}
# M1 用模块级内存(单进程);M2 起随会话树持久化
_pending: dict[str, dict[str, Any]] = {}
# 动作 → 权力等级 映射(封闭表:未登记的动作一律拒绝)
# ⚠ 文档同步铁律(plan.md §12.9):改本表必同步 docs/architecture/harness.md 权力矩阵
_POWER_MAP: dict[str, str] = {
"schedule.run": "P1", # 试排:写草稿版本(沙盒语义)
"schedule.publish": "P2", # 发布:写主干世界状态 → 必须人工确认
"order.upsert": "P2", # 订单新增/编辑:写主干订单池 → 必须人工确认
"order.cancel": "P2", # 订单取消:影响后续排产输入 → 必须人工确认
"order.complete": "P2", # 订单完成:影响后续排产输入 → 必须人工确认
"order.delete": "P2", # 订单删除:物理移除订单池 → 必须人工确认
"order.clear": "P2", # 一键清空订单池 → 必须人工确认
"order.submit": "P1", # OR-03:提交审核(不写批准态)
"order.approve": "P2", # OR-03:批准进排产池
"order.reject": "P2", # OR-03:驳回
"order.pool": "P0", # OR-03:订单池摘要
"rush.evaluate": "P1", # OR-04:插单影响快评(沙盒)
"rush.apply": "P2", # OR-04:采用插单写主干+草稿排产
"forecast.query": "P0", # OR-05:预测订单只读
"forecast.upsert": "P2", # OR-05:新建/编辑预测
"forecast.delete": "P2", # OR-05:删除预测
"forecast.convert": "P2", # OR-05:预测转正为销售订单
"plan.buckets": "P0", # PL-01:时间分桶计划(只读)
"plan.rccp": "P0", # PL-02:有限/无限粗能力对照(只读)
"plan.feasibility": "P0", # PL-03:交期可行性(只读)
"plan.inventory": "P0", # PL-04:库存投影(只读)
"plan.leveling": "P0", # PL-05:产能平衡/削峰建议(只读)
"plan.supply": "P0", # PL-06:产供方向决策(只读)
"data.import": "P2", # 批量导入订单/物料:写主干 → 必须人工确认
"import.commit": "P2", # Excel/CSV 导入入库:写主干 → 必须人工确认
"flex.site.load": "P2", # 现场生产路线:替换 flex* 主数据 → 确认卡
"master.line.upsert": "P2", # 产线编辑/停启用:改后续新排产资源池 → 必须人工确认
"master.material.upsert": "P2", # 物料新建/全字段/停用:改齐套与产品池 → 必须人工确认
"master.maintenance.upsert": "P2", # 维保新增/取消:改设备避让窗口 → 必须人工确认
"master.bom.upsert": "P2", # BOM 增删改:改齐套需求与采购建议 → 必须人工确认
"master.routing.upsert": "P2", # 工艺路线/步骤:改占槽工时与外协标记 → 必须人工确认
"master.operation.upsert": "P2", # 工序库:改路线可选步骤 → 必须人工确认
"master.lineProduct.upsert": "P2", # 产线-产品绑定:改选线依据 → 必须人工确认
"master.changeover.upsert": "P2", # MD-06:换型矩阵写入 → 必须人工确认
"master.clear": "P2", # 一键清理主数据板块(回种)→ 必须人工确认
"master.query": "P0", # 自然语言检索订单/主数据(只读)
"changeover.query": "P0", # MD-06:换型矩阵只读
"campaign.preview": "P0", # SC-08:战役合并预览(只读)
"params.update": "P2", # OR-02:排产参数/客户等级权重 → 必须人工确认
"params.query": "P0", # OR-02:查看当前排产参数(只读)
"constraint.profile.query": "P0", # SC-04:约束剖面只读
"constraint.profile.save": "P2", # SC-04:约束剖面保存
"order.decompose": "P1", # 订单分解:产出采购/委外建议草稿,不碰主数据
"mrp.release": "P2", # MRP 下达:草稿建议单转正式采购/委外 → 必须人工确认
"flex.schedule": "P1", # 柔性排产:能力池+虚拟产线,只写 flex* 草稿表
"flex.reschedule": "P2", # 分级重排 L2/L3/L4:影响面大 → 确认卡
"flex.swap": "P1", # L1 局部换机:只改正目标工单
"flex.capacity": "P0", # 瓶颈产能评估:只读能力池算日产能
"flex.simulate_due": "P0", # 交期承诺模拟:深拷贝沙盒试排,不碰主干
"flex.compare": "P0", # 三模式对比:沙盒试排,不碰主干
"flex.rush": "P1", # 紧急插单:写 flexOrders + 重排草稿
"flex.fault": "P1", # 设备故障:缩池 + 可选重排
"flex.resource.patch": "P1", # 柔性资源补丁:设备状态/区域、模具寿命/锁定
"flex.conflict.resolve": "P1", # EX-03:冲突安全修复(重排类另走 P2 stage)
"conflict.list": "P0", # EX-03:冲突中心只读
"flex.adjust.preview": "P0", # EX-04:拖拽调程预览(只读校验)
"flex.adjust.commit": "P2", # EX-04:拖拽调程提交(新草稿版本)
"schedule.adjust.preview": "P0", # EX-04:固定轨调程预览
"schedule.adjust.commit": "P2", # EX-04:固定轨调程提交
"sap.status": "P0", # MD-05:SAP 连接状态(只读)
"sap.sync.inbound": "P2", # MD-05:SAP→APS 拉单/库存
"sap.sync.outbound": "P2", # MD-05:APS→SAP 开完工回写(Mock)
"mes.status": "P0", # EX-05:MES 连接状态
"mes.dispatch": "P3", # EX-05:下发车间(外部副作用)
"mes.report": "P1", # EX-09:报工回流写工单进度
"scenario.compare": "P1", # 方案对比:只写 Explore 沙盒(§5.1)
"scenario.sensitivity": "P1", # SC-06:敏感性分析沙盒 Tornado
"sop.compile": "P0", # IND-02:SOP 编译预览
"sop.apply": "P2", # IND-02:应用 SOP 规则包
"checkpoint.create": "P1", # 建档:只新增快照,不改世界
"checkpoint.rollback": "P2", # 回滚:整体替换主干世界 → 必须人工确认
"data.reset": "P2", # 重置数据:破坏性 → 必须人工确认
"knowledge.query": "P0", # 知识检索:只读(M3 §8.2)
"guidance.next": "P0", # AG-07:主动引导下一步建议(只读)
"project.create": "P1", # AG-08:新建项目(不改世界)
"project.delete": "P1", # AG-08:删项目元数据(不删世界)
"session.create": "P1", # AG-08:新建会话
"message.append": "P1", # AG-08:追加/同步会话消息
"message.replace": "P1", # AG-08:整包替换会话消息
"workspace.replace": "P1", # AG-08:整包同步工作区(迁移用)
"plan.trace": "P0", # 计划追溯:只读单据链
"report.generate": "P1", # 报告生成:产出文档并入知识库(M3 §9.10 规则3)
"viewport.*": "P0", # 视口命令:纯视图状态
"query.*": "P0", # 查询:只读
}
# 各动作的中文说明(门禁管理台"权力矩阵"页签展示用;与 _POWER_MAP 键集合一致)
_POLICY_DESC: dict[str, str] = {
"schedule.run": "试排:只产草稿版本,不动已发布主干",
"schedule.publish": "发布:写主干并推进订单状态,影响下游执行",
"order.upsert": "新增/编辑销售订单:改后续排产输入",
"order.cancel": "取消销售订单:从后续排产输入中排除",
"order.complete": "完成销售订单:从后续排产输入中排除",
"order.delete": "删除销售订单:从订单池物理移除",
"order.clear": "一键清空全部销售订单与采购/委外建议",
"order.submit": "订单提交审核:DRAFT/CHANGED → SUBMITTED",
"order.approve": "订单批准:SUBMITTED/CHANGED → APPROVED(可排产)",
"order.reject": "订单驳回:SUBMITTED/CHANGED → REJECTED",
"order.pool": "订单池摘要:待审/已批/变更计数(只读)",
"rush.evaluate": "插单快评:沙盒对比受影响订单/延迟/冲突(不改主干)",
"rush.apply": "采用插单:写急单入池并生成 DRAFT 排产版本(写前自动建档)",
"forecast.query": "预测订单查询:长周期需求台账(只读)",
"forecast.upsert": "新建/编辑预测订单(不默认进正式排产)",
"forecast.delete": "删除预测订单台账",
"forecast.convert": "预测转正为 APPROVED 销售订单",
"plan.buckets": "时间分桶计划:日/周/月/混合粗能力对照(只读)",
"plan.rccp": "有限/无限产能粗评估对照(RCCP,只读)",
"plan.feasibility": "交期可行性:累计粗能力能否按期(前置于排产)",
"plan.inventory": "库存投影:物料随时间可用量曲线(只读)",
"plan.leveling": "产能平衡/削峰:超载桶挪到空档建议(只读)",
"plan.supply": "产供方向:加班/扩线/外协结构化建议(只读)",
"data.import": "自然语言批量导入订单或物料主数据",
"import.commit": "Excel/CSV 导入入库(校验通过行写入主干)",
"flex.site.load": "加载现场完整生产路线:清空 flex* 演示种子并写入现场订单/路线/BOM",
"master.line.upsert": "编辑/停启用产线:改后续新排产的资源池",
"master.material.upsert": "新建/编辑/停用物料:改齐套检查与可选产品池",
"master.maintenance.upsert": "新增/取消维保:改设备避让窗口",
"master.bom.upsert": "BOM 头/明细增删改:改齐套需求与采购建议输入",
"master.routing.upsert": "工艺路线/步骤增删改:改占槽工时与外协标记",
"master.operation.upsert": "工序库新建/编辑:改路线可选步骤",
"master.lineProduct.upsert": "产线-产品绑定:改排产选线依据(排产前必需)",
"master.changeover.upsert": "换型矩阵:产品族切换准备时间(分钟)",
"master.clear": "一键清理主数据板块:按范围清空(非回种)",
"master.query": "查询订单/物料/工序/BOM/工艺路线/主数据概览(只读)",
"changeover.query": "换型矩阵只读查询",
"campaign.preview": "战役/批次合并预览(同产品交期窗口)",
"params.update": "排产参数:客户等级权重/目标权重/展望期(只影响新版本)",
"params.query": "排产参数查询:只读当前客户等级与目标权重",
"constraint.profile.query": "约束配置查询:只读硬/软约束剖面",
"constraint.profile.save": "约束配置保存:启停/硬软切换(只影响新版本与发布门禁)",
"order.decompose": "订单分解:按 BOM/工艺产出采购与委外建议(草稿)",
"mrp.release": "MRP 下达:草稿采购/委外建议单确认转为正式(RELEASED)",
"flex.schedule": "柔性排产:设备能力池动态组虚拟产线,只产草稿版本",
"flex.reschedule": "分级重排:L2短窗/L3日窗/L4全局(需确认)",
"flex.swap": "L1局部换机:池内备机接手,他单不动",
"flex.capacity": "瓶颈产能:按能力池实时算各工序日产能与限制性瓶颈",
"flex.simulate_due": "交期承诺:询单沙盒试排,给乐观/预计/悲观完工与缺口建议",
"flex.compare": "三模式对比:正排/倒排/瓶颈锚沙盒 KPI 对比",
"flex.rush": "紧急插单:高优先柔性订单写入并瓶颈锚重排",
"flex.fault": "设备故障:设备退出能力池并可选全量重排",
"flex.resource.patch": "柔性资源补丁:设备停复机/调区、模具寿命与锁定",
"flex.conflict.resolve": "冲突修复:L1换机/解锁模具等安全修复(重排类走确认卡)",
"conflict.list": "冲突中心:列出最新版本未解决冲突与建议",
"flex.adjust.preview": "甘特调程预览:边拖边校验,不改世界",
"flex.adjust.commit": "甘特调程提交:生成新草稿版本(需确认)",
"schedule.adjust.preview": "固定甘特调程预览:工位占槽/先后/维保校验",
"schedule.adjust.commit": "固定甘特调程提交:生成新草稿版本(需确认)",
"sap.status": "SAP 连接状态:Mock 桩只读探测",
"sap.sync.inbound": "SAP 入站:拉生产订单与库存写入柔性世界",
"sap.sync.outbound": "SAP 出站:工序开完工回写(Mock,幂等)",
"mes.status": "MES 连接状态:Mock 桩只读探测",
"mes.dispatch": "MES 下发:排产工单推送车间(外部副作用,幂等)",
"mes.report": "MES 报工:进度回流更新工单状态",
"scenario.compare": "方案对比:多策略试排全程在深拷贝沙盒",
"scenario.sensitivity": "敏感性分析:参数扰动 Tornado(沙盒不写主干)",
"sop.compile": "SOP→约束编译预览(不写主干)",
"sop.apply": "应用 SOP 规则包到约束剖面/换型策略",
"checkpoint.create": "建档:只新增成对快照文件",
"checkpoint.rollback": "回滚:整体替换主干世界(时间旅行)",
"data.reset": "重置:清空重播种子数据",
"knowledge.query": "知识检索:只读命中知识资产,回答强制带出处",
"guidance.next": "主动引导:按空态/冲突/知识未命中给出下一步口令",
"project.create": "新建项目与默认会话(只写工作区元数据,不改排产世界)",
"project.delete": "删除项目及其会话/文件/消息(不删除世界状态)",
"session.create": "在项目下新建会话话题",
"message.append": "向会话追加一条消息",
"message.replace": "整包替换会话消息(前端防抖同步)",
"workspace.replace": "整包同步工作区(本地迁移到服务端)",
"plan.trace": "计划追溯/钉扎:固定 SO 链或柔性 FO→BOM→VL→WO→设备负荷(OR-06)",
"report.generate": "报告生成:冻结快照模板化产出,报告入知识库",
"viewport.*": "视口命令:纯前端视图状态(模式/过滤/聚焦/高亮)",
"query.*": "查询:只读(KPI/世界视图)",
}
def list_policy() -> list[dict[str, str]]:
"""权力矩阵投影(门禁管理台数据源;P0 只读,§6.10.2)。
Returns: [{action, power, desc, confirm}] 列表,顺序与 _POWER_MAP 声明一致。
"""
return [{
"action": action, # 动作名(支持前缀通配)
"power": power, # 权力等级 P0-P3
"desc": _POLICY_DESC.get(action, ""), # 中文说明
"confirm": power in ("P2", "P3"), # 是否需要人工确认
} for action, power in _POWER_MAP.items()]
def power_of(action: str) -> str:
"""查动作的权力等级(前缀通配;未登记按最高 P3 拒绝——白名单原则)。"""
if action in _POWER_MAP: # 精确命中
return _POWER_MAP[action]
prefix = action.split(".")[0] + ".*" # 前缀通配(viewport.* / query.*)
return _POWER_MAP.get(prefix, "P3") # 未登记 → P3(默认最严)
def needs_confirm(action: str) -> bool:
"""该动作是否需要人工确认(P2/P3 强制过卡,§3.2 权力边界)。"""
return power_of(action) in ("P2", "P3") # 写主干/外部副作用必须确认
def stage_confirmation(session_id: str, action: str, params: dict[str, Any],
title: str, summary_lines: list[str]) -> UIBlock:
"""把 P2 动作压入待确认队列并生成确认卡 UI 块(动作此刻并未执行)。
权力等级:P1(只登记意图与出卡,不触发任何写)。
"""
confirm_id = uuid.uuid4().hex[:12] # 生成确认 ID(防猜测)
_pending[confirm_id] = { # 登记待确认动作
"sessionId": session_id, "action": action, "params": params,
}
return UIBlock( # 确认卡:前端渲染为审批卡片
blockId=f"confirm-{confirm_id}", # 块 ID 复用确认 ID
type="confirm-card", # 块类型
props={ # 卡片内容
"confirmId": confirm_id, # 前端回传用
"title": title, # 卡片标题
"summary": summary_lines, # 影响面说明(逐行)
"power": power_of(action), # 展示权力等级
"action": action, # 待执行动作名
},
actions=[ # 批准/驳回两个动作
UIAction(actionId="confirm", label="批准执行", power=power_of(action), # type: ignore[arg-type]
payload={"confirmId": confirm_id}),
UIAction(actionId="reject", label="驳回", power="P0", payload={"confirmId": confirm_id}),
],
)
def take_confirmation(confirm_id: str) -> dict[str, Any] | None:
"""取出并移除一条待确认动作(一次性令牌语义:批准/驳回后即失效)。"""
return _pending.pop(confirm_id, None) # 不存在返回 None(过期/重放防护)
def list_pending() -> list[dict[str, Any]]:
"""列出全部待确认动作(门禁管理台"待审批"页签的数据源,§6.10.2)。"""
return [{"confirmId": cid, **info} for cid, info in _pending.items()] # 附带令牌一并返回(只读投影)
def guard(action: str, params: dict[str, Any], executor: Callable[[], Any]):
"""P0/P1 动作的直通执行护栏:校验权力等级后执行(P2/P3 禁止走此通道)。
权力等级:随 action 而定;本函数强制 P2/P3 抛错(必须走 stage_confirmation)。
"""
if needs_confirm(action): # P2/P3 误入直通通道
raise PermissionError(f"动作 {action} 为 {power_of(action)},必须经确认卡执行") # 硬拒绝
return executor() # P0/P1 直接执行