139 lines
8.7 KiB
Python
139 lines
8.7 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 门禁 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", # 订单完成:影响后续排产输入 → 必须人工确认
|
|||
|
|
"master.line.upsert": "P2", # 产线编辑/停启用:改后续新排产资源池 → 必须人工确认
|
|||
|
|
"master.material.upsert": "P2", # 物料库存编辑:改齐套检查输入 → 必须人工确认
|
|||
|
|
"master.maintenance.upsert": "P2", # 维保新增/取消:改设备避让窗口 → 必须人工确认
|
|||
|
|
"master.bom.upsert": "P2", # BOM 明细编辑:改齐套需求与采购建议 → 必须人工确认
|
|||
|
|
"master.routing.upsert": "P2", # 工艺步骤编辑:改占槽工时与外协标记 → 必须人工确认
|
|||
|
|
"order.decompose": "P1", # 订单分解:产出采购/委外建议草稿,不碰主数据
|
|||
|
|
|
|||
|
|
"scenario.compare": "P1", # 方案对比:只写 Explore 沙盒(§5.1)
|
|||
|
|
"checkpoint.create": "P1", # 建档:只新增快照,不改世界
|
|||
|
|
"checkpoint.rollback": "P2", # 回滚:整体替换主干世界 → 必须人工确认
|
|||
|
|
"data.reset": "P2", # 重置数据:破坏性 → 必须人工确认
|
|||
|
|
"knowledge.query": "P0", # 知识检索:只读(M3 §8.2)
|
|||
|
|
"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": "完成销售订单:从后续排产输入中排除",
|
|||
|
|
"master.line.upsert": "编辑/停启用产线:改后续新排产的资源池",
|
|||
|
|
"master.material.upsert": "编辑物料库存:改后续新排产的齐套检查",
|
|||
|
|
"master.maintenance.upsert": "新增/取消维保:改设备避让窗口",
|
|||
|
|
"master.bom.upsert": "编辑 BOM 明细:改齐套需求与采购建议输入",
|
|||
|
|
"master.routing.upsert": "编辑工艺步骤:改占槽工时与外协标记",
|
|||
|
|
"order.decompose": "订单分解:按 BOM/工艺产出采购与委外建议(草稿)",
|
|||
|
|
"scenario.compare": "方案对比:多策略试排全程在深拷贝沙盒",
|
|||
|
|
"checkpoint.create": "建档:只新增成对快照文件",
|
|||
|
|
"checkpoint.rollback": "回滚:整体替换主干世界(时间旅行)",
|
|||
|
|
"data.reset": "重置:清空重播种子数据",
|
|||
|
|
"knowledge.query": "知识检索:只读命中知识资产,回答强制带出处",
|
|||
|
|
"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="P2", 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 直接执行
|