| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- #!/usr/bin/env python3
- """WP-SD7 live API loop: diagnose, create, approve, execute, verify, close.
- Passwords come from the environment and are never written to evidence.
- Does not hand-edit plan status in the database.
- """
- from __future__ import annotations
- import argparse
- import json
- import os
- import urllib.error
- import urllib.request
- from datetime import datetime, timedelta
- from pathlib import Path
- from gmssl import sm2
- ROOT = Path(__file__).resolve().parents[4]
- EVIDENCE = ROOT / "doc" / "plan" / "UAT留证" / "2026-08-17-智慧诊断正式落地"
- BASE = os.environ.get("AIDOP_API_BASE", "http://127.0.0.1:5005")
- SM2_PK = "84C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312"
- BATCH = datetime.now().strftime("%Y%m%d")
- TENANTS = {
- "A": {"account": "UATAdminA", "tenant_id": "838257186181189", "user_id": "838257187360837"},
- "B": {"account": "UATAdminB", "tenant_id": "838257212780613", "user_id": "838257213620293"},
- "DEMO": {"account": "UATDemoAdmin", "tenant_id": "838257237606469", "user_id": "838257238302789"},
- }
- def request(method: str, path: str, payload=None, token=None):
- headers = {"Accept": "application/json"}
- data = None
- if payload is not None:
- headers["Content-Type"] = "application/json"
- data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
- if token:
- headers["Authorization"] = "Bearer " + token
- req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
- try:
- with urllib.request.urlopen(req, timeout=120) as resp:
- raw = resp.read()
- return resp.status, json.loads(raw) if raw else {}
- except urllib.error.HTTPError as exc:
- raw = exc.read()
- try:
- body = json.loads(raw) if raw else {}
- except json.JSONDecodeError:
- body = {"raw": raw.decode("utf-8", errors="replace")}
- return exc.code, body
- def encrypt(value: str) -> str:
- return sm2.CryptSM2(public_key=SM2_PK, private_key=None, mode=1).encrypt(value.encode()).hex()
- def unwrap(body):
- if isinstance(body, dict) and "result" in body:
- return body.get("result")
- return body
- def as_dict(value, fallback=None):
- return value if isinstance(value, dict) else (fallback or {})
- def login(account: str, tenant_id: str, password: str, already_encrypted: bool = False) -> str:
- cipher = password if already_encrypted else encrypt(password)
- status, body = request(
- "POST",
- "/api/sysAuth/login",
- {"account": account, "password": cipher, "tenantId": tenant_id},
- )
- token = (unwrap(body) or {}).get("accessToken") if isinstance(unwrap(body), dict) else None
- if status != 200 or not token:
- raise SystemExit(f"login failed for {account}: HTTP {status} {body}")
- return token
- def diagnose(token: str, module: str, metric_code: str | None = None) -> dict:
- path = f"/api/AidopKanban/smart-diagnosis/{module}"
- if metric_code:
- path += f"?metricCode={metric_code}"
- status, body = request("GET", path, token=token)
- return {"http": status, "body": unwrap(body) or {}}
- def dashboard(token: str, module: str) -> dict:
- status, body = request("GET", f"/api/AidopKanban/dashboard-page/{module}", token=token)
- return {"http": status, "body": unwrap(body) or {}}
- def create_plan(token: str, diagnosis: dict, module: str) -> dict:
- root = diagnosis.get("root") or {}
- evidence = diagnosis.get("evidence") or {}
- items = evidence.get("items") or []
- if not items:
- return {"skipped": True, "reason": "no_evidence_items"}
- first = items[0]
- payload = {
- "moduleCode": module,
- "metricCode": diagnosis.get("metricCode") or root.get("metricCode"),
- "problemLevel": 1,
- "problemMetricCode": root.get("metricCode"),
- "problemName": root.get("metricName") or f"{module} 诊断问题",
- "problemDept": root.get("department"),
- "problemSeverity": "yellow",
- "isCrossDept": module == "S9",
- "targetValue": str(root.get("targetValue") or ""),
- "actualValue": str(root.get("currentValue") or ""),
- "gapLabel": root.get("gapLabel"),
- "rootCause": (
- f"[WP-SD7/{BATCH}] 依据事实 {first.get('objectType')} {first.get('objectCode')}:"
- f"{first.get('title')}。人工填写,非自动建议。"
- ),
- "actionItems": [
- {
- "content": f"核对事实 {first.get('objectCode')} 并关闭偏差",
- "dueDate": (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d"),
- "status": "pending",
- },
- {
- "content": "复盘看板筛选与无筛选口径是否一致",
- "dueDate": (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d"),
- "status": "pending",
- },
- ],
- "dueDate": (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%dT00:00:00"),
- }
- status, body = request("POST", "/api/AdoSmartOpsImprovementPlan/createFromDiagnosis", payload, token)
- return {"http": status, "body": unwrap(body) or {}}
- def approve_until_done(token: str, plan_id) -> list[dict]:
- steps = []
- for _ in range(6):
- status, body = request(
- "POST",
- "/api/flowTask/myPendingPage",
- {"page": 1, "pageSize": 50, "bizType": "SMART_OPS_IMPROVEMENT"},
- token,
- )
- page = unwrap(body) or {}
- items = page.get("items") or page.get("Items") or []
- match = None
- for item in items:
- biz_id = item.get("bizId") or item.get("BizId")
- if str(biz_id) == str(plan_id):
- match = item
- break
- if not match:
- steps.append({"done": True, "pending": len(items)})
- break
- task_id = match.get("id") or match.get("Id") or match.get("taskId")
- st, app = request("POST", "/api/flowTask/approve", {"taskId": task_id, "comment": "WP-SD7 正式审批通过"}, token)
- steps.append({"http": st, "taskId": str(task_id) if task_id else None, "body": unwrap(app)})
- return steps
- def complete_actions(token: str, plan: dict) -> list[dict]:
- results = []
- actions = plan.get("actionItems") or plan.get("ActionItems") or []
- plan_id = plan.get("id") or plan.get("Id")
- today = datetime.now().strftime("%Y-%m-%d")
- for action in actions:
- action_id = action.get("id") or action.get("Id")
- steps = []
- current = (action.get("status") or action.get("Status") or "pending").lower()
- wanted = []
- if current == "pending":
- wanted.append("in_progress")
- if current in ("pending", "in_progress", "doing"):
- wanted.append("completed")
- for status in wanted:
- st, body = request(
- "POST",
- "/api/AdoSmartOpsImprovementPlan/updateActionItem",
- {
- "id": plan_id,
- "actionId": action_id,
- "status": status,
- "completedAt": today if status == "completed" else None,
- "proofRemark": "WP-SD7 按事实核对后关闭,非自动建议",
- },
- token,
- )
- msg = body.get("message") if isinstance(body, dict) else None
- if not msg and isinstance(unwrap(body), str):
- msg = unwrap(body)
- steps.append({"http": st, "status": status, "message": msg})
- nxt = unwrap(body)
- if isinstance(nxt, dict):
- plan = nxt
- results.append({"actionId": str(action_id) if action_id else None, "steps": steps})
- return results, plan
- def run_module(token: str, module: str) -> dict:
- board = dashboard(token, module)
- global_diag = diagnose(token, module)
- body = global_diag.get("body") or {}
- metric = body.get("metricCode")
- specified = diagnose(token, module, metric) if metric else None
- evidence = body.get("evidence") or {}
- created = create_plan(token, body, module) if evidence.get("items") else {
- "skipped": True,
- "reason": "no_evidence_or_diagnosis",
- }
- plan = as_dict(created.get("body"))
- plan_id = plan.get("id") or plan.get("Id")
- board_metrics = {
- (m.get("metricCode") or m.get("MetricCode")): (m.get("currentValue") or m.get("CurrentValue") or m.get("metricValue"))
- for m in ((board.get("body") or {}).get("metrics") or [])
- if (m.get("metricLevel") or m.get("MetricLevel") or m.get("level")) in (1, "1", None)
- }
- submit = approve = start = actions = verify = close = None
- if plan_id:
- st, sub = request("POST", "/api/AdoSmartOpsImprovementPlan/submitApproval", {"id": plan_id}, token)
- submit_msg = sub.get("message") if isinstance(sub, dict) else None
- submit = {"http": st, "message": submit_msg, "body": unwrap(sub)}
- submit_failed = st >= 400 or (isinstance(submit_msg, str) and ("失败" in submit_msg or "拒绝" in submit_msg))
- if submit_failed:
- return {
- "module": module,
- "dashboardHttp": board.get("http"),
- "global": {
- "http": global_diag["http"],
- "selectionMode": body.get("selectionMode"),
- "metricCode": metric,
- "hasChildren": body.get("hasChildren"),
- "rootStatus": (body.get("root") or {}).get("status"),
- "evidenceScope": evidence.get("scope"),
- "evidenceTotal": evidence.get("total"),
- "firstFact": ((evidence.get("items") or [{}])[0] or {}).get("objectCode"),
- },
- "specified_same_as_current": None
- if not specified
- else (specified.get("body") or {}).get("metricCode") == metric,
- "kanban_root_present": True if module == "S8" else (metric in board_metrics if metric else False),
- "create": {
- "http": created.get("http"),
- "skipped": created.get("skipped"),
- "reason": created.get("reason"),
- "planNo": plan.get("planNo") or plan.get("PlanNo"),
- "planId": str(plan_id) if plan_id else None,
- },
- "submitApproval": submit,
- "loopClosed": False,
- }
- approve = approve_until_done(token, plan_id)
- st, started = request("POST", "/api/AdoSmartOpsImprovementPlan/startExecution", {"id": plan_id}, token)
- started_body = as_dict(unwrap(started), plan)
- start = {"http": st, "status": started_body.get("status") or started_body.get("Status")}
- actions, plan = complete_actions(token, started_body)
- st, ver = request(
- "POST",
- "/api/AdoSmartOpsImprovementPlan/submitVerification",
- {"id": plan_id, "verifyResult": "INSUFFICIENT_DATA", "verifyRemark": "样本不足时与自动评价一致"},
- token,
- )
- verify = {
- "http": st,
- "auto": (unwrap(ver) or {}).get("autoVerifyResult") or (unwrap(ver) or {}).get("AutoVerifyResult"),
- "status": (unwrap(ver) or {}).get("status") or (unwrap(ver) or {}).get("Status"),
- }
- st, closed = request("POST", "/api/AdoSmartOpsImprovementPlan/close", {"id": plan_id}, token)
- close = {"http": st, "status": (unwrap(closed) or {}).get("status") or (unwrap(closed) or {}).get("Status")}
- return {
- "module": module,
- "dashboardHttp": board.get("http"),
- "global": {
- "http": global_diag["http"],
- "selectionMode": body.get("selectionMode"),
- "metricCode": metric,
- "hasChildren": body.get("hasChildren"),
- "rootStatus": (body.get("root") or {}).get("status"),
- "evidenceScope": evidence.get("scope"),
- "evidenceTotal": evidence.get("total"),
- "firstFact": ((evidence.get("items") or [{}])[0] or {}).get("objectCode"),
- },
- "specified_same_as_current": None
- if not specified
- else (specified.get("body") or {}).get("metricCode") == metric,
- "kanban_root_present": True if module == "S8" else (metric in board_metrics if metric else False),
- "create": {
- "http": created.get("http"),
- "skipped": created.get("skipped"),
- "reason": created.get("reason"),
- "planNo": plan.get("planNo") or plan.get("PlanNo"),
- "planId": str(plan_id) if plan_id else None,
- },
- "submitApproval": {"http": (submit or {}).get("http"), "message": (submit or {}).get("message")} if submit else None,
- "approveSteps": [{"http": x.get("http"), "done": x.get("done")} for x in (approve or [])],
- "startExecution": start,
- "actionsCompleted": actions,
- "verify": verify,
- "close": close,
- "loopClosed": (close or {}).get("status") == "closed",
- }
- def main() -> None:
- parser = argparse.ArgumentParser()
- parser.add_argument("--tenant", choices=TENANTS.keys(), default="A")
- parser.add_argument("--modules", default="S1,S2,S3,S4,S5,S6,S7,S9")
- parser.add_argument("--password", default="")
- parser.add_argument("--out", default="", help="Evidence filename under the UAT folder")
- args = parser.parse_args()
- password = args.password or os.environ.get("AIDOP_UAT_PASSWORD", "").strip()
- if not password:
- raise SystemExit("AIDOP_UAT_PASSWORD or --password is required")
- tenant = TENANTS[args.tenant]
- token = login(tenant["account"], tenant["tenant_id"], password)
- report = {
- "work_package": "WP-SD7-API",
- "generated_at": datetime.now().isoformat(timespec="seconds"),
- "tenant": args.tenant,
- "account": tenant["account"],
- "auto_passed": False,
- "modules": [],
- }
- for module in [x.strip().upper() for x in args.modules.split(",") if x.strip()]:
- report["modules"].append(run_module(token, module))
- report["closed_count"] = sum(1 for x in report["modules"] if x.get("loopClosed"))
- report["blocked_count"] = sum(1 for x in report["modules"] if not x.get("loopClosed"))
- out = EVIDENCE / (args.out or f"07-api-loop-{args.tenant}.json")
- out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
- print(out)
- print(json.dumps(
- {
- "tenant": args.tenant,
- "closed": report["closed_count"],
- "blocked": report["blocked_count"],
- "modules": [
- f"{x['module']}:{x['global']['evidenceScope']}:closed={x.get('loopClosed')}"
- for x in report["modules"]
- ],
- },
- ensure_ascii=False,
- ))
- if __name__ == "__main__":
- main()
|