新增 AutoRDO 需求清洗工作台与消息中枢,迭代 OneOS V2 设计规范及租赁合同/工作台/车辆等原型,同步云效技能与导航注册;并归档一批 legacy 原型快照。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
227
.cursor/skills/YunxiaoPM/scripts/list_projects.py
Normal file
227
.cursor/skills/YunxiaoPM/scripts/list_projects.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user