diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/pipeline_opportunity/__pycache__/__init__.cpython-310.pyc b/pipeline_opportunity/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index a116116..0000000 Binary files a/pipeline_opportunity/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/pipeline_opportunity/__pycache__/init.cpython-310.pyc b/pipeline_opportunity/__pycache__/init.cpython-310.pyc deleted file mode 100644 index 60d2eb1..0000000 Binary files a/pipeline_opportunity/__pycache__/init.cpython-310.pyc and /dev/null differ diff --git a/pipeline_opportunity/__pycache__/opp_ability.cpython-310.pyc b/pipeline_opportunity/__pycache__/opp_ability.cpython-310.pyc deleted file mode 100644 index 4ef77fc..0000000 Binary files a/pipeline_opportunity/__pycache__/opp_ability.cpython-310.pyc and /dev/null differ diff --git a/pipeline_opportunity/__pycache__/opp_common.cpython-310.pyc b/pipeline_opportunity/__pycache__/opp_common.cpython-310.pyc deleted file mode 100644 index 729cc08..0000000 Binary files a/pipeline_opportunity/__pycache__/opp_common.cpython-310.pyc and /dev/null differ diff --git a/pipeline_opportunity/__pycache__/opp_data_capability.cpython-310.pyc b/pipeline_opportunity/__pycache__/opp_data_capability.cpython-310.pyc deleted file mode 100644 index 786f667..0000000 Binary files a/pipeline_opportunity/__pycache__/opp_data_capability.cpython-310.pyc and /dev/null differ diff --git a/pipeline_opportunity/__pycache__/opp_report_capability.cpython-310.pyc b/pipeline_opportunity/__pycache__/opp_report_capability.cpython-310.pyc deleted file mode 100644 index b050d1b..0000000 Binary files a/pipeline_opportunity/__pycache__/opp_report_capability.cpython-310.pyc and /dev/null differ diff --git a/pipeline_opportunity/opp_ability.py b/pipeline_opportunity/opp_ability.py index b806b80..9fe6207 100644 --- a/pipeline_opportunity/opp_ability.py +++ b/pipeline_opportunity/opp_ability.py @@ -333,6 +333,32 @@ def register_opp_slash_commands(): register_slash_command(cmd) +OPP_DD_BIZ_TYPE = "opp_dev_approval" + + +def register_opp_dingtalk_hook(): + """把商机产线审批结论回流挂到钉钉审批回调上。 + + dingdingflow 已加载时注册成功;未加载(宿主没装钉钉模块)时静默跳过, + 发起审批会走降级人工通道,闭环不受影响。 + """ + try: + from ahserver.serverenv import ServerEnv + env = ServerEnv() + register_biz_handler = getattr(env, "register_biz_handler", None) + if register_biz_handler is None: + logger.info("[opp] dingdingflow 未加载,跳过钉钉审批钩子注册") + return False + from .opp_report_capability import on_dingtalk_approval_done + register_biz_handler(OPP_DD_BIZ_TYPE, on_dingtalk_approval_done) + logger.info("[opp] 钉钉审批钩子已注册 biz_type=%s", OPP_DD_BIZ_TYPE) + return True + except Exception as e: + logger.warning("[opp] 钉钉审批钩子注册失败(不影响降级人工): %s", str(e)[:200]) + return False + + # import 即注册(与投标产线一致:宿主 import pipeline_opportunity 即生效) register_opp_ability() register_opp_slash_commands() +register_opp_dingtalk_hook() diff --git a/pipeline_opportunity/opp_report_capability.py b/pipeline_opportunity/opp_report_capability.py index 948f175..edcfb53 100644 --- a/pipeline_opportunity/opp_report_capability.py +++ b/pipeline_opportunity/opp_report_capability.py @@ -12,6 +12,8 @@ import logging +from ahserver.serverenv import ServerEnv + from .opp_common import ( get_db, new_id, rec_to_dict, rows_to_dicts, can_transition, RP_DRAFT, RP_CONFIRMED, RP_APPROVAL_INITIATED, RP_APPROVED, RP_REJECTED, @@ -133,8 +135,17 @@ async def confirm_report(report_id, ok, operator=""): return True, "已确认" if ok else "已退回草稿" -async def initiate_approval(report_id, note="", created_by="agent.opportunity"): - """发起研发审批:要求报告已 confirmed(人工确认门禁)。""" +async def initiate_approval(report_id, note="", created_by="agent.opportunity", + applicant_id=""): + """发起研发审批:要求报告已 confirmed(人工确认门禁)。 + + 审批通道优先级: + 1. 钉钉审批——dda_approval_configs 配了 biz_type=opp_dev_approval 且启用时, + 调 dingdingflow.submit_approval 发起,审批人节点链由配置的 + approvers_config 决定;结果经回调写回(见 opp_common 注册的钩子)。 + 2. 降级人工——无钉钉配置/发起失败时,落平台内人工审批任务兜底, + 保证产线闭环不因缺少钉钉环境而卡死。 + """ db, dbname = get_db() async with db.sqlorContext(dbname) as sor: rep = await get_report(sor, report_id) @@ -153,12 +164,40 @@ async def initiate_approval(report_id, note="", created_by="agent.opportunity"): }) await sor.U("opp_reports", { "id": report_id, "status": RP_APPROVAL_INITIATED}) - # 发人工审批任务 - await create_human_task( - sor, rep.get("project_id", ""), HT_DEV_APPROVAL, - "研发审批:%s" % rep.get("title", ""), - note or "请审批《%s》的研发立项。" % rep.get("title", "")) await sor.sqlExe("COMMIT", {}) + + # 尝试钉钉审批通道 + dd_ok = False + env = ServerEnv() + submit_approval = getattr(env, "submit_approval", None) + if submit_approval is not None: + try: + org_id = rep.get("org_id", "") or "0" + r = await submit_approval( + "opp_dev_approval", aid, + "研发立项审批:%s" % rep.get("title", ""), + applicant_id or created_by, org_id) + if isinstance(r, dict) and r.get("success") and r.get("instance_id"): + dd_ok = True + logger.info("研发审批已推钉钉: report=%s instance=%s", + report_id, r.get("instance_id")) + else: + logger.warning("钉钉审批发起未成功(降级人工): report=%s msg=%s", + report_id, (r or {}).get("message", "")) + except Exception as e: + logger.warning("钉钉审批发起异常(降级人工): report=%s err=%s", + report_id, str(e)[:200]) + + if not dd_ok: + # 降级:平台内人工审批任务 + db, dbname = get_db() + async with db.sqlorContext(dbname) as sor: + await create_human_task( + sor, rep.get("project_id", ""), HT_DEV_APPROVAL, + "研发审批:%s" % rep.get("title", ""), + note or "请审批《%s》的研发立项。" % rep.get("title", "")) + await sor.sqlExe("COMMIT", {}) + return True, aid return True, aid @@ -194,6 +233,25 @@ async def resolve_approval(approval_id, result, operator=""): return True, result +async def on_dingtalk_approval_done(biz_id, status, approval_id, comment): + """钉钉审批结果回调钩子(dingdingflow register_biz_handler 签名)。 + + biz_id = opp_approvals.id(发起审批时传入)。 + status ∈ approved / rejected / cancelled,cancelled 按驳回处理 + (产线不能停在 approval_initiated 永久卡死)。 + """ + if status == "approved": + result = AP_APPROVED + else: + result = AP_REJECTED + ok, msg = await resolve_approval( + biz_id, result, operator="dingtalk:%s" % (approval_id or "")) + if ok: + logger.info("钉钉审批结论已回流: opp_approval=%s -> %s", biz_id, result) + else: + logger.warning("钉钉审批结论回流失败: opp_approval=%s msg=%s", biz_id, msg) + + async def list_approvals(project_id="", status=""): db, dbname = get_db() async with db.sqlorContext(dbname) as sor: