2026-07-21 11:05:57 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 门禁 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", # 订单完成:影响后续排产输入 → 必须人工确认
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"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 导入入库:写主干 → 必须人工确认
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"folder.analyze": "P0", # 工程目录深度解析:只读
|
|
|
|
|
|
"folder.schedule": "P2", # 导入工程目录并试排
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"flex.site.load": "P2", # 现场生产路线:替换 flex* 主数据 → 确认卡
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"master.line.upsert": "P2", # 产线编辑/停启用:改后续新排产资源池 → 必须人工确认
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"master.material.upsert": "P2", # 物料新建/全字段/停用:改齐套与产品池 → 必须人工确认
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"master.maintenance.upsert": "P2", # 维保新增/取消:改设备避让窗口 → 必须人工确认
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"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:约束剖面保存
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"order.decompose": "P1", # 订单分解:产出采购/委外建议草稿,不碰主数据
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"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:报工回流写工单进度
|
2026-07-21 11:05:57 +08:00
|
|
|
|
|
|
|
|
|
|
"scenario.compare": "P1", # 方案对比:只写 Explore 沙盒(§5.1)
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"scenario.sensitivity": "P1", # SC-06:敏感性分析沙盒 Tornado
|
|
|
|
|
|
"sop.compile": "P0", # IND-02:SOP 编译预览
|
|
|
|
|
|
"sop.apply": "P2", # IND-02:应用 SOP 规则包
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"checkpoint.create": "P1", # 建档:只新增快照,不改世界
|
|
|
|
|
|
"checkpoint.rollback": "P2", # 回滚:整体替换主干世界 → 必须人工确认
|
|
|
|
|
|
"data.reset": "P2", # 重置数据:破坏性 → 必须人工确认
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"readiness.query": "P0", # 数据齐备度检查:只读
|
|
|
|
|
|
"data.analyze": "P0", # 分析项目/文件数据与排产缺口:只读
|
|
|
|
|
|
"assistant.reply": "P0", # 通用助理:只读作答
|
|
|
|
|
|
"flex.time.update": "P2", # 工时维护:改柔性路线工时 → 确认卡
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"knowledge.query": "P0", # 知识检索:只读(M3 §8.2)
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"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", # 引导式排产向导(对话态,本身只读)
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"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:追加/同步会话消息
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"message.replace": "P1", # AG-08:整包替换会话消息
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"workspace.replace": "P1", # AG-08:整包同步工作区(迁移用)
|
|
|
|
|
|
"plan.trace": "P0", # 计划追溯:只读单据链
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"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": "完成销售订单:从后续排产输入中排除",
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"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 导入入库(校验通过行写入主干)",
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"folder.analyze": "工程目录深度解析:读表内容、样例与排产齐备度",
|
|
|
|
|
|
"folder.schedule": "导入工程目录数据包并柔性试排",
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"flex.site.load": "加载现场完整生产路线:清空 flex* 演示种子并写入现场订单/路线/BOM",
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"master.line.upsert": "编辑/停启用产线:改后续新排产的资源池",
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"master.material.upsert": "新建/编辑/停用物料:改齐套检查与可选产品池",
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"master.maintenance.upsert": "新增/取消维保:改设备避让窗口",
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"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": "约束配置保存:启停/硬软切换(只影响新版本与发布门禁)",
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"order.decompose": "订单分解:按 BOM/工艺产出采购与委外建议(草稿)",
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"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 报工:进度回流更新工单状态",
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"scenario.compare": "方案对比:多策略试排全程在深拷贝沙盒",
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"scenario.sensitivity": "敏感性分析:参数扰动 Tornado(沙盒不写主干)",
|
|
|
|
|
|
"sop.compile": "SOP→约束编译预览(不写主干)",
|
|
|
|
|
|
"sop.apply": "应用 SOP 规则包到约束剖面/换型策略",
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"checkpoint.create": "建档:只新增成对快照文件",
|
|
|
|
|
|
"checkpoint.rollback": "回滚:整体替换主干世界(时间旅行)",
|
|
|
|
|
|
"data.reset": "重置:清空重播种子数据",
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"readiness.query": "数据齐备度:产品→路线→工时→资源→日历逐项检查,输出缺失清单",
|
|
|
|
|
|
"data.analyze": "分析项目/附件数据:盘点现有主数据与订单,并列出排产缺口",
|
|
|
|
|
|
"assistant.reply": "通用排产助理:结合项目上下文与知识库作答",
|
|
|
|
|
|
"flex.time.update": "工时维护:更新产品×工序单件工时并标记来源(需确认)",
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"knowledge.query": "知识检索:只读命中知识资产,回答强制带出处",
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"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→缺口引导→模板推荐→试排",
|
2026-07-23 13:38:43 +08:00
|
|
|
|
"guidance.next": "主动引导:按空态/冲突/知识未命中给出下一步口令",
|
|
|
|
|
|
"project.create": "新建项目与默认会话(只写工作区元数据,不改排产世界)",
|
|
|
|
|
|
"project.delete": "删除项目及其会话/文件/消息(不删除世界状态)",
|
|
|
|
|
|
"session.create": "在项目下新建会话话题",
|
|
|
|
|
|
"message.append": "向会话追加一条消息",
|
|
|
|
|
|
"message.replace": "整包替换会话消息(前端防抖同步)",
|
|
|
|
|
|
"workspace.replace": "整包同步工作区(本地迁移到服务端)",
|
|
|
|
|
|
"plan.trace": "计划追溯/钉扎:固定 SO 链或柔性 FO→BOM→VL→WO→设备负荷(OR-06)",
|
2026-07-21 11:05:57 +08:00
|
|
|
|
"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(防猜测)
|
2026-07-28 02:12:46 +08:00
|
|
|
|
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"
|
2026-07-21 11:05:57 +08:00
|
|
|
|
_pending[confirm_id] = { # 登记待确认动作
|
|
|
|
|
|
"sessionId": session_id, "action": action, "params": params,
|
2026-07-28 02:12:46 +08:00
|
|
|
|
"tenantUuid": identity.tenant_uuid,
|
|
|
|
|
|
"ownerUserId": identity.user_id,
|
|
|
|
|
|
"projectId": project_id,
|
2026-07-21 11:05:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
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=[ # 批准/驳回两个动作
|
2026-07-23 13:38:43 +08:00
|
|
|
|
UIAction(actionId="confirm", label="批准执行", power=power_of(action), # type: ignore[arg-type]
|
|
|
|
|
|
payload={"confirmId": confirm_id}),
|
2026-07-21 11:05:57 +08:00
|
|
|
|
UIAction(actionId="reject", label="驳回", power="P0", payload={"confirmId": confirm_id}),
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def take_confirmation(confirm_id: str) -> dict[str, Any] | None:
|
|
|
|
|
|
"""取出并移除一条待确认动作(一次性令牌语义:批准/驳回后即失效)。"""
|
2026-07-28 02:12:46 +08:00
|
|
|
|
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)
|
2026-07-21 11:05:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_pending() -> list[dict[str, Any]]:
|
|
|
|
|
|
"""列出全部待确认动作(门禁管理台"待审批"页签的数据源,§6.10.2)。"""
|
2026-07-28 02:12:46 +08:00
|
|
|
|
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)
|
|
|
|
|
|
]
|
2026-07-21 11:05:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 直接执行
|