"""钉钉审批适配器 — 把 dingdingflow 模块接成产线的审批关卡。 设计:不重造审批状态机。产线引擎已内置 approval_gate 交互步骤 (executor 遇到即置 WAITING 挂起,approval_approve/reject 会 save_artifact + STATE_COMPLETED + resume_task 自动推进)。 本适配器只做两件事: 1. 注册 step_type='dingtalk_approval'(interactive),进入该步骤时 自动向钉钉发起审批实例; 2. 注册 dingdingflow 的 biz_type 回调钩子,钉钉审批结果回来时 调 approval_approve / approval_reject 推进产线。 biz_id 编码为 "{tenant_id}:{task_id}:{step_name}",回调据此定位步骤。 宿主集成(在 load_pipeline_service() 之后调用): from pipeline_service.dingtalk_approval import load_dingtalk_approval load_dingtalk_approval() 未安装 dingdingflow 时本模块自动降级:step_type 仍注册(步骤照常挂起 等人工在产线页面审批),只是不向钉钉推送。 """ import json import logging from ahserver.serverenv import ServerEnv from .step_registry import register_step_type from .human import approval_approve, approval_reject logger = logging.getLogger("pipeline.dingtalk_approval") STEP_TYPE = "dingtalk_approval" BIZ_TYPE = "pipeline_approval" BIZ_ID_SEP = ":" def _encode_biz_id(tenant_id, task_id, step_name): """审批记录里存的业务 ID,回调靠它定位产线步骤。""" return BIZ_ID_SEP.join([str(tenant_id), str(task_id), str(step_name)]) def _decode_biz_id(biz_id): """反解 biz_id。step_name 可能含分隔符,故只切前两段。""" parts = str(biz_id).split(BIZ_ID_SEP, 2) if len(parts) != 3: raise ValueError("bad biz_id: %s" % biz_id) return parts[0], parts[1], parts[2] async def notify_dingtalk_approval(task_id, step_name, step_type, version, input_data, step_config, tenant_id=""): """进入审批步骤时向钉钉发起审批实例。 由 executor 的交互步骤钩子调用(见 _INTERACTIVE_NOTIFIERS)。 失败不抛异常:钉钉推送失败不应让产线步骤变 FAILED, 步骤仍停在 WAITING,可在产线页面人工审批兜底。 """ env = ServerEnv() submit_approval = getattr(env, "submit_approval", None) if submit_approval is None: logger.warning("dingdingflow 未加载,步骤 %s 仅挂起等人工审批(未推钉钉)", step_name) return None if not isinstance(step_config, dict): step_config = {} title = step_config.get("approval_title") or "产线审批:%s" % step_name applicant_id = (step_config.get("applicant_id") or (input_data or {}).get("applicant_id", "") or "") biz_type = step_config.get("biz_type") or BIZ_TYPE biz_id = _encode_biz_id(tenant_id, task_id, step_name) try: result = await submit_approval(biz_type, biz_id, title, applicant_id, str(tenant_id) or "0") approval_id = (result or {}).get("approval_id", "") logger.info("钉钉审批已发起: step=%s approval_id=%s biz_id=%s", step_name, approval_id, biz_id) return approval_id except Exception as e: logger.error("钉钉审批发起失败(步骤仍可人工审批): step=%s err=%s", step_name, e) return None async def on_dingtalk_approval_done(biz_id, status, approval_id, comment): """钉钉审批结果回调 → 推进产线步骤。 dingdingflow 的 register_biz_handler 钩子签名。 status ∈ ('approved', 'rejected', 'cancelled') """ try: tenant_id, task_id, step_name = _decode_biz_id(biz_id) except ValueError as e: logger.error("回调 biz_id 解析失败: %s", e) return reviewer_id = "dingtalk:%s" % (approval_id or "") if status == "approved": raw = await approval_approve(tenant_id, task_id, step_name, reviewer_id, comment) elif status in ("rejected", "cancelled"): # cancelled 也走 reject:产线不能停在 WAITING 永久卡死 raw = await approval_reject(tenant_id, task_id, step_name, reviewer_id, comment) else: logger.warning("未知审批状态,忽略: %s", status) return try: parsed = json.loads(raw) if isinstance(raw, str) else (raw or {}) except (ValueError, TypeError): parsed = {} if parsed.get("success"): logger.info("产线步骤已推进: task=%s step=%s status=%s", task_id, step_name, status) else: logger.error("产线步骤推进失败: task=%s step=%s status=%s msg=%s", task_id, step_name, status, parsed.get("message")) def load_dingtalk_approval(): """注册钉钉审批步骤类型 + 回调钩子。""" env = ServerEnv() # 1. 注册为交互步骤:executor 遇到即置 WAITING 挂起,不需要 handler register_step_type(STEP_TYPE, { "display_name": "钉钉审批", "category": "interactive", "is_interactive": True, "description": "向钉钉发起审批,审批通过后产线继续,驳回则中止", "notifier": "notify_dingtalk_approval", }) # 2. 暴露通知函数(executor 交互步骤钩子 / 手工重推都能用) env.notify_dingtalk_approval = notify_dingtalk_approval # 3. 注册审批结果回调钩子到 dingdingflow register_biz_handler = getattr(env, "register_biz_handler", None) if register_biz_handler: register_biz_handler(BIZ_TYPE, on_dingtalk_approval_done) logger.info("dingtalk_approval loaded — biz_type=%s 钩子已注册", BIZ_TYPE) else: logger.warning("dingdingflow 未加载:step_type=%s 已注册但审批结果无法自动回流," "请先 load_dingdingflow()", STEP_TYPE) return True