run_wp_sd7_api_loop.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. #!/usr/bin/env python3
  2. """WP-SD7 live API loop: diagnose, create, approve, execute, verify, close.
  3. Passwords come from the environment and are never written to evidence.
  4. Does not hand-edit plan status in the database.
  5. """
  6. from __future__ import annotations
  7. import argparse
  8. import json
  9. import os
  10. import urllib.error
  11. import urllib.request
  12. from datetime import datetime, timedelta
  13. from pathlib import Path
  14. from gmssl import sm2
  15. ROOT = Path(__file__).resolve().parents[4]
  16. EVIDENCE = ROOT / "doc" / "plan" / "UAT留证" / "2026-08-17-智慧诊断正式落地"
  17. BASE = os.environ.get("AIDOP_API_BASE", "http://127.0.0.1:5005")
  18. SM2_PK = "84C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312"
  19. BATCH = datetime.now().strftime("%Y%m%d")
  20. TENANTS = {
  21. "A": {"account": "UATAdminA", "tenant_id": "838257186181189", "user_id": "838257187360837"},
  22. "B": {"account": "UATAdminB", "tenant_id": "838257212780613", "user_id": "838257213620293"},
  23. "DEMO": {"account": "UATDemoAdmin", "tenant_id": "838257237606469", "user_id": "838257238302789"},
  24. }
  25. def request(method: str, path: str, payload=None, token=None):
  26. headers = {"Accept": "application/json"}
  27. data = None
  28. if payload is not None:
  29. headers["Content-Type"] = "application/json"
  30. data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
  31. if token:
  32. headers["Authorization"] = "Bearer " + token
  33. req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
  34. try:
  35. with urllib.request.urlopen(req, timeout=120) as resp:
  36. raw = resp.read()
  37. return resp.status, json.loads(raw) if raw else {}
  38. except urllib.error.HTTPError as exc:
  39. raw = exc.read()
  40. try:
  41. body = json.loads(raw) if raw else {}
  42. except json.JSONDecodeError:
  43. body = {"raw": raw.decode("utf-8", errors="replace")}
  44. return exc.code, body
  45. def encrypt(value: str) -> str:
  46. return sm2.CryptSM2(public_key=SM2_PK, private_key=None, mode=1).encrypt(value.encode()).hex()
  47. def unwrap(body):
  48. if isinstance(body, dict) and "result" in body:
  49. return body.get("result")
  50. return body
  51. def as_dict(value, fallback=None):
  52. return value if isinstance(value, dict) else (fallback or {})
  53. def login(account: str, tenant_id: str, password: str, already_encrypted: bool = False) -> str:
  54. cipher = password if already_encrypted else encrypt(password)
  55. status, body = request(
  56. "POST",
  57. "/api/sysAuth/login",
  58. {"account": account, "password": cipher, "tenantId": tenant_id},
  59. )
  60. token = (unwrap(body) or {}).get("accessToken") if isinstance(unwrap(body), dict) else None
  61. if status != 200 or not token:
  62. raise SystemExit(f"login failed for {account}: HTTP {status} {body}")
  63. return token
  64. def diagnose(token: str, module: str, metric_code: str | None = None) -> dict:
  65. path = f"/api/AidopKanban/smart-diagnosis/{module}"
  66. if metric_code:
  67. path += f"?metricCode={metric_code}"
  68. status, body = request("GET", path, token=token)
  69. return {"http": status, "body": unwrap(body) or {}}
  70. def dashboard(token: str, module: str) -> dict:
  71. status, body = request("GET", f"/api/AidopKanban/dashboard-page/{module}", token=token)
  72. return {"http": status, "body": unwrap(body) or {}}
  73. def create_plan(token: str, diagnosis: dict, module: str) -> dict:
  74. root = diagnosis.get("root") or {}
  75. evidence = diagnosis.get("evidence") or {}
  76. items = evidence.get("items") or []
  77. if not items:
  78. return {"skipped": True, "reason": "no_evidence_items"}
  79. first = items[0]
  80. payload = {
  81. "moduleCode": module,
  82. "metricCode": diagnosis.get("metricCode") or root.get("metricCode"),
  83. "problemLevel": 1,
  84. "problemMetricCode": root.get("metricCode"),
  85. "problemName": root.get("metricName") or f"{module} 诊断问题",
  86. "problemDept": root.get("department"),
  87. "problemSeverity": "yellow",
  88. "isCrossDept": module == "S9",
  89. "targetValue": str(root.get("targetValue") or ""),
  90. "actualValue": str(root.get("currentValue") or ""),
  91. "gapLabel": root.get("gapLabel"),
  92. "rootCause": (
  93. f"[WP-SD7/{BATCH}] 依据事实 {first.get('objectType')} {first.get('objectCode')}:"
  94. f"{first.get('title')}。人工填写,非自动建议。"
  95. ),
  96. "actionItems": [
  97. {
  98. "content": f"核对事实 {first.get('objectCode')} 并关闭偏差",
  99. "dueDate": (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d"),
  100. "status": "pending",
  101. },
  102. {
  103. "content": "复盘看板筛选与无筛选口径是否一致",
  104. "dueDate": (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d"),
  105. "status": "pending",
  106. },
  107. ],
  108. "dueDate": (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%dT00:00:00"),
  109. }
  110. status, body = request("POST", "/api/AdoSmartOpsImprovementPlan/createFromDiagnosis", payload, token)
  111. return {"http": status, "body": unwrap(body) or {}}
  112. def approve_until_done(token: str, plan_id) -> list[dict]:
  113. steps = []
  114. for _ in range(6):
  115. status, body = request(
  116. "POST",
  117. "/api/flowTask/myPendingPage",
  118. {"page": 1, "pageSize": 50, "bizType": "SMART_OPS_IMPROVEMENT"},
  119. token,
  120. )
  121. page = unwrap(body) or {}
  122. items = page.get("items") or page.get("Items") or []
  123. match = None
  124. for item in items:
  125. biz_id = item.get("bizId") or item.get("BizId")
  126. if str(biz_id) == str(plan_id):
  127. match = item
  128. break
  129. if not match:
  130. steps.append({"done": True, "pending": len(items)})
  131. break
  132. task_id = match.get("id") or match.get("Id") or match.get("taskId")
  133. st, app = request("POST", "/api/flowTask/approve", {"taskId": task_id, "comment": "WP-SD7 正式审批通过"}, token)
  134. steps.append({"http": st, "taskId": str(task_id) if task_id else None, "body": unwrap(app)})
  135. return steps
  136. def complete_actions(token: str, plan: dict) -> list[dict]:
  137. results = []
  138. actions = plan.get("actionItems") or plan.get("ActionItems") or []
  139. plan_id = plan.get("id") or plan.get("Id")
  140. today = datetime.now().strftime("%Y-%m-%d")
  141. for action in actions:
  142. action_id = action.get("id") or action.get("Id")
  143. steps = []
  144. current = (action.get("status") or action.get("Status") or "pending").lower()
  145. wanted = []
  146. if current == "pending":
  147. wanted.append("in_progress")
  148. if current in ("pending", "in_progress", "doing"):
  149. wanted.append("completed")
  150. for status in wanted:
  151. st, body = request(
  152. "POST",
  153. "/api/AdoSmartOpsImprovementPlan/updateActionItem",
  154. {
  155. "id": plan_id,
  156. "actionId": action_id,
  157. "status": status,
  158. "completedAt": today if status == "completed" else None,
  159. "proofRemark": "WP-SD7 按事实核对后关闭,非自动建议",
  160. },
  161. token,
  162. )
  163. msg = body.get("message") if isinstance(body, dict) else None
  164. if not msg and isinstance(unwrap(body), str):
  165. msg = unwrap(body)
  166. steps.append({"http": st, "status": status, "message": msg})
  167. nxt = unwrap(body)
  168. if isinstance(nxt, dict):
  169. plan = nxt
  170. results.append({"actionId": str(action_id) if action_id else None, "steps": steps})
  171. return results, plan
  172. def run_module(token: str, module: str) -> dict:
  173. board = dashboard(token, module)
  174. global_diag = diagnose(token, module)
  175. body = global_diag.get("body") or {}
  176. metric = body.get("metricCode")
  177. specified = diagnose(token, module, metric) if metric else None
  178. evidence = body.get("evidence") or {}
  179. created = create_plan(token, body, module) if evidence.get("items") else {
  180. "skipped": True,
  181. "reason": "no_evidence_or_diagnosis",
  182. }
  183. plan = as_dict(created.get("body"))
  184. plan_id = plan.get("id") or plan.get("Id")
  185. board_metrics = {
  186. (m.get("metricCode") or m.get("MetricCode")): (m.get("currentValue") or m.get("CurrentValue") or m.get("metricValue"))
  187. for m in ((board.get("body") or {}).get("metrics") or [])
  188. if (m.get("metricLevel") or m.get("MetricLevel") or m.get("level")) in (1, "1", None)
  189. }
  190. submit = approve = start = actions = verify = close = None
  191. if plan_id:
  192. st, sub = request("POST", "/api/AdoSmartOpsImprovementPlan/submitApproval", {"id": plan_id}, token)
  193. submit_msg = sub.get("message") if isinstance(sub, dict) else None
  194. submit = {"http": st, "message": submit_msg, "body": unwrap(sub)}
  195. submit_failed = st >= 400 or (isinstance(submit_msg, str) and ("失败" in submit_msg or "拒绝" in submit_msg))
  196. if submit_failed:
  197. return {
  198. "module": module,
  199. "dashboardHttp": board.get("http"),
  200. "global": {
  201. "http": global_diag["http"],
  202. "selectionMode": body.get("selectionMode"),
  203. "metricCode": metric,
  204. "hasChildren": body.get("hasChildren"),
  205. "rootStatus": (body.get("root") or {}).get("status"),
  206. "evidenceScope": evidence.get("scope"),
  207. "evidenceTotal": evidence.get("total"),
  208. "firstFact": ((evidence.get("items") or [{}])[0] or {}).get("objectCode"),
  209. },
  210. "specified_same_as_current": None
  211. if not specified
  212. else (specified.get("body") or {}).get("metricCode") == metric,
  213. "kanban_root_present": True if module == "S8" else (metric in board_metrics if metric else False),
  214. "create": {
  215. "http": created.get("http"),
  216. "skipped": created.get("skipped"),
  217. "reason": created.get("reason"),
  218. "planNo": plan.get("planNo") or plan.get("PlanNo"),
  219. "planId": str(plan_id) if plan_id else None,
  220. },
  221. "submitApproval": submit,
  222. "loopClosed": False,
  223. }
  224. approve = approve_until_done(token, plan_id)
  225. st, started = request("POST", "/api/AdoSmartOpsImprovementPlan/startExecution", {"id": plan_id}, token)
  226. started_body = as_dict(unwrap(started), plan)
  227. start = {"http": st, "status": started_body.get("status") or started_body.get("Status")}
  228. actions, plan = complete_actions(token, started_body)
  229. st, ver = request(
  230. "POST",
  231. "/api/AdoSmartOpsImprovementPlan/submitVerification",
  232. {"id": plan_id, "verifyResult": "INSUFFICIENT_DATA", "verifyRemark": "样本不足时与自动评价一致"},
  233. token,
  234. )
  235. verify = {
  236. "http": st,
  237. "auto": (unwrap(ver) or {}).get("autoVerifyResult") or (unwrap(ver) or {}).get("AutoVerifyResult"),
  238. "status": (unwrap(ver) or {}).get("status") or (unwrap(ver) or {}).get("Status"),
  239. }
  240. st, closed = request("POST", "/api/AdoSmartOpsImprovementPlan/close", {"id": plan_id}, token)
  241. close = {"http": st, "status": (unwrap(closed) or {}).get("status") or (unwrap(closed) or {}).get("Status")}
  242. return {
  243. "module": module,
  244. "dashboardHttp": board.get("http"),
  245. "global": {
  246. "http": global_diag["http"],
  247. "selectionMode": body.get("selectionMode"),
  248. "metricCode": metric,
  249. "hasChildren": body.get("hasChildren"),
  250. "rootStatus": (body.get("root") or {}).get("status"),
  251. "evidenceScope": evidence.get("scope"),
  252. "evidenceTotal": evidence.get("total"),
  253. "firstFact": ((evidence.get("items") or [{}])[0] or {}).get("objectCode"),
  254. },
  255. "specified_same_as_current": None
  256. if not specified
  257. else (specified.get("body") or {}).get("metricCode") == metric,
  258. "kanban_root_present": True if module == "S8" else (metric in board_metrics if metric else False),
  259. "create": {
  260. "http": created.get("http"),
  261. "skipped": created.get("skipped"),
  262. "reason": created.get("reason"),
  263. "planNo": plan.get("planNo") or plan.get("PlanNo"),
  264. "planId": str(plan_id) if plan_id else None,
  265. },
  266. "submitApproval": {"http": (submit or {}).get("http"), "message": (submit or {}).get("message")} if submit else None,
  267. "approveSteps": [{"http": x.get("http"), "done": x.get("done")} for x in (approve or [])],
  268. "startExecution": start,
  269. "actionsCompleted": actions,
  270. "verify": verify,
  271. "close": close,
  272. "loopClosed": (close or {}).get("status") == "closed",
  273. }
  274. def main() -> None:
  275. parser = argparse.ArgumentParser()
  276. parser.add_argument("--tenant", choices=TENANTS.keys(), default="A")
  277. parser.add_argument("--modules", default="S1,S2,S3,S4,S5,S6,S7,S9")
  278. parser.add_argument("--password", default="")
  279. parser.add_argument("--out", default="", help="Evidence filename under the UAT folder")
  280. args = parser.parse_args()
  281. password = args.password or os.environ.get("AIDOP_UAT_PASSWORD", "").strip()
  282. if not password:
  283. raise SystemExit("AIDOP_UAT_PASSWORD or --password is required")
  284. tenant = TENANTS[args.tenant]
  285. token = login(tenant["account"], tenant["tenant_id"], password)
  286. report = {
  287. "work_package": "WP-SD7-API",
  288. "generated_at": datetime.now().isoformat(timespec="seconds"),
  289. "tenant": args.tenant,
  290. "account": tenant["account"],
  291. "auto_passed": False,
  292. "modules": [],
  293. }
  294. for module in [x.strip().upper() for x in args.modules.split(",") if x.strip()]:
  295. report["modules"].append(run_module(token, module))
  296. report["closed_count"] = sum(1 for x in report["modules"] if x.get("loopClosed"))
  297. report["blocked_count"] = sum(1 for x in report["modules"] if not x.get("loopClosed"))
  298. out = EVIDENCE / (args.out or f"07-api-loop-{args.tenant}.json")
  299. out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  300. print(out)
  301. print(json.dumps(
  302. {
  303. "tenant": args.tenant,
  304. "closed": report["closed_count"],
  305. "blocked": report["blocked_count"],
  306. "modules": [
  307. f"{x['module']}:{x['global']['evidenceScope']}:closed={x.get('loopClosed')}"
  308. for x in report["modules"]
  309. ],
  310. },
  311. ensure_ascii=False,
  312. ))
  313. if __name__ == "__main__":
  314. main()