Files
OneOS1.2/.cursor/skills/YunxiaoPM/scripts/list_projects.py

228 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""拉取云效项目列表(供 YunxiaoPMapp 门禁 PJ 点选。stdout = JSON。
无法对应时自动重拉一次:
python3 scripts/list_projects.py --match '01_ONEOS'
python3 scripts/list_projects.py --match-id 1280be963a5a2cc126a4118dca
"""
from __future__ import annotations
import argparse
import json
import urllib.parse
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
import requests
ROOT = Path(__file__).resolve().parents[1]
RUNTIME = json.loads((ROOT / "assets" / "runtime-ids.json").read_text())
ORG = RUNTIME.get("project", {}).get("organizationIdentifier", "697c54a19df7fdfa65466405")
WANG = RUNTIME.get("people", {}).get("wangmian", {}).get("identifier", "6811df000601d2fea60144a9")
ALIASES = [a.lower() for a in (RUNTIME.get("project", {}).get("name_aliases") or [])]
TZ = timezone(timedelta(hours=8))
def load_jar() -> dict[str, str]:
jar: dict[str, str] = {}
try:
import browser_cookie3
for domain in (".aliyun.com", "devops.aliyun.com", ".devops.aliyun.com"):
try:
for c in browser_cookie3.chrome(domain_name=domain):
jar[c.name] = c.value
except Exception:
pass
except Exception:
pass
if not jar.get("XSRF-TOKEN"):
p = Path("/tmp/yunxiao_cookies.json")
if p.exists():
raw = json.loads(p.read_text())
jar = (
raw
if isinstance(raw, dict) and "XSRF-TOKEN" in raw
else {c["name"]: c["value"] for c in raw.get("cookies", [])}
)
if not jar.get("XSRF-TOKEN"):
raise RuntimeError("缺少 XSRF-TOKEN请先在 Chrome 登录 devops.aliyun.com")
return jar
def fetch_projects() -> list[dict[str, Any]]:
jar = load_jar()
x = jar.get("XSRF-TOKEN", "")
xsrf = urllib.parse.unquote(x) if "%" in x else x
cookie = "; ".join(f"{k}={v}" for k, v in jar.items())
headers = {
"Cookie": cookie,
"x-xsrf-token": xsrf,
"X-XSRF-TOKEN": xsrf,
"Origin": "https://devops.aliyun.com",
"Referer": "https://devops.aliyun.com/projex",
"accept": "application/json",
}
extra = json.dumps(
{
"conditionGroups": [
[
{
"className": "user",
"fieldIdentifier": "users",
"format": "multiList",
"operator": "CONTAINS",
"value": [WANG],
}
],
[
{
"className": "string",
"fieldIdentifier": "scope",
"format": "list",
"operator": "CONTAINS",
"value": ["public"],
}
],
]
},
separators=(",", ":"),
)
params = {
"extraConditions": extra,
"conditions": json.dumps({"conditionGroups": [[]]}, separators=(",", ":")),
"orderBy": json.dumps(
{
"fieldIdentifier": "gmtCreate",
"format": "input",
"order": "desc",
"className": "date",
},
separators=(",", ":"),
),
"scope": "all",
"category": "Project",
"toPage": 1,
"pageSize": 100,
"_input_charset": "utf-8",
}
r = requests.get(
"https://devops.aliyun.com/projex/api/workspace/project/search/list",
headers=headers,
params=params,
timeout=30,
)
r.raise_for_status()
rows = r.json().get("result") or []
return [
{
"name": p.get("name"),
"identifier": p.get("identifier"),
"customCode": p.get("customCode"),
"logicalStatus": p.get("logicalStatus"),
"status": (p.get("status") or {}).get("displayName"),
"scope": p.get("scope"),
}
for p in rows
]
def match_projects(
projects: list[dict[str, Any]],
*,
query: str | None = None,
space_id: str | None = None,
) -> list[dict[str, Any]]:
if space_id:
sid = space_id.strip()
return [p for p in projects if p.get("identifier") == sid]
if not query:
return []
q = query.strip().lower()
hits: list[dict[str, Any]] = []
for p in projects:
name = (p.get("name") or "").lower()
code = (p.get("customCode") or "").lower()
if q == name or q == code or q in name or name in q:
hits.append(p)
continue
if q in ALIASES and (code == "oneos" or "oneos" in name or "运营" in name):
hits.append(p)
# de-dupe by identifier
seen: set[str] = set()
out: list[dict[str, Any]] = []
for p in hits:
i = p.get("identifier") or ""
if i in seen:
continue
seen.add(i)
out.append(p)
return out
def list_projects(
*,
match: str | None = None,
match_id: str | None = None,
) -> dict:
projects = fetch_projects()
payload: dict[str, Any] = {
"fetched_at": datetime.now(TZ).isoformat(),
"organizationIdentifier": ORG,
"count": len(projects),
"projects": projects,
"selection_required": True,
"refetched": False,
"note": "禁止默认直指;须 AskQuestion/Plan 点选后再写入 spaceIdentifier",
}
if not match and not match_id:
return payload
hits = match_projects(projects, query=match, space_id=match_id)
if len(hits) == 1:
payload["matches"] = hits
payload["match_status"] = "unique"
return payload
# 无法对应0 或多)→ 自动重拉一次
projects2 = fetch_projects()
hits2 = match_projects(projects2, query=match, space_id=match_id)
payload["projects"] = projects2
payload["count"] = len(projects2)
payload["refetched"] = True
payload["refetch_reason"] = "无法对应" if len(hits) != 1 else "unexpected"
if len(hits) == 0:
payload["refetch_reason"] = "首次0命中"
elif len(hits) > 1:
payload["refetch_reason"] = "首次多命中"
payload["matches"] = hits2
if len(hits2) == 1:
payload["match_status"] = "unique_after_refetch"
elif len(hits2) == 0:
payload["match_status"] = "none_after_refetch"
payload["note"] = "已自动重拉一次仍无法对应请用户从最新列表点选禁止再拉第3次"
else:
payload["match_status"] = "ambiguous_after_refetch"
payload["note"] = "已自动重拉一次仍多命中;请用户点选;禁止自动选定"
payload["fetched_at"] = datetime.now(TZ).isoformat()
return payload
def main() -> None:
ap = argparse.ArgumentParser(description="YunxiaoPMapp 项目列表 / 匹配(无法对应则重拉一次)")
ap.add_argument("--match", help="按项目名或 customCode 匹配")
ap.add_argument("--match-id", help="按 spaceIdentifier 匹配")
args = ap.parse_args()
print(
json.dumps(
list_projects(match=args.match, match_id=args.match_id),
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
main()