Files
iAOP/deploy/fba/seed_iaop_menus.py
T

197 lines
8.4 KiB
Python
Raw 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
"""iAOP 菜单/角色播种到 FBA(Epic #159 · Phase 2)。
把 iAOP 的 6 个功能模块注册为 FBA sys_menu,并创建 engineer / viewer 角色、
按 PRD 8.2 三级权限分配菜单,使 Sider 菜单可由 FBA 服务端按角色授权驱动
(前端 web/shared/session.js 的 menuTree() 消费 /sys/menus/sidebar)。
命名约定(与 session.js 的匹配逻辑对应,改动需双侧同步):
· 模块菜单 name = 模块 key(cockpit/chat/studio/admin/users/mobile)
· 子菜单 name = 子页 key(import/hyper/layout/version/models/kb/alerts/audit)
· studio/admin 为目录(type=0)挂子菜单(type=1),其余为叶子菜单(type=1)
幂等:按 name 查重,已存在的菜单/角色跳过创建,只补齐缺失项;
角色菜单关联每次按目标集合全量刷新(PUT /sys/roles/{id}/menus)。
用法(服务器上执行,仅需 Python 3.8+ 标准库):
python3 seed_iaop_menus.py \
--base http://127.0.0.1:8001/api/v1 \
--username admin --password '<FBA管理员密码>'
说明:登录走 /auth/login/swagger(HTTP Basic,免图形验证码),
专为运维/调试通道;不依赖 LOGIN_CAPTCHA_ENABLED 开关。
"""
import argparse
import base64
import getpass
import json
import sys
import urllib.error
import urllib.request
# ---- iAOP 模块注册表(镜像 web/shared/session.js 的 MODULES,顺序即菜单顺序) ----
MODULES = [
{"key": "cockpit", "title": "配置化驾驶舱", "icon": "🚀", "path": "/cockpit/"},
{"key": "chat", "title": "对话助手", "icon": "💬", "path": "/chat/assistant.html"},
{"key": "studio", "title": "模板配置台", "icon": "🧩", "path": "/studio/", "children": [
{"key": "import", "title": "点位导入"},
{"key": "hyper", "title": "模型超参"},
{"key": "layout", "title": "驾驶舱编排"},
{"key": "version", "title": "版本发布"},
]},
{"key": "admin", "title": "管理控制台", "icon": "⚙️", "path": "/admin/", "children": [
{"key": "models", "title": "模型管理"},
{"key": "kb", "title": "知识库管理"},
{"key": "alerts", "title": "告警确认"},
{"key": "audit", "title": "审计查询"},
]},
{"key": "users", "title": "用户与角色", "icon": "👤", "path": "/auth/users.html"},
{"key": "mobile", "title": "移动端驾驶舱", "icon": "📱", "path": "/mobile/"},
]
# 角色 → 可见模块(PRD 8.2;admin 通常为 is_superuser,无需分配,看到全部)
ROLE_MENUS = {
"viewer": ["cockpit", "chat", "mobile"],
"engineer": ["cockpit", "chat", "mobile", "studio"],
}
class FbaClient:
def __init__(self, base: str, username: str, password: str):
self.base = base.rstrip("/")
cred = base64.b64encode(f"{username}:{password}".encode()).decode()
d = self._req("POST", "/auth/login/swagger",
headers={"Authorization": f"Basic {cred}"})
self.token = d["data"]["access_token"]
print(f"[ok] 登录成功:{username}({d['data']['user']['username']})")
def _req(self, method: str, path: str, body=None, headers=None):
h = {"Content-Type": "application/json"}
if getattr(self, "token", None):
h["Authorization"] = f"Bearer {self.token}"
if headers:
h.update(headers)
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(self.base + path, data=data,
headers=h, method=method)
try:
with urllib.request.urlopen(req, timeout=15) as r:
d = json.loads(r.read().decode())
except urllib.error.HTTPError as e:
detail = e.read().decode(errors="replace")[:300]
raise SystemExit(f"[fail] {method} {path} → HTTP {e.code}: {detail}")
if d.get("code") != 200:
raise SystemExit(f"[fail] {method} {path} → {d.get('msg')}")
return d
def get(self, path):
return self._req("GET", path)["data"]
def post(self, path, body):
return self._req("POST", path, body)
def put(self, path, body):
return self._req("PUT", path, body)
def flatten_menu_names(tree, out=None):
"""菜单树 → {name: id}(含子级)"""
out = out if out is not None else {}
for node in tree or []:
if node.get("name"):
out[node["name"]] = node["id"]
flatten_menu_names(node.get("children"), out)
return out
def main():
ap = argparse.ArgumentParser(description="iAOP 菜单/角色播种到 FBA")
ap.add_argument("--base", default="http://127.0.0.1:8001/api/v1",
help="FBA API 根(直连容器端口用 8001;走 nginx 用 http://<host>/fba/api/v1)")
ap.add_argument("--username", default="admin")
ap.add_argument("--password", default=None, help="缺省时交互输入")
args = ap.parse_args()
password = args.password or getpass.getpass("FBA 管理员密码: ")
cli = FbaClient(args.base, args.username, password)
# ---- 1. 播种菜单(幂等:按 name 查重) --------------------------------
existing = flatten_menu_names(cli.get("/sys/menus"))
created, skipped = 0, 0
def ensure_menu(name, title, path, mtype, sort, parent_id=None):
nonlocal created, skipped
if name in existing:
skipped += 1
return existing[name]
body = {
"title": title, "name": name, "path": path,
"parent_id": parent_id, "sort": sort, "icon": None,
"type": mtype, # 0目录 1菜单
"component": None, "perms": f"iaop:{name}",
"status": 1, "display": 1, "cache": 0,
"link": None, "remark": "iAOP 模块(seed_iaop_menus.py)",
}
cli.post("/sys/menus", body)
created += 1
# 创建后重新拉取以拿到 id(接口不回传 id)
nonlocal_existing = flatten_menu_names(cli.get("/sys/menus"))
existing.update(nonlocal_existing)
print(f"[ok] 创建菜单 {name}({title})")
return existing[name]
for i, m in enumerate(MODULES):
children = m.get("children")
if children:
pid = ensure_menu(m["key"], m["title"], m["path"], 0, i)
for j, c in enumerate(children):
ensure_menu(c["key"], c["title"], f"{m['path']}#{c['key']}",
1, j, parent_id=pid)
else:
ensure_menu(m["key"], m["title"], m["path"], 1, i)
print(f"[ok] 菜单:新建 {created},已存在跳过 {skipped}")
# ---- 2. 播种角色并分配菜单 -------------------------------------------
menu_ids = flatten_menu_names(cli.get("/sys/menus"))
def module_menu_ids(mod_key):
"""模块目录 id + 其子菜单 id(叶子模块仅自身 id)"""
ids = [menu_ids[mod_key]]
mod = next(m for m in MODULES if m["key"] == mod_key)
for c in mod.get("children", []):
ids.append(menu_ids[c["key"]])
return ids
roles = {r["name"]: r["id"] for r in cli.get("/sys/roles/all")}
for role_name, mod_keys in ROLE_MENUS.items():
if role_name in roles:
rid = roles[role_name]
print(f"[ok] 角色已存在:{role_name}(id={rid}),刷新菜单关联")
else:
cli.post("/sys/roles", {
"name": role_name, "status": 1,
"is_filter_scopes": True,
"remark": "iAOP 角色(seed_iaop_menus.py)",
})
roles = {r["name"]: r["id"] for r in cli.get("/sys/roles/all")}
rid = roles[role_name]
print(f"[ok] 创建角色:{role_name}(id={rid})")
ids = []
for k in mod_keys:
ids.extend(module_menu_ids(k))
cli.put(f"/sys/roles/{rid}/menus", {"menus": ids})
print(f"[ok] 角色 {role_name} 菜单:{', '.join(mod_keys)}({len(ids)} 项)")
# ---- 3. 验证 sidebar ---------------------------------------------------
side = cli.get("/sys/menus/sidebar")
names = [n.get("name") for n in side or []]
print(f"[ok] 当前管理员 sidebar 可见菜单:{names}")
print("\n完成。后续:在 FBA 后台创建用户并分配 engineer/viewer 角色,"
"前端 Sider 即按服务端授权渲染(admin 为超管可见全部)。")
if __name__ == "__main__":
sys.exit(main())