aps-agent/server/agent_core/harness.py

336 lines
24 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 导入入库:写主干 → 必须人工确认
"folder.analyze": "P0", # 工程目录深度解析:只读
"folder.schedule": "P2", # 导入工程目录并试排
"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", # 重置数据:破坏性 → 必须人工确认
"readiness.query": "P0", # 数据齐备度检查:只读
"data.analyze": "P0", # 分析项目/文件数据与排产缺口:只读
"assistant.reply": "P0", # 通用助理:只读作答
"flex.time.update": "P2", # 工时维护:改柔性路线工时 → 确认卡
"knowledge.query": "P0", # 知识检索:只读(M3 §8.2)
"knowledge.import": "P2", # RAG:导入知识文档切块入库
"skill.list": "P0", # 外部算法 skill 清单
"skill.health": "P0", # skill 健康检查
"skill.register": "P2", # 登记/更新 skill 配置
"skill.enable": "P2", # 启停 skill
"rag.query": "P0", # RAG 查询:外部 skill 消费知识库(只读,带鉴权)
"routing.template.apply": "P2", # 行业模板生成工艺路线 → 确认卡
"schedule.wizard": "P0", # 引导式排产向导(对话态,本身只读)
"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 导入入库(校验通过行写入主干)",
"folder.analyze": "工程目录深度解析:读表内容、样例与排产齐备度",
"folder.schedule": "导入工程目录数据包并柔性试排",
"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": "重置:清空重播种子数据",
"readiness.query": "数据齐备度:产品→路线→工时→资源→日历逐项检查,输出缺失清单",
"data.analyze": "分析项目/附件数据:盘点现有主数据与订单,并列出排产缺口",
"assistant.reply": "通用排产助理:结合项目上下文与知识库作答",
"flex.time.update": "工时维护:更新产品×工序单件工时并标记来源(需确认)",
"knowledge.query": "知识检索:只读命中知识资产,回答强制带出处",
"knowledge.import": "导入知识文档:PDF/docx/md 切块入库(需确认)",
"skill.list": "外部算法 skill 清单(只读)",
"skill.health": "外部算法 skill 健康检查(只读)",
"skill.register": "登记/更新外部算法 skill 端点配置",
"skill.enable": "启用或停用外部算法 skill",
"rag.query": "RAG 查询:命中知识 chunk + 出处(按 manifest ragScopes 鉴权)",
"routing.template.apply": "行业工艺模板实例化为产品路线(工时标「模板」,需确认)",
"schedule.wizard": "引导式排产向导:readiness→缺口引导→模板推荐→试排",
"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(防猜测)
from server.auth.context import get_identity
identity = get_identity()
if identity.user_id:
from server.state.projects import get_project_store
project_id = get_project_store().active_world_key()
else:
project_id = "default"
_pending[confirm_id] = { # 登记待确认动作
"sessionId": session_id, "action": action, "params": params,
"tenantUuid": identity.tenant_uuid,
"ownerUserId": identity.user_id,
"projectId": project_id,
}
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:
"""取出并移除一条待确认动作(一次性令牌语义:批准/驳回后即失效)。"""
pending = _pending.get(confirm_id)
if pending is None:
return None
from server.auth.context import get_identity
identity = get_identity()
if pending.get("tenantUuid") != identity.tenant_uuid:
return None
project_id = pending.get("projectId") or "default"
if project_id == "default":
if pending.get("ownerUserId") != identity.user_id:
return None
elif identity.user_id:
from server.state.projects import get_project_store
if get_project_store().active_world_key() != project_id:
return None
return _pending.pop(confirm_id, None)
def list_pending() -> list[dict[str, Any]]:
"""列出全部待确认动作(门禁管理台"待审批"页签的数据源,§6.10.2)。"""
from server.auth.context import get_identity
identity = get_identity()
if identity.user_id:
from server.state.projects import get_project_store
project_id = get_project_store().active_world_key()
else:
project_id = "default"
return [
{"confirmId": cid, **info}
for cid, info in _pending.items()
if info.get("tenantUuid") == identity.tenant_uuid
and info.get("projectId") == project_id
and (project_id != "default" or info.get("ownerUserId") == identity.user_id)
]
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 直接执行