| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- # -*- coding: utf-8 -*-
- """sync_pages.py —— 网页指引同步工具(Intent F 的核心实现)
- 功能:
- 1. 调平台权限接口拉取全量菜单树(权威路由清单,无需遍历爬取)
- 2. 与 pages.yaml 比对:报告新增页面(菜单有、注册表无)/ 疑似下线页面(注册表可见、菜单无)
- 3. 为新增页面生成 pages.yaml 草稿片段(desc/usage 留 TODO,由人工或智能体补写)
- 4. 输出到 stdout 与 sync_draft.yaml(只读比对 + 草稿,不改动 pages.yaml)
- 用法:
- python sync_pages.py <JWT_TOKEN> # token 也可用环境变量 TF_TOKEN
- python sync_pages.py <JWT_TOKEN> --base-url http://x.x.x.x:8092 # 覆盖 config.yaml
- 说明:
- - 只比对叶子菜单(有 children 的容器节点跳过)
- - 注册表的 path 与 menu_key 都参与比对(设备详情页 path 带 ?id=,按 menu_key 认领)
- - HTTP 状态无法识别 SPA 的客户端 404,页面可用性验证需浏览器渲染(见 SKILL.md 流程 F 第3步)
- """
- import io
- import json
- import os
- import sys
- import urllib.request
- from pathlib import Path
- import yaml
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
- HERE = Path(__file__).parent
- ROOT = HERE.parent
- # ---------- 参数 ----------
- token = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("TF_TOKEN", "")
- base_url = None
- if "--base-url" in sys.argv:
- base_url = sys.argv[sys.argv.index("--base-url") + 1]
- if not token:
- print("用法: python sync_pages.py <JWT_TOKEN>(或设环境变量 TF_TOKEN)")
- sys.exit(1)
- cfg = yaml.safe_load((ROOT / "config.yaml").read_text(encoding="utf-8"))
- base_url = (base_url or cfg["base_url"]).rstrip("/")
- # ---------- 拉取菜单树 ----------
- url = f"{base_url}/modelreq/sys/permissionNew/getUserPermissionByToken?token={token}"
- req = urllib.request.Request(url, headers={"X-Access-Token": token})
- with urllib.request.urlopen(req, timeout=15) as resp:
- payload = json.loads(resp.read().decode("utf-8"))
- if not payload.get("success"):
- print(f"权限接口调用失败: {payload.get('message')}(token 是否过期?重新登录平台后从请求头 X-Access-Token 复制)")
- sys.exit(1)
- leaves = [] # 叶子菜单(新增比对用)
- all_paths = set() # 全部节点路径含容器(下线比对用,避免父节点误报)
- def walk(nodes, parent):
- for n in nodes or []:
- path = n.get("path") or ""
- if path:
- all_paths.add(path.split("?")[0])
- if n.get("children"):
- walk(n["children"], n.get("meta", {}).get("title") or parent)
- else:
- leaves.append((n.get("meta", {}).get("title") or "?",
- path, bool(n.get("hidden")), parent))
- walk(payload["result"]["menu"], "")
- # ---------- 读取注册表 ----------
- data = yaml.safe_load((HERE / "pages.yaml").read_text(encoding="utf-8"))
- reg_paths, reg_keys = set(), set()
- for cat in data["categories"]:
- items = list(cat.get("pages") or [])
- for g in cat.get("groups") or []:
- items += g.get("pages") or []
- for p in items:
- reg_paths.add(p.get("path", "").split("?")[0])
- if p.get("menu_key"):
- reg_keys.add(p["menu_key"].split("?")[0])
- # ---------- 差集 ----------
- new_pages, known = [], []
- for title, path, hidden, parent in leaves:
- base = path.split("?")[0]
- if base in reg_paths or base in reg_keys:
- known.append(title)
- else:
- new_pages.append((title, path, hidden, parent))
- reg_visible_keys = set()
- for cat in data["categories"]:
- items = list(cat.get("pages") or [])
- for g in cat.get("groups") or []:
- items += g.get("pages") or []
- for p in items:
- if p.get("visible", True) and p.get("menu_key"):
- reg_visible_keys.add((p["name"], p["menu_key"].split("?")[0]))
- menu_bases = all_paths
- removed = [(n, k) for n, k in reg_visible_keys if k not in menu_bases]
- # ---------- 报告 ----------
- print(f"菜单叶子页: {len(leaves)} | 注册表已认领: {len(known)} | 菜单新增: {len(new_pages)} | 疑似下线: {len(removed)}")
- print(f"\n== 菜单新增(候选待录入) ==")
- for t, p, h, par in new_pages:
- print(f" [{'hidden' if h else '可见 '}] {par} > {t} {p}")
- print(f"\n== 疑似下线/权限变化(注册表可见但当前账号菜单中无) ==")
- for n, k in removed:
- print(f" {n} {k}")
- # ---------- 草稿 ----------
- if new_pages:
- lines = ["# ==== 同步草稿(生成于菜单比对,请确认后合并进 pages.yaml 对应分类) ===="]
- for t, p, h, par in new_pages:
- lines += [
- f" - id: TODO-{abs(hash(t)) % 100000}",
- f" name: {t}",
- f" aliases: [] # TODO 补口语别名",
- f" keywords: [] # TODO 补关键词",
- f" menu_key: {p.split('?')[0]}",
- f" path: {p}",
- f" desc: TODO # TODO 浏览器打开页面阅读内容后拟写(≤60字)",
- f" usage: null # TODO 实测后补操作步骤",
- f" visible: false # 验证可用后删除此行启用",
- f" note: 同步自动发现({par}),{'隐藏菜单页' if h else '可见菜单页'}",
- ]
- (HERE / "sync_draft.yaml").write_text("\n".join(lines) + "\n", encoding="utf-8")
- print(f"\n草稿已生成: references/sync_draft.yaml(合并后删除该文件,并运行 build_pages.py)")
|